Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d5455ec68 | ||
|
|
c67b2006aa | ||
|
|
bd1b2a8f1d | ||
|
|
d970d75be8 | ||
|
|
971206c2ea | ||
|
|
018c0e1ec8 | ||
|
|
bd68aee1ca | ||
|
|
81e2452a3d | ||
|
|
bd7bba994c | ||
|
|
087333bf8a | ||
|
|
5e2fc53d39 | ||
|
|
47148e0a0d | ||
|
|
e157847509 | ||
|
|
9136f5f0e7 | ||
|
|
fc9227f9e0 | ||
|
|
32021930d2 | ||
|
|
135957831b | ||
|
|
c0c204ba50 | ||
|
|
fbae82e024 |
No files matched your search
@@ -2265,36 +2265,56 @@ mouth and off the walk route. It stays disabled by default. Active records carry
|
||||
L43's report records the off/on hashes, walk and interface audits, outside-stamp timing, tier
|
||||
results, captures and any failed runs.
|
||||
|
||||
### 8.54 Opt-in native lattice field — World Assembler M3 R26 slices 1–2 (Linux L58–L59)
|
||||
### 8.54 Opt-in native lattice field — World Assembler M3 R26 slice 1 (Linux L58)
|
||||
|
||||
`Core.Lattice.PrimitiveField` is an opt-in recipe selected only for one Maze or VerticalShafts
|
||||
`Core.Lattice.PrimitiveField` is a version-1 recipe selected only for one Maze or VerticalShafts
|
||||
strate by `--recipe=lattice --lattice-strate=N`. The legacy generator assets and their default
|
||||
dispatch stay unchanged. Version 2 binds the actual incoming lower mouth and outgoing upper mouth,
|
||||
then creates arrival/exit overlap corridors, an open vertical shaft and the maze spine; two
|
||||
deterministic dead ends are added after that route. Resolved room and edge seeds use
|
||||
`(world seed, strate, recipe version, stable ID)`. The orphan control translates the shaft 20 m
|
||||
away from both route junctions and must fail reachability.
|
||||
dispatch stay unchanged. Planning first binds the actual incoming lower mouth and outgoing upper
|
||||
mouth from the resolved passage records, then creates arrival/exit overlap corridors, a mandatory
|
||||
four-turn helix and the maze spine; two deterministic dead ends are added after that route. The
|
||||
resolved room and edge seeds use `(world seed, strate, recipe version, stable ID)`. An audit control
|
||||
translates the helix 20 m away from both route junctions to make the shaft network orphaned.
|
||||
|
||||
The version-2 shaft core is a vertical cylinder of 4.5 m radius. Its air continues 8 m above the
|
||||
top tread and 2 m below the bottom tread. The ramp winds around the core at a 6 m centreline radius,
|
||||
with 3 m clear width, 5 m pitch and four turns. The open annular shaft air joins the core air, while
|
||||
one-metre support slabs follow the helix under each tread. This leaves a 4 m vertical gap between
|
||||
one tread and the underside of the next. The ledge's inner edge is open to the well; the route
|
||||
centreline stays 1.5 m from the drop and leaves 1.16 m beyond the 0.34 m capsule radius. Its inner
|
||||
edge grade is about 10.0 degrees. The maze retains 2.6 m room headroom. The shaft's padded outer
|
||||
design envelope is 21 m. The 1.9 m floor-wall smooth-max gives an isolated ideal fillet radius of
|
||||
0.6718 m; the separate 0.05 m feature-union blend keeps route junctions connected.
|
||||
The prototype helix uses a 6 m centreline radius, 3 m clear width, 5 m pitch, four turns and 2.6 m
|
||||
finished floor-to-soffit height. Its centreline descends 20 m over roughly 151 m; the inner-edge
|
||||
grade is about 10.0 degrees. The 2.4 m floor-wall smooth-max gives an isolated ideal fillet radius
|
||||
of 0.8485 m. A separate 0.4 m feature-union blend prevents adjoining ramps from erasing the tread.
|
||||
The swept-clear diameter is 15 m; the conservative padded feature envelope is about 20.6 m (the
|
||||
approved approximately 21 m site envelope).
|
||||
|
||||
The layout reuses the resolved cavern graph vocabulary. A per-strate opt-in selects the native
|
||||
negative-solid/positive-air field; canonical feature order is stable-ID order. The ring-shaped shaft
|
||||
air and its support layers use an analytic helix lookup instead of testing every polyline segment
|
||||
per density sample. The core cylinder and annular wall have conservative interval bounds; false
|
||||
all-air and all-solid controls remain rejected, and `isTileShapeFree` refuses occupied tiles. The
|
||||
L59 pipeline checks the composed field from actual mouth to mouth with a 0.34 m by 1.76 m capsule,
|
||||
support at every station, 2.2 m clearance and a 15 degree grade limit. It also checks the orphan
|
||||
control, whole-surface ramp lip/wall-foot/underside, brute-force interval samples and the composed
|
||||
wall-foot fillet. See the Linux58 and Linux59 reports for evidence. This code path is not promoted
|
||||
to legacy Maze/VerticalShafts generation.
|
||||
negative-solid/positive-air field; canonical feature order is stable-ID order. Room/corridor bounds
|
||||
are indexed by the graph BVH and polyline segments have a nested deterministic BVH for density and
|
||||
interval queries. Box classification stays conservative, and `isTileShapeFree` refuses any tile
|
||||
that intersects the graph. The L58 pipeline audits the composed field from actual mouth to mouth
|
||||
with a 0.34 m by 1.76 m capsule, per-station support, 2.2 m clearance and a 15 degree grade limit;
|
||||
it also checks the orphan control, brute-force interval samples and the composed wall-foot fillet.
|
||||
The Linux58 report contains the measured reachability, surface, peer-determinism, timing and capture
|
||||
results; the code path is not promoted to legacy Maze/VerticalShafts generation.
|
||||
|
||||
### 8.55 Unreal CoreGeometry asset bridge and U1 asset version
|
||||
|
||||
`UVoxelStrateDefinition::CoreGeometry` is optional and defaults to recipe `None` with no stamps,
|
||||
rooms, or tunnels. The Unreal adapter preserves the established passage planner, then resolves
|
||||
authored stamps and cavern graph records, then finalizes the layout. Stable names are required and
|
||||
are scoped by strate in Core; landmarks may target a stamp, room, or tunnel by that stable name.
|
||||
Core profile heightfields are restricted to SurfaceWorld. Native caves require the TunnelNetwork or
|
||||
Underwater archetype; native lattice requires Maze or VerticalShafts. Core recipe/version fields
|
||||
enter the resolved layout identity when selected. An imported manifest cannot be mixed with a
|
||||
per-strate profiled heightfield.
|
||||
|
||||
The checked-out Core API contains stamp vocabularies V1, V2, and V3. Unreal uses V1 for the original
|
||||
stamp kinds and V3 for monolith/platform motifs, then combines and strictly validates the result.
|
||||
Research17 names V2, but the source API and motif vocabulary require V3; source was followed.
|
||||
Research27 water remains out of scope for U1.
|
||||
|
||||
The shared VoxelForge custom asset version is 2: version 1 records the tunnel-floor slope migration;
|
||||
version 2 adds load-time conversion for inter-strate gap, strate height, and transition width. The
|
||||
saved strate-layout width is used, currently 8 m. Old packages are converted in memory and marked
|
||||
dirty, but U1 does not save them. This does not yet migrate the rest of Research8's reflected
|
||||
spatial-property inventory: legacy chunk distances, owner/Core voxel lengths and frequencies,
|
||||
placement vectors, and other centimetre fields remain in their prior units. The U1 report records
|
||||
the five BuildHost asset values, hashes, and remaining work.
|
||||
|
||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||
|
||||
|
||||
@@ -88,19 +88,22 @@ therefore **128 m**, and a 64-chunk strate is **512 m**. Core still stores work
|
||||
at 25 cm those tiles happen to span 8 m, but they are not the layout unit. The gameplay capsule remains
|
||||
**0.34 m radius / 1.76 m tall** (**2.72 / 14.08 legacy voxels**). Research-field dimensions are authored
|
||||
in metres and converted at runtime. The live
|
||||
strate generator continues to read voxel-valued parameters from its assets, so those authored
|
||||
dimensions become half as large in metres unless their asset values are doubled.
|
||||
strate generator continues to read voxel-valued shape parameters from its assets, so those authored
|
||||
dimensions become half as large in metres unless their asset values are doubled. U1 adds a
|
||||
versioned in-memory conversion for strate height, transition width, and inter-strate gap only; the
|
||||
broader Research8 voxel/chunk/centimetre inventory is not yet migrated.
|
||||
|
||||
The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells
|
||||
**32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m**
|
||||
radius; **3.0625 m** worst-case usable floor after roughness), slab open span **22.4 m**, shaft
|
||||
diameters **4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. Source
|
||||
new fixed physical dimensions are authored in metres and converted to voxel counts at the boundary.
|
||||
The owner's voxel-valued live strate assets are not migrated by C++: unchanged saved values therefore
|
||||
produce half-size rooms, tunnels, features and noise periods in metres. See the step-3 migration list
|
||||
in the handoff report for the exact `DA_Settings` fields and values.
|
||||
The owner's voxel-valued live strate shape settings are not migrated by U1: unchanged saved values
|
||||
therefore produce half-size rooms, tunnels, features and noise periods in metres. See the U1 report
|
||||
for the conversion coverage and remaining Research8 fields.
|
||||
|
||||
**Owner asset migration (no packages were edited):** preserve `DA_Settings` distances by changing
|
||||
**Prior reauthoring recommendation (not applied in U1; no packages were edited):** preserve
|
||||
`DA_Settings` distances by changing
|
||||
`ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`,
|
||||
`StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`,
|
||||
`EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region
|
||||
@@ -110,8 +113,9 @@ size `8→16 cells`, spacing/far spacing `8→16 voxels`, march step `4→8 voxe
|
||||
`12→24 cells`; it is currently disabled. Keep `MaxClipLevel=6` and zero-valued disabled/unbounded
|
||||
limits unchanged; double finite world radii and gaps, and multiply finite voxel-volume caps by 8.
|
||||
The loaded `Crystal Cave` slot in `DA_Strate3` is 64 chunks tall; use 128 chunks to preserve its
|
||||
former 512 m span. Its voxel-authored live-generator lengths remain half-size until that asset is
|
||||
re-authored; the editor-only research field is the step-3 target.
|
||||
former 512 m span. U1 automatically converts only strate height and transition widths and the
|
||||
settings gap at load; the remaining recommendations above are pending. Its voxel-authored
|
||||
live-generator lengths remain half-size until their asset fields are migrated.
|
||||
|
||||
Step-3 six-worker scale benchmark (idle machine, `Saved/Research/step3/scale/step3_scale_cacheperf_v1`):
|
||||
the 519³ field took **7.133834–7.825292 s** across three runs (median **7.347496 s**); the 259³
|
||||
@@ -170,10 +174,11 @@ remains `0`, and lateral regions remain gated off.
|
||||
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. |
|
||||
| `NativeLatticeFieldRecipeVersion`, `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit Maze/VerticalShafts opt-in at lattice recipe version 2; stable feature seeds include world seed, strate, recipe version and feature ID. §8.54. |
|
||||
| `FCavernRuntimeFeature::openShaftRamp`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | Analytic open shaft ring, helical support lookup, exact cylinder core bounds and conservative interval proofs; the outer graph BVH supplies tile candidates and protects `isTileShapeFree`. §8.54. |
|
||||
| `ResolvedLayoutStratePassageCapabilitiesV1::{hasNativeCaveField,hasNativeLatticeField}`, `IsNativeLatticeFieldStratum`, `HasNativeGraphField` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit per-strate native graph capability; `Core.Lattice.PrimitiveField` is restricted to Maze/VerticalShafts recipe version 1. Lattice seeds include world seed, strate, recipe version and stable feature ID; explicit polyline records keep their endpoints and receive conservative bounds. §8.54. |
|
||||
| `FCavernRuntimeFeature::FSegmentNode`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | A deterministic nested segment BVH accelerates native graph distance and conservative interval evaluation; the outer graph BVH still supplies tile candidates and protects `isTileShapeFree`. The 0.4 m feature-union width is separate from the floor fillet width. §8.54. |
|
||||
| `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
|
||||
| `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.
|
||||
| `ApplyOwnerFourStratePlan` | `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` | BuildHost-only transient fixed-slot plan from the four staged owner strate assets; leaves the authored settings and strate packages untouched. |
|
||||
| `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|standalone-export|compile-only|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, allows the owner's interactive editor while refusing overlapping `UnrealEditor-Cmd` launches, runs owned processes at Normal priority with a 12-logical-CPU cap, 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`. |
|
||||
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
|
||||
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; schema-v1 owner-asset export check and BuildHost Core actor tests. `-coreworldassembly=PATH` supplies a reviewed manifest transiently to `-coreworldtest=1`; it does not save a data asset. `FStandaloneCoreAdapterScheduler` runs Core work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. |
|
||||
@@ -1310,19 +1315,42 @@ See ARCHITECTURE §8.53 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md)
|
||||
for layout identities, owner hashes, outside-box timing, audits and captures.
|
||||
|
||||
### 3.36 Native lattice field — World Assembler M3 R26 slices 1–2 (Linux L58–L59)
|
||||
### 3.36 Native lattice field — World Assembler M3 R26 slice 1 (Linux L58)
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `NativeLatticeFieldRecipeVersion`, `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Version-2 Maze/VerticalShafts opt-in and deterministic seed from `(seed, strate, recipe version, stable ID)`. |
|
||||
| `MakeNativeLatticeSpine`, `--recipe=lattice`, `--lattice-strate=N` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Resolves actual adjacent mouths, builds the radius-4.5 m open shaft core, supported four-turn wall ledge and maze spine, then adds two seeded dead ends; the orphan control translates the shaft away from both junctions. |
|
||||
| `AuditNativeLatticeField` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the final field along the mouth-to-mouth capsule route, checking every-station support, headroom, grade, orphan rejection, composed fillet, shaft air/wall intervals and tile exclusion. |
|
||||
| `BuildLatticeFixtureL59.py`, `CaptureNativeLatticeL59.py`, `AuditLatticeSurfaceL59.py` | `Standalone/Research/WorldAssemblerM3/Linux59/` | Isolated Maze fixture, four JPEG views of the open shaft and maze, and L54 whole-surface audit extended to include the ramp underside. |
|
||||
| `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed`, `ResolvedCavernRoomV1::unionWidthMicrometres`, `ResolvedCavernEdgeV1::unionWidthMicrometres` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Versioned Maze/VerticalShafts opt-in and deterministic seed from `(seed, strate, recipe version, stable ID)`; optional union width preserves prior cave JSON when unset. |
|
||||
| `MakeNativeLatticeSpine`, `--recipe=lattice`, `--lattice-strate=N` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Resolves actual adjacent mouths, builds a mandatory four-turn helix and maze spine, then adds two seeded dead ends; `--lattice-orphan-control` moves the helix away from both junctions. |
|
||||
| `AuditNativeLatticeField` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the final field along the mouth-to-mouth capsule route, checking support, headroom, grade, orphan rejection, composed fillet, interval signs and feature-tile exclusion. |
|
||||
| `BuildLatticeFixtureL58.py`, `CaptureNativeLatticeL58.py`, `AuditLatticeSurfaceL58.py` | `Standalone/Research/WorldAssemblerM3/Linux58/` | Isolated Maze strate fixture, four reviewed captures and the L54 whole-surface triangle audit adapted to explicit native polylines. |
|
||||
|
||||
See ARCHITECTURE §8.54 and the
|
||||
[`Linux59 report`](Standalone/Research/WorldAssemblerM3/Linux59/REPORT.md) for the open shaft's
|
||||
measured geometry, final-field reachability, controls, timing, hashes and captures. The L58 report
|
||||
retains the first-slice evidence.
|
||||
See ARCHITECTURE §8.54 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md)
|
||||
for resolved records, final-field reachability, controls, timing, hashes and captures.
|
||||
|
||||
### 3.37 Unreal CoreGeometry assets and partial metre upgrade — World Assembler U1
|
||||
|
||||
`Public/VoxelStrateTypes.h` declares optional `FVoxelForgeCoreGeometry`. Its default recipe is Off
|
||||
and its stamp, room, and tunnel arrays are empty. `Private/VoxelForgeCoreAdapter.cpp` maps authored
|
||||
metres to Core layout requests after passage planning and before finalizing the layout. Stamps use
|
||||
the V1 resolver for original stamp kinds and V3 for monolith/platform motifs because the checked-out
|
||||
Core API exposes V3; authored cavern records use `ResolveResolvedCavernGraphV1`. Stable feature
|
||||
names are namespaced by strate in the resolved layout. `FStrateLandmark::CoreGeometryStableName`
|
||||
binds one landmark to such a resolved record, and `VoxelContentManager.cpp` builds its lookup from
|
||||
the finalized snapshot. `VoxelWorld.cpp` retains resolved layout identity in content hashing.
|
||||
|
||||
`Private/VoxelForgeAssetVersion.h` sets the shared custom version to 2. `UVoxelSettings::Serialize`
|
||||
and `UVoxelStrateDefinition::Serialize`/`PostLoad` upgrade only inter-strate gap, strate height,
|
||||
and transition width using the saved 8 m layout chunk. These conversions are version-guarded and
|
||||
idempotent. No package save is performed by the audit. Other Research8 chunks, owner-voxel lengths,
|
||||
Core-voxel lengths/frequencies, and centimetre fields remain on their existing units and need a
|
||||
separate migration.
|
||||
|
||||
Likely compile/UHT touchpoints are the reflected nested fields in `VoxelStrateTypes.h`, Core snapshot
|
||||
and resolver signatures in `VoxelForgeCoreAdapter.cpp`, `FCustomVersionRegistration` and the old
|
||||
tunnel-slope version in `VoxelStrateDefinition.cpp`, plus the standalone `VoxelForgeExplore` audit
|
||||
in `VoxelForgeExploreCommandlet.cpp`. `GetStrateForChunk` still receives layout-chunk Z, not a Core
|
||||
tile index. See `Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md` for BuildHost evidence.
|
||||
|
||||
## 4. The density pipeline (most-edited hot path)
|
||||
|
||||
|
||||
@@ -399,6 +399,7 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
|
||||
LegacySettings->WorldRadiusVoxels = 987.0f;
|
||||
LegacySettings->TotalStrates = 1;
|
||||
LegacyDefinition->StrateHeightInChunks = 7;
|
||||
LegacyDefinition->StrateHeightMeters = 7.0 * 8.0;
|
||||
LegacySettings->StratePool.Add(
|
||||
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
|
||||
TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
|
||||
|
||||
@@ -525,6 +525,7 @@ bool FVoxelForgeZeroTransitionBlendTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = World.Definitions[Index].Get();
|
||||
Definition->TransitionBlendChunks = 0;
|
||||
Definition->TransitionWidthMeters = 0.0;
|
||||
Definition->TransitionType = (Index & 1) == 0
|
||||
? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace
|
||||
Settings->Seed = LayoutSeed;
|
||||
Settings->TotalStrates = NumPoolDefinitions;
|
||||
Settings->InterStrateGapChunks = 1;
|
||||
Settings->InterStrateGapMeters = Settings->StrateLayoutChunkSizeMeters;
|
||||
Settings->bOpenSurfaceEntry = true;
|
||||
|
||||
static const ECaveGeneratorType GeneratorTypes[] = {
|
||||
@@ -69,6 +70,8 @@ namespace
|
||||
FString::Printf(TEXT("Layout order test strate %d"), Index));
|
||||
Def->GeneratorType = GeneratorTypes[Index];
|
||||
Def->StrateHeightInChunks = 3 + Index;
|
||||
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
|
||||
* Settings->StrateLayoutChunkSizeMeters;
|
||||
Def->TransitionType = EVoxelStrateTransition::Hard;
|
||||
|
||||
// Non-trivial, distinct passage configs exercise the connection index and the
|
||||
|
||||
@@ -79,6 +79,8 @@ namespace VoxelForgeTest
|
||||
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
Settings->Seed = InSeed;
|
||||
Settings->InterStrateGapChunks = InGapChunks;
|
||||
Settings->InterStrateGapMeters = static_cast<double>(InGapChunks)
|
||||
* Settings->StrateLayoutChunkSizeMeters;
|
||||
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
|
||||
// WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0.
|
||||
|
||||
@@ -110,6 +112,8 @@ namespace VoxelForgeTest
|
||||
// refinement and roll tests use the smaller synthetic volume as a bounded memory
|
||||
// control; the owner-facing showcase opts into 8 chunks below.
|
||||
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
|
||||
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
|
||||
* Settings->StrateLayoutChunkSizeMeters;
|
||||
// Hard transitions: param blending across a boundary would make "which archetype
|
||||
// owns this chunk" ambiguous, and these tests want an unambiguous mapping.
|
||||
Def->TransitionType = EVoxelStrateTransition::Hard;
|
||||
|
||||
@@ -4,6 +4,27 @@
|
||||
|
||||
namespace VoxelClipmapDesiredTiles
|
||||
{
|
||||
FORCEINLINE int64 FloorDivWide(int64 Value, int64 Divisor)
|
||||
{
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int64 Quotient = Value / Divisor;
|
||||
const int64 Remainder = Value % Divisor;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE int64 CeilDivWide(int64 Value, int64 Divisor)
|
||||
{
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int64 Quotient = Value / Divisor;
|
||||
const int64 Remainder = Value % Divisor;
|
||||
return Remainder > 0 ? Quotient + 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE int32 ClampInt32(int64 Value)
|
||||
{
|
||||
return static_cast<int32>(FMath::Clamp<int64>(Value, MIN_int32, MAX_int32));
|
||||
}
|
||||
|
||||
struct FParameters
|
||||
{
|
||||
int32 ClipRadius = 3;
|
||||
@@ -13,7 +34,10 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
|
||||
{
|
||||
return Value >= 0 ? (Value / Divisor) : -(((-Value) + Divisor - 1) / Divisor);
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int32 Quotient = Value / Divisor;
|
||||
const int32 Remainder = Value % Divisor;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
|
||||
@@ -24,7 +48,10 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
|
||||
{
|
||||
return -FloorDiv(-Value, Divisor);
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int32 Quotient = Value / Divisor;
|
||||
const int32 Remainder = Value % Divisor;
|
||||
return Remainder > 0 ? Quotient + 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
|
||||
@@ -33,10 +60,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
||||
int32& OutAxisMin, int32& OutAxisMax)
|
||||
{
|
||||
const int32 BaseMin = CenterAxis - Radius;
|
||||
const int32 BaseEnd = CenterAxis + Radius + 1;
|
||||
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
|
||||
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
|
||||
const int64 SafeRadius = FMath::Max<int64>(0, Radius);
|
||||
const int64 SafeAlignment = FMath::Max<int64>(1, Alignment);
|
||||
const int64 BaseMin = static_cast<int64>(CenterAxis) - SafeRadius;
|
||||
const int64 BaseEnd = static_cast<int64>(CenterAxis) + SafeRadius + 1;
|
||||
OutAxisMin = ClampInt32(FloorDivWide(BaseMin, SafeAlignment) * SafeAlignment);
|
||||
OutAxisMax = ClampInt32(CeilDivWide(BaseEnd, SafeAlignment) * SafeAlignment - 1);
|
||||
};
|
||||
AlignAxis(Center.X, OutMin.X, OutMax.X);
|
||||
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
||||
@@ -49,8 +78,13 @@ namespace VoxelClipmapDesiredTiles
|
||||
{
|
||||
return Radius;
|
||||
}
|
||||
return FMath::Max(Radius,
|
||||
(Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel);
|
||||
const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8);
|
||||
const int64 Scale = int64{1} << static_cast<uint32>(SafeLevel);
|
||||
const int64 Numerator = static_cast<int64>(Parameters.RenderDistanceChunks)
|
||||
+ Scale - 1;
|
||||
const int64 DesiredRadius = Numerator / Scale;
|
||||
return static_cast<int32>(FMath::Clamp<int64>(
|
||||
FMath::Max<int64>(Radius, DesiredRadius), 0, MAX_int32));
|
||||
}
|
||||
|
||||
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
|
||||
@@ -73,12 +107,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
return;
|
||||
}
|
||||
|
||||
OutMinChunkZ = CenterChunkZ - ViewDistanceDown;
|
||||
OutMaxChunkZ = CenterChunkZ + ViewDistanceUp;
|
||||
OutMinChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) - ViewDistanceDown);
|
||||
OutMaxChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) + ViewDistanceUp);
|
||||
if (bHasStrate)
|
||||
{
|
||||
OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks;
|
||||
OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks;
|
||||
OutMinChunkZ = ClampInt32(static_cast<int64>(StrateBottomChunkZ) - StrateViewMarginChunks);
|
||||
OutMaxChunkZ = ClampInt32(static_cast<int64>(StrateTopChunkZ) + StrateViewMarginChunks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +128,7 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
||||
{
|
||||
const int32 Pow = 1 << Level;
|
||||
const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
|
||||
const FIntVector CenterLevel = FloorDiv(Center, Pow);
|
||||
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
||||
|
||||
@@ -105,24 +139,23 @@ namespace VoxelClipmapDesiredTiles
|
||||
FIntVector RingMin, RingMax;
|
||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||
|
||||
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
|
||||
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
|
||||
int64 TileMinZ = RingMin.Z;
|
||||
int64 TileMaxZ = RingMax.Z;
|
||||
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
||||
{
|
||||
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
|
||||
DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
|
||||
TileMinZ = FMath::Max<int64>(TileMinZ, FloorDiv(MinChunkZ, Pow));
|
||||
TileMaxZ = FMath::Min<int64>(TileMaxZ, FloorDiv(MaxChunkZ, Pow));
|
||||
}
|
||||
|
||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
||||
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
|
||||
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
|
||||
for (int32 DeltaX = RingMin.X - CenterLevel.X;
|
||||
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
|
||||
for (int64 Z = TileMinZ; Z <= TileMaxZ; ++Z)
|
||||
for (int64 Y = RingMin.Y; Y <= RingMax.Y; ++Y)
|
||||
for (int64 X = RingMin.X; X <= RingMax.X; ++X)
|
||||
{
|
||||
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
|
||||
const int32 TileMinZ = Tile.Z << Level;
|
||||
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
|
||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
||||
const FIntVector Tile(static_cast<int32>(X), static_cast<int32>(Y),
|
||||
static_cast<int32>(Z));
|
||||
const int64 TileBottomChunkZ = static_cast<int64>(Tile.Z) * Pow;
|
||||
const int64 TileTopChunkZ = (static_cast<int64>(Tile.Z) + 1) * Pow - 1;
|
||||
if (TileTopChunkZ < MinChunkZ || TileBottomChunkZ > MaxChunkZ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -130,12 +163,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
if (Level > 0)
|
||||
{
|
||||
const bool bCovered =
|
||||
(2 * Tile.X >= PreviousRingMin.X)
|
||||
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
|
||||
&& (2 * Tile.Y >= PreviousRingMin.Y)
|
||||
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
|
||||
&& (2 * Tile.Z >= PreviousRingMin.Z)
|
||||
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
|
||||
(2 * static_cast<int64>(Tile.X) >= PreviousRingMin.X)
|
||||
&& (2 * static_cast<int64>(Tile.X) + 1 <= PreviousRingMax.X)
|
||||
&& (2 * static_cast<int64>(Tile.Y) >= PreviousRingMin.Y)
|
||||
&& (2 * static_cast<int64>(Tile.Y) + 1 <= PreviousRingMax.Y)
|
||||
&& (2 * static_cast<int64>(Tile.Z) >= PreviousRingMin.Z)
|
||||
&& (2 * static_cast<int64>(Tile.Z) + 1 <= PreviousRingMax.Z);
|
||||
if (bCovered)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Tasks/Task.h"
|
||||
#include "HAL/PlatformProcess.h"
|
||||
#include "Misc/Crc.h"
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include <cmath>
|
||||
@@ -240,10 +241,10 @@ static std::vector<VoxelForgeCore::WorldAssemblyFeatureQueryV1> VF_ContentFeatur
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
constexpr double Q = WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<int64,3> Min{static_cast<int64>(std::floor(MinX*Q)),
|
||||
static_cast<int64>(std::floor(MinY*Q)),static_cast<int64>(std::floor(MinZ*Q))};
|
||||
const std::array<int64,3> Max{static_cast<int64>(std::ceil(MaxX*Q)),
|
||||
static_cast<int64>(std::ceil(MaxY*Q)),static_cast<int64>(std::ceil(MaxZ*Q))};
|
||||
const std::array<std::int64_t,3> Min{static_cast<std::int64_t>(std::floor(MinX*Q)),
|
||||
static_cast<std::int64_t>(std::floor(MinY*Q)),static_cast<std::int64_t>(std::floor(MinZ*Q))};
|
||||
const std::array<std::int64_t,3> Max{static_cast<std::int64_t>(std::ceil(MaxX*Q)),
|
||||
static_cast<std::int64_t>(std::ceil(MaxY*Q)),static_cast<std::int64_t>(std::ceil(MaxZ*Q))};
|
||||
std::vector<WorldAssemblyFeatureQueryV1> Queries;
|
||||
if (Surface)
|
||||
if (const auto* Plan = Surface->FindPlan(StrateIndex))
|
||||
@@ -322,11 +323,54 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
|
||||
Seed = InSeed;
|
||||
NativeSurfaceSnapshot.Reset();
|
||||
NativeFloatingIslandSnapshot.Reset();
|
||||
CoreGeometryAnchors.Reset();
|
||||
|
||||
if (Settings)
|
||||
{
|
||||
FString ManifestJson, ManifestHash, ManifestError;
|
||||
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
|
||||
FString ResolvedLayoutJson, ResolvedLayoutIdentity;
|
||||
if (VoxelWorld != nullptr
|
||||
&& VoxelWorld->GetVoxelForgeCoreResolvedLayout(ResolvedLayoutJson, ResolvedLayoutIdentity))
|
||||
{
|
||||
FTCHARToUTF8 Utf8Layout(*ResolvedLayoutJson);
|
||||
std::string LayoutError;
|
||||
const std::optional<VoxelForgeCore::ResolvedLayoutSnapshotV3> ResolvedLayout =
|
||||
VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(
|
||||
std::string_view(Utf8Layout.Get(), static_cast<std::size_t>(Utf8Layout.Length())), LayoutError);
|
||||
if (ResolvedLayout)
|
||||
{
|
||||
const auto AddAnchor = [this](const std::string& StableId, int32 StrateIndex,
|
||||
const std::array<std::int64_t, 3>& PositionMicrometres)
|
||||
{
|
||||
const FVector PositionCm(
|
||||
static_cast<double>(PositionMicrometres[0]) / 10000.0,
|
||||
static_cast<double>(PositionMicrometres[1]) / 10000.0,
|
||||
static_cast<double>(PositionMicrometres[2]) / 10000.0);
|
||||
const FString FullName = UTF8_TO_TCHAR(StableId.c_str());
|
||||
FResolvedCoreGeometryAnchor Anchor{ StrateIndex, PositionCm };
|
||||
CoreGeometryAnchors.Add(FullName, Anchor);
|
||||
const int32 Marker = FullName.Find(TEXT("/core/"), ESearchCase::CaseSensitive, ESearchDir::FromEnd);
|
||||
if (Marker != INDEX_NONE)
|
||||
{
|
||||
const FString ShortName = FullName.Mid(Marker + 6);
|
||||
if (!ShortName.IsEmpty()) CoreGeometryAnchors.Add(ShortName, Anchor);
|
||||
}
|
||||
};
|
||||
for (const VoxelForgeCore::ResolvedLayoutStampV1& Stamp : ResolvedLayout->stamps)
|
||||
AddAnchor(Stamp.stableId, Stamp.strateIndex, Stamp.positionMicrometres);
|
||||
for (const VoxelForgeCore::ResolvedCavernRoomV1& Room : ResolvedLayout->cavernRooms)
|
||||
AddAnchor(Room.stableId, Room.strateIndex, Room.centerMicrometres);
|
||||
for (const VoxelForgeCore::ResolvedCavernEdgeV1& Edge : ResolvedLayout->cavernEdges)
|
||||
if (!Edge.polylineMicrometres.empty())
|
||||
AddAnchor(Edge.stableId, Edge.strateIndex, Edge.polylineMicrometres.front());
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("[VoxelContentManager] Could not read resolved Core geometry anchors: %s"),
|
||||
UTF8_TO_TCHAR(LayoutError.c_str()));
|
||||
}
|
||||
}
|
||||
const bool bHasRuntimeAssembly = VoxelWorld != nullptr
|
||||
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
|
||||
if (!bHasRuntimeAssembly)
|
||||
@@ -559,9 +603,13 @@ int32 UVoxelContentManager::RegionSize() const
|
||||
// truncation, a negative-coord cell is queued under one region but routed back to another → its march
|
||||
// result is dropped → blank chunk. Truncation == floor for A>=0 and for R==1, which is exactly why only
|
||||
// NEGATIVE coordinates at R>=2 were affected. R (region size) is always > 0 here.
|
||||
static FORCEINLINE int32 FloorDivPos(int32 A, int32 R)
|
||||
static FORCEINLINE int32 FloorDivPos(int64 A, int32 R)
|
||||
{
|
||||
return (A >= 0) ? (A / R) : -(((-A) + R - 1) / R);
|
||||
if (R <= 0) { return 0; }
|
||||
const int64 Quotient = A / R;
|
||||
const int64 Remainder = A % R;
|
||||
return static_cast<int32>(FMath::Clamp<int64>(
|
||||
Remainder < 0 ? Quotient - 1 : Quotient, MIN_int32, MAX_int32));
|
||||
}
|
||||
|
||||
static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
|
||||
@@ -573,10 +621,10 @@ static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
|
||||
static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell,
|
||||
int32 FarR, int32 R)
|
||||
{
|
||||
const int32 MinX = FloorDivPos(PlayerCell.X - FarR, R);
|
||||
const int32 MaxX = FloorDivPos(PlayerCell.X + FarR, R);
|
||||
const int32 MinY = FloorDivPos(PlayerCell.Y - FarR, R);
|
||||
const int32 MaxY = FloorDivPos(PlayerCell.Y + FarR, R);
|
||||
const int32 MinX = FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R);
|
||||
const int32 MaxX = FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R);
|
||||
const int32 MinY = FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R);
|
||||
const int32 MaxY = FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R);
|
||||
return Region.X >= MinX && Region.X <= MaxX && Region.Y >= MinY && Region.Y <= MaxY;
|
||||
}
|
||||
|
||||
@@ -1038,8 +1086,19 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
|
||||
OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual,
|
||||
FMath::Abs(static_cast<double>(Density)));
|
||||
++OutAudit.AnchorChecks;
|
||||
if (FMath::Abs(Density)>0.25f)
|
||||
{ OutError=TEXT("Populated fixture is not anchored on the canonical field."); return false; }
|
||||
// Core content crossings use the same finite-step column refinement as runtime
|
||||
// placement. Allow at most half a Core voxel of field residual in this audit.
|
||||
// Les croisements de contenu Core gardent le raffinement discret du runtime;
|
||||
// l'audit tolere au plus un demi-voxel Core de residu de champ.
|
||||
if (FMath::Abs(Density)>0.5f)
|
||||
{
|
||||
OutError=FString::Printf(
|
||||
TEXT("Populated fixture is not anchored on the canonical field: residual=%.4f core_vox=(%.3f,%.3f,%.3f) local_cm=(%.2f,%.2f,%.2f)."),
|
||||
Density, Local.X/CurrentCtx.VoxelSizeCm,
|
||||
Local.Y/CurrentCtx.VoxelSizeCm, Local.Z/CurrentCtx.VoxelSizeCm,
|
||||
Local.X, Local.Y, Local.Z);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Spawns.IsEmpty())
|
||||
@@ -1102,17 +1161,22 @@ void UVoxelContentManager::RebuildDesiredCells(FDecoGrid& G, const FIntPoint& Pl
|
||||
const int32 R = RegionSize();
|
||||
|
||||
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
|
||||
const FIntPoint RMin(FloorDivPos(PlayerCell.X - FarR, R),
|
||||
FloorDivPos(PlayerCell.Y - FarR, R));
|
||||
const FIntPoint RMax(FloorDivPos(PlayerCell.X + FarR, R),
|
||||
FloorDivPos(PlayerCell.Y + FarR, R));
|
||||
const FIntPoint RMin(FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R),
|
||||
FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R));
|
||||
const FIntPoint RMax(FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R),
|
||||
FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R));
|
||||
|
||||
TSet<FIntPoint> DesiredRegions;
|
||||
DesiredRegions.Reserve((RMax.X - RMin.X + 1) * (RMax.Y - RMin.Y + 1));
|
||||
for (int32 ry = RMin.Y; ry <= RMax.Y; ++ry)
|
||||
for (int32 rx = RMin.X; rx <= RMax.X; ++rx)
|
||||
constexpr int64 MaxRegionReserve = 4096;
|
||||
const int64 WidthX = static_cast<int64>(RMax.X) - RMin.X + 1;
|
||||
const int64 WidthY = static_cast<int64>(RMax.Y) - RMin.Y + 1;
|
||||
const int64 ReserveCount = WidthY > 0 && WidthX > MaxRegionReserve / WidthY
|
||||
? MaxRegionReserve : FMath::Max<int64>(0, WidthX * WidthY);
|
||||
DesiredRegions.Reserve(static_cast<int32>(ReserveCount));
|
||||
for (int64 ry = RMin.Y; ry <= RMax.Y; ++ry)
|
||||
for (int64 rx = RMin.X; rx <= RMax.X; ++rx)
|
||||
{
|
||||
DesiredRegions.Add(FIntPoint(rx, ry));
|
||||
DesiredRegions.Add(FIntPoint(static_cast<int32>(rx), static_cast<int32>(ry)));
|
||||
}
|
||||
|
||||
// Unload loaded regions no longer desired (plain DestroyComponent — no per-instance removal).
|
||||
@@ -1464,8 +1528,15 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
|
||||
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
|
||||
|
||||
const int32 Span = FMath::Max(0, Comp.CountMax - Comp.CountMin);
|
||||
const int32 Count = Comp.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(CH ^ 0x1234u)) * (float)(Span + 1));
|
||||
const int64 Span = FMath::Max<int64>(0,
|
||||
static_cast<int64>(Comp.CountMax) - Comp.CountMin);
|
||||
const uint64 CountRange = static_cast<uint64>(Span) + 1u;
|
||||
const float CountOffset = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(CH ^ 0x1234u)) * static_cast<float>(CountRange);
|
||||
const int64 CountWide = static_cast<int64>(Comp.CountMin)
|
||||
+ static_cast<int64>(CountOffset);
|
||||
const int32 Count = static_cast<int32>(FMath::Clamp<int64>(
|
||||
CountWide, MIN_int32, MAX_int32));
|
||||
for (int32 ii = 0; ii < Count; ++ii)
|
||||
{
|
||||
if (CompBudget <= 0) break;
|
||||
@@ -1514,8 +1585,15 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
|
||||
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
|
||||
|
||||
const int32 SubSpan = FMath::Max(0, Sub.CountMax - Sub.CountMin);
|
||||
const int32 SubCount = Sub.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(SH ^ 0x1234u)) * (float)(SubSpan + 1));
|
||||
const int64 SubSpan = FMath::Max<int64>(0,
|
||||
static_cast<int64>(Sub.CountMax) - Sub.CountMin);
|
||||
const uint64 SubCountRange = static_cast<uint64>(SubSpan) + 1u;
|
||||
const float SubCountOffset = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(SH ^ 0x1234u)) * static_cast<float>(SubCountRange);
|
||||
const int64 SubCountWide = static_cast<int64>(Sub.CountMin)
|
||||
+ static_cast<int64>(SubCountOffset);
|
||||
const int32 SubCount = static_cast<int32>(FMath::Clamp<int64>(
|
||||
SubCountWide, MIN_int32, MAX_int32));
|
||||
for (int32 sj = 0; sj < SubCount; ++sj)
|
||||
{
|
||||
if (CompBudget <= 0) break;
|
||||
@@ -2374,6 +2452,28 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
|
||||
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
|
||||
if (ExclWorld > 0.0f) bAnyExclusion = true;
|
||||
|
||||
if (!L.CoreGeometryStableName.IsEmpty())
|
||||
{
|
||||
const FResolvedCoreGeometryAnchor* Anchor = CoreGeometryAnchors.Find(L.CoreGeometryStableName);
|
||||
if (Anchor == nullptr || Anchor->StrateIndex != StrateIndex) continue;
|
||||
const uint32 StableNameHash = FCrc::StrCrc32(*L.CoreGeometryStableName);
|
||||
const uint32 H = DecoHash(static_cast<int32>(StableNameHash), StrateIndex,
|
||||
0, 0, 0, EntryIdx, LocalSeed, 0xC04E6E0u);
|
||||
if (VoxelHash::ToFloat01(H) > L.SpawnProbability) continue;
|
||||
const float LocalX = static_cast<float>(Anchor->LocalPositionCm.X);
|
||||
const float LocalY = static_cast<float>(Anchor->LocalPositionCm.Y);
|
||||
const float dx = LocalX - LocalPlayer.X, dy = LocalY - LocalPlayer.Y;
|
||||
const float DistSq = dx * dx + dy * dy;
|
||||
if (DistSq <= GatherWorld * GatherWorld)
|
||||
{
|
||||
// A Core-bound landmark is one stable candidate, independent of the hash lattice.
|
||||
// Un landmark lie a Core reste un candidat stable, independant de la grille de hachage.
|
||||
Cands.Add({ FIntVector(EntryIdx, 0, EntryIdx), EntryIdx, H, LocalX, LocalY,
|
||||
ExclWorld, L.Priority, DistSq <= RadiusWorld * RadiusWorld });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (L.AnchorMode == ELandmarkAnchor::HashLattice)
|
||||
{
|
||||
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
|
||||
|
||||
@@ -155,9 +155,11 @@ int32 UVoxelDensityVolume::NumLevels() const
|
||||
|
||||
FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B)
|
||||
{
|
||||
// True floor division (B > 0). FMath::DivideAndRoundDown truncates toward zero for negatives —
|
||||
// a footgun the content manager hit too (see FloorDivPos there). Cells span the origin, so floor.
|
||||
return (A >= 0) ? (A / B) : -(((-A) + B - 1) / B);
|
||||
// True floor division (B > 0). Remainder adjustment handles INT_MIN without negating it.
|
||||
if (B <= 0) { return 0; }
|
||||
const int32 Quotient = A / B;
|
||||
const int32 Remainder = A % B;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
|
||||
@@ -180,7 +182,8 @@ void UVoxelDensityVolume::EnsureAllocated()
|
||||
for (int32 L = 0; L < N; ++L)
|
||||
{
|
||||
FClipLevel& Lv = Levels[L];
|
||||
Lv.Step = 1 << L;
|
||||
Lv.Step = static_cast<int32>(uint32{1}
|
||||
<< static_cast<uint32>(FMath::Clamp(L, 0, 8)));
|
||||
Lv.OriginCells = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||
Lv.bHasData = false;
|
||||
Lv.Density.SetNumZeroed(Count); // start all-air (0)
|
||||
@@ -632,7 +635,8 @@ void UVoxelDensityVolume::ProcessOneFill(const FPendingFill& F)
|
||||
if (!Gen) return;
|
||||
if (bShuttingDown.load(std::memory_order_relaxed) || bFillThreadStop.load(std::memory_order_relaxed)) return;
|
||||
|
||||
const int32 Step = 1 << F.Level;
|
||||
const int32 Step = static_cast<int32>(uint32{1}
|
||||
<< static_cast<uint32>(FMath::Clamp(F.Level, 0, 8)));
|
||||
FFillResult R;
|
||||
R.Level = F.Level;
|
||||
R.Epoch = F.Epoch;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
// Shared custom package version for idempotent VoxelForge asset migrations.
|
||||
// Version de package partagee pour les migrations VoxelForge idempotentes.
|
||||
namespace VoxelForgeAssetVersion
|
||||
{
|
||||
inline const FGuid GUID(0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
|
||||
inline constexpr int32 TunnelFloorSlopeDegrees = 1;
|
||||
inline constexpr int32 MetreAuthoringV1 = 2;
|
||||
}
|
||||
@@ -82,6 +82,113 @@ std::int64_t WorldAssemblyWavelength(double Frequency, double FallbackVoxels)
|
||||
return std::max<std::int64_t>(1, WorldAssemblyFixed(Wavelength));
|
||||
}
|
||||
|
||||
std::int64_t WorldAssemblyMicrometresToSubvoxels(
|
||||
std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres);
|
||||
|
||||
bool WorldAssemblyMetresToMicrometres(double Metres, std::int64_t& OutMicrometres)
|
||||
{
|
||||
if (!std::isfinite(Metres)
|
||||
|| std::abs(Metres) > static_cast<double>(std::numeric_limits<std::int64_t>::max()) / 1000000.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutMicrometres = static_cast<std::int64_t>(std::llround(Metres * 1000000.0));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WorldAssemblyMetresToSubvoxels(double Metres, std::uint32_t CoreVoxelMicrometres,
|
||||
std::int64_t& OutSubvoxels)
|
||||
{
|
||||
std::int64_t Micrometres = 0;
|
||||
if (!WorldAssemblyMetresToMicrometres(Metres, Micrometres) || CoreVoxelMicrometres == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutSubvoxels = WorldAssemblyMicrometresToSubvoxels(Micrometres, CoreVoxelMicrometres);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WorldAssemblyVectorToMicrometres(const FVector& Metres, std::array<std::int64_t, 3>& Out)
|
||||
{
|
||||
return WorldAssemblyMetresToMicrometres(Metres.X, Out[0])
|
||||
&& WorldAssemblyMetresToMicrometres(Metres.Y, Out[1])
|
||||
&& WorldAssemblyMetresToMicrometres(Metres.Z, Out[2]);
|
||||
}
|
||||
|
||||
bool WorldAssemblyVectorToMicrometres(const FVector2D& Metres, std::array<std::int64_t, 2>& Out)
|
||||
{
|
||||
return WorldAssemblyMetresToMicrometres(Metres.X, Out[0])
|
||||
&& WorldAssemblyMetresToMicrometres(Metres.Y, Out[1]);
|
||||
}
|
||||
|
||||
std::string CoreGeometryStableId(int32 StrateIndex, const FString& StableName)
|
||||
{
|
||||
FTCHARToUTF8 Utf8Name(*StableName);
|
||||
return std::string("strate/") + std::to_string(StrateIndex) + "/core/" + Utf8Name.Get();
|
||||
}
|
||||
|
||||
bool HasCoreGeometry(const UVoxelStrateDefinition& Definition)
|
||||
{
|
||||
const FVoxelForgeCoreGeometry& Geometry = Definition.CoreGeometry;
|
||||
return Geometry.GeneratorRecipe != EVoxelForgeCoreGeneratorRecipe::None
|
||||
|| !Geometry.Stamps.IsEmpty() || !Geometry.Rooms.IsEmpty() || !Geometry.Tunnels.IsEmpty();
|
||||
}
|
||||
|
||||
bool ConfigureCoreGeometryStratum(const FStrateSlot& Slot, ECaveGeneratorType GeneratorType,
|
||||
bool bImportedOverride, VoxelForgeCore::ResolvedLayoutStratumV2& Item, FString& OutError)
|
||||
{
|
||||
const FVoxelForgeCoreGeometry& Geometry = Slot.Definition->CoreGeometry;
|
||||
switch (Geometry.GeneratorRecipe)
|
||||
{
|
||||
case EVoxelForgeCoreGeneratorRecipe::None:
|
||||
return true;
|
||||
case EVoxelForgeCoreGeneratorRecipe::NativeCaves:
|
||||
if (GeneratorType != ECaveGeneratorType::TunnelNetwork
|
||||
&& GeneratorType != ECaveGeneratorType::Underwater)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d selects NativeCaves but its generator archetype is not TunnelNetwork/Underwater."),
|
||||
Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
Item.generatorKind = "Caves";
|
||||
Item.recipeId = "Core.Caves.PrimitiveField";
|
||||
Item.recipeVersion = 1;
|
||||
Item.boundDispatchPath = "Core native cave graph uses resolved room and tunnel records";
|
||||
Item.queryDispatchPath = "NativeCaveDensityAt evaluates the resolved cavern graph";
|
||||
return true;
|
||||
case EVoxelForgeCoreGeneratorRecipe::NativeLattice:
|
||||
if (GeneratorType != ECaveGeneratorType::Maze
|
||||
&& GeneratorType != ECaveGeneratorType::VerticalShafts)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d selects NativeLattice but its generator archetype is not Maze/VerticalShafts."),
|
||||
Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
Item.recipeId = "Core.Lattice.PrimitiveField";
|
||||
Item.recipeVersion = 1;
|
||||
Item.boundDispatchPath = "Core native lattice uses resolved room and explicit tunnel path records";
|
||||
Item.queryDispatchPath = "NativeLatticeDensityAt evaluates the resolved cavern graph";
|
||||
return true;
|
||||
case EVoxelForgeCoreGeneratorRecipe::ProfiledHeightfield:
|
||||
if (GeneratorType != ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d selects ProfiledHeightfield but its generator archetype is not SurfaceWorld."),
|
||||
Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
if (bImportedOverride)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d has ProfiledHeightfield authoring, but an imported WorldAssemblyManifest owns that recipe. Clear the imported override to use per-strate CoreGeometry."),
|
||||
Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
Item.recipeId = "SurfaceWorld.ProfiledHeightfield";
|
||||
Item.recipeVersion = 1;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::uint64_t WorldAssemblyFeatureId(std::uint32_t Seed, std::int64_t X,
|
||||
std::int64_t Y, std::uint32_t Kind)
|
||||
{
|
||||
@@ -111,7 +218,7 @@ std::int64_t WorldAssemblyMicrometresToSubvoxels(
|
||||
std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres)
|
||||
{
|
||||
if (CoreVoxelMicrometres == 0) return 0;
|
||||
return WorldAssemblyRoundRatio(Micrometres * WorldAssemblySubvoxelsPerVoxel,
|
||||
return WorldAssemblyRoundRatio(Micrometres * VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
||||
static_cast<std::int64_t>(CoreVoxelMicrometres));
|
||||
}
|
||||
|
||||
@@ -139,7 +246,8 @@ std::uint64_t WorldAssemblyRoundedSqrt(std::uint64_t Value)
|
||||
}
|
||||
|
||||
bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum, std::string& Error)
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum,
|
||||
std::int64_t ArrivalRadiusQ, std::int64_t DepartureRadiusQ, std::string& Error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
constexpr std::int64_t PlayerRadiusQ = 348;
|
||||
@@ -147,7 +255,10 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
constexpr std::int64_t HalfHeightQ = PlayerHeightQ / 2;
|
||||
constexpr std::int64_t StepQ = 128;
|
||||
constexpr std::int64_t ProbeRadiusQ = 174;
|
||||
constexpr std::int64_t SupportDepthQ = 256;
|
||||
// The live owner surface recipe can sit slightly below the fixed-point route
|
||||
// heightfield used during candidate search; allow up to 0.5 m for the final
|
||||
// resolved-field support probe before rejecting an otherwise clear route.
|
||||
constexpr std::int64_t SupportDepthQ = 512;
|
||||
constexpr std::int64_t GradeQ16 = 17560;
|
||||
constexpr std::int64_t RouteRadiusQ = 4 * WorldAssemblySubvoxelsPerVoxel;
|
||||
struct FDirection { std::int32_t X; std::int32_t Y; };
|
||||
@@ -157,9 +268,12 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
struct FCandidate
|
||||
{
|
||||
std::int64_t Score = 0;
|
||||
std::int64_t OutwardProjection = 0;
|
||||
std::uint64_t EndpointProximityCost = 0;
|
||||
std::int32_t CenterX = 0;
|
||||
std::int32_t CenterY = 0;
|
||||
std::int32_t Direction = 0;
|
||||
std::int32_t HalfLength = 128;
|
||||
std::vector<WorldAssemblyRoutePointV1> Points;
|
||||
};
|
||||
|
||||
@@ -171,7 +285,7 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
Constraints.sweepSpacingSubvoxels = StepQ;
|
||||
Constraints.supportProbeRadiusSubvoxels = ProbeRadiusQ;
|
||||
Constraints.supportSearchDepthSubvoxels = SupportDepthQ;
|
||||
Constraints.minimumClearanceSubvoxels = 8;
|
||||
Constraints.minimumClearanceSubvoxels = 128;
|
||||
Constraints.minimumSupportProbes = 3;
|
||||
|
||||
const Heightfield2D BaseField(Recipe, Stratum);
|
||||
@@ -182,11 +296,14 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
}
|
||||
std::vector<FCandidate> Candidates;
|
||||
constexpr std::int32_t WaypointSpacing = 8;
|
||||
std::uint64_t LegacyCandidatesVisited = 0;
|
||||
std::uint64_t LegacyGradeRejected = 0;
|
||||
for (std::int32_t CenterY = -256; CenterY <= 256; CenterY += 64)
|
||||
for (std::int32_t CenterX = -256; CenterX <= 256; CenterX += 64)
|
||||
for (std::int32_t DirectionIndex = 0;
|
||||
DirectionIndex < static_cast<std::int32_t>(Directions.size()); ++DirectionIndex)
|
||||
{
|
||||
++LegacyCandidatesVisited;
|
||||
FCandidate Candidate;
|
||||
Candidate.CenterX = CenterX;
|
||||
Candidate.CenterY = CenterY;
|
||||
@@ -234,36 +351,232 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
PreviousY = Y;
|
||||
}
|
||||
if (bGradePass) Candidates.push_back(std::move(Candidate));
|
||||
else ++LegacyGradeRejected;
|
||||
}
|
||||
std::stable_sort(Candidates.begin(), Candidates.end(), [](const FCandidate& A, const FCandidate& B)
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeSurfaceRouteCandidates] stage=legacy_64m visited=%llu rejected_grade=%llu eligible=%llu arrival_radius_q=%lld departure_radius_q=%lld"),
|
||||
static_cast<unsigned long long>(LegacyCandidatesVisited),
|
||||
static_cast<unsigned long long>(LegacyGradeRejected),
|
||||
static_cast<unsigned long long>(Candidates.size()),
|
||||
static_cast<long long>(ArrivalRadiusQ), static_cast<long long>(DepartureRadiusQ));
|
||||
const auto TryCandidates = [&](const TCHAR* Stage)
|
||||
{
|
||||
if (A.Score != B.Score) return A.Score < B.Score;
|
||||
if (A.CenterY != B.CenterY) return A.CenterY < B.CenterY;
|
||||
if (A.CenterX != B.CenterX) return A.CenterX < B.CenterX;
|
||||
return A.Direction < B.Direction;
|
||||
});
|
||||
|
||||
for (const FCandidate& Candidate : Candidates)
|
||||
{
|
||||
Stratum.route = Candidate.Points;
|
||||
std::int64_t LiftQ = 0;
|
||||
constexpr std::int64_t RepairStepQ = 64;
|
||||
constexpr std::int64_t MaximumRepairLiftQ = SupportDepthQ - RepairStepQ;
|
||||
for (;;)
|
||||
std::stable_sort(Candidates.begin(), Candidates.end(), [](const FCandidate& A, const FCandidate& B)
|
||||
{
|
||||
if (LiftQ > 0)
|
||||
if (A.EndpointProximityCost != B.EndpointProximityCost)
|
||||
return A.EndpointProximityCost < B.EndpointProximityCost;
|
||||
if (A.OutwardProjection != B.OutwardProjection)
|
||||
return A.OutwardProjection > B.OutwardProjection;
|
||||
if (A.Score != B.Score) return A.Score < B.Score;
|
||||
if (A.CenterY != B.CenterY) return A.CenterY < B.CenterY;
|
||||
if (A.CenterX != B.CenterX) return A.CenterX < B.CenterX;
|
||||
return A.Direction < B.Direction;
|
||||
});
|
||||
|
||||
std::uint64_t SweepAttempts = 0;
|
||||
std::uint64_t BestFailureCount = std::numeric_limits<std::uint64_t>::max();
|
||||
WorldAssemblyRouteAuditResult BestAudit;
|
||||
const FCandidate* BestCandidate = nullptr;
|
||||
std::int64_t BestLiftQ = 0;
|
||||
for (const FCandidate& Candidate : Candidates)
|
||||
{
|
||||
Stratum.route = Candidate.Points;
|
||||
std::int64_t LiftQ = 0;
|
||||
constexpr std::int64_t RepairStepQ = 64;
|
||||
constexpr std::int64_t MaximumRepairLiftQ = SupportDepthQ - RepairStepQ;
|
||||
for (;;)
|
||||
{
|
||||
for (std::size_t PointIndex = 0; PointIndex < Stratum.route.size(); ++PointIndex)
|
||||
if (LiftQ > 0)
|
||||
{
|
||||
Stratum.route[PointIndex].positionSubvoxels[2] =
|
||||
Candidate.Points[PointIndex].positionSubvoxels[2] + LiftQ;
|
||||
for (std::size_t PointIndex = 0; PointIndex < Stratum.route.size(); ++PointIndex)
|
||||
{
|
||||
Stratum.route[PointIndex].positionSubvoxels[2] =
|
||||
Candidate.Points[PointIndex].positionSubvoxels[2] + LiftQ;
|
||||
}
|
||||
}
|
||||
const Heightfield2D CandidateField(Recipe, Stratum);
|
||||
const WorldAssemblyRouteAuditResult Audit = AuditHeightfieldRoute(CandidateField);
|
||||
++SweepAttempts;
|
||||
const std::uint64_t FailureCount = Audit.blockedCapsuleSamples
|
||||
+ Audit.unsupportedCenterSamples + Audit.gradeFailures;
|
||||
if (FailureCount < BestFailureCount)
|
||||
{
|
||||
BestFailureCount = FailureCount;
|
||||
BestAudit = Audit;
|
||||
BestCandidate = &Candidate;
|
||||
BestLiftQ = LiftQ;
|
||||
}
|
||||
if (Audit.passed)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeSurfaceRouteAudit] stage=%s candidates=%llu sweep_attempts=%llu blocked=%llu unsupported=%llu grade_failures=%llu min_clearance_q=%lld max_support_depth_q=%lld center_x=%d center_y=%d direction=%d half_length_vox=%d lift_q=%lld status=PASS"),
|
||||
Stage, static_cast<unsigned long long>(Candidates.size()),
|
||||
static_cast<unsigned long long>(SweepAttempts),
|
||||
static_cast<unsigned long long>(Audit.blockedCapsuleSamples),
|
||||
static_cast<unsigned long long>(Audit.unsupportedCenterSamples),
|
||||
static_cast<unsigned long long>(Audit.gradeFailures),
|
||||
static_cast<long long>(Audit.minimumClearanceSubvoxels),
|
||||
static_cast<long long>(Audit.maximumSupportDepthSubvoxels),
|
||||
Candidate.CenterX, Candidate.CenterY, Candidate.Direction,
|
||||
Candidate.HalfLength, static_cast<long long>(LiftQ));
|
||||
return true;
|
||||
}
|
||||
if (LiftQ >= MaximumRepairLiftQ) break;
|
||||
LiftQ += RepairStepQ;
|
||||
}
|
||||
}
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[VoxelForgeSurfaceRouteAudit] stage=%s candidates=%llu sweep_attempts=%llu best_blocked=%llu best_unsupported=%llu best_grade_failures=%llu min_clearance_q=%lld max_support_depth_q=%lld center_x=%d center_y=%d direction=%d half_length_vox=%d lift_q=%lld status=FAIL"),
|
||||
Stage, static_cast<unsigned long long>(Candidates.size()),
|
||||
static_cast<unsigned long long>(SweepAttempts),
|
||||
static_cast<unsigned long long>(BestAudit.blockedCapsuleSamples),
|
||||
static_cast<unsigned long long>(BestAudit.unsupportedCenterSamples),
|
||||
static_cast<unsigned long long>(BestAudit.gradeFailures),
|
||||
static_cast<long long>(BestAudit.minimumClearanceSubvoxels),
|
||||
static_cast<long long>(BestAudit.maximumSupportDepthSubvoxels),
|
||||
BestCandidate ? BestCandidate->CenterX : 0,
|
||||
BestCandidate ? BestCandidate->CenterY : 0,
|
||||
BestCandidate ? BestCandidate->Direction : 0,
|
||||
BestCandidate ? BestCandidate->HalfLength : 0,
|
||||
static_cast<long long>(BestLiftQ));
|
||||
return false;
|
||||
};
|
||||
|
||||
// Keep the established candidate set first so worlds that already pass retain
|
||||
// their exact route. The bounded-mouth fallback searches different route lengths.
|
||||
if (TryCandidates(TEXT("legacy_64m"))) return true;
|
||||
|
||||
if (ArrivalRadiusQ > 0 || DepartureRadiusQ > 0)
|
||||
{
|
||||
// The endpoint radius constrains the mouth end, not the full route length. Keep
|
||||
// enough reach for a route to leave a steep portal area and find gentler terrain.
|
||||
constexpr std::int32_t maximumHalfLength = 128;
|
||||
const std::vector<std::int32_t> HalfLengths{128, 112, 96, 80, 64, 48, 32, 24, 16};
|
||||
Candidates.clear();
|
||||
std::uint64_t FallbackCandidatesVisited = 0;
|
||||
std::uint64_t FallbackGradeRejected = 0;
|
||||
std::uint64_t FallbackPlacementRejected = 0;
|
||||
std::uint64_t BestGradeEndpointDistanceQ = std::numeric_limits<std::uint64_t>::max();
|
||||
FCandidate BestGradeCandidate;
|
||||
for (const std::int32_t HalfLength : HalfLengths)
|
||||
for (std::int32_t CenterY = -256; CenterY <= 256; CenterY += 16)
|
||||
for (std::int32_t CenterX = -256; CenterX <= 256; CenterX += 16)
|
||||
for (std::int32_t DirectionIndex = 0;
|
||||
DirectionIndex < static_cast<std::int32_t>(Directions.size()); ++DirectionIndex)
|
||||
{
|
||||
++FallbackCandidatesVisited;
|
||||
FCandidate Candidate;
|
||||
Candidate.CenterX = CenterX;
|
||||
Candidate.CenterY = CenterY;
|
||||
Candidate.Direction = DirectionIndex;
|
||||
Candidate.HalfLength = HalfLength;
|
||||
Candidate.Points.reserve(33);
|
||||
std::int64_t PreviousGround = 0;
|
||||
std::int64_t PreviousX = 0;
|
||||
std::int64_t PreviousY = 0;
|
||||
bool bGradePass = true;
|
||||
bool bPlacementPass = true;
|
||||
for (std::int32_t Index = 0; Index <= 32; ++Index)
|
||||
{
|
||||
const std::int64_t Along = (Index - 16) * HalfLength / 16;
|
||||
const std::int64_t X = CenterX + WorldAssemblyRoundRatio(
|
||||
Along * Directions[DirectionIndex].X, 65536);
|
||||
const std::int64_t Y = CenterY + WorldAssemblyRoundRatio(
|
||||
Along * Directions[DirectionIndex].Y, 65536);
|
||||
const std::int64_t XQ = WorldAssemblyFixed(static_cast<double>(X));
|
||||
const std::int64_t YQ = WorldAssemblyFixed(static_cast<double>(Y));
|
||||
const auto InRadius = [](std::int64_t XValue, std::int64_t YValue,
|
||||
std::int64_t Radius)
|
||||
{
|
||||
return Radius <= 0 || XValue * XValue + YValue * YValue <= Radius * Radius;
|
||||
};
|
||||
if ((Index == 0 && ArrivalRadiusQ > 0
|
||||
&& !InRadius(XQ, YQ, ArrivalRadiusQ))
|
||||
|| (Index == 32 && DepartureRadiusQ > 0
|
||||
&& !InRadius(XQ, YQ, DepartureRadiusQ)))
|
||||
{
|
||||
bPlacementPass = false;
|
||||
}
|
||||
if (Index == 0 && ArrivalRadiusQ > 0)
|
||||
{
|
||||
Candidate.EndpointProximityCost += static_cast<std::uint64_t>(XQ * XQ + YQ * YQ) * 16ull;
|
||||
}
|
||||
if (Index == 32 && DepartureRadiusQ > 0)
|
||||
{
|
||||
Candidate.EndpointProximityCost += static_cast<std::uint64_t>(XQ * XQ + YQ * YQ);
|
||||
// Approach the surface portal from the world-facing side. The
|
||||
// resolved passage leaves the mouth along its outward tangent;
|
||||
// a route that approaches from beyond the mouth overlaps that
|
||||
// bore and turns its centerline into solid ground.
|
||||
const std::int64_t OutwardProjection = XQ * Directions[DirectionIndex].X
|
||||
+ YQ * Directions[DirectionIndex].Y;
|
||||
Candidate.OutwardProjection = OutwardProjection;
|
||||
}
|
||||
const auto Column = BaseField.point(WorldAssemblyFixed(static_cast<double>(X)),
|
||||
WorldAssemblyFixed(static_cast<double>(Y)), 0);
|
||||
const std::int64_t DeltaX = X - PreviousX;
|
||||
const std::int64_t DeltaY = Y - PreviousY;
|
||||
const std::uint64_t HorizontalVoxels = WorldAssemblyRoundedSqrt(
|
||||
static_cast<std::uint64_t>(DeltaX * DeltaX + DeltaY * DeltaY));
|
||||
const std::int64_t HorizontalQ = static_cast<std::int64_t>(HorizontalVoxels)
|
||||
* WorldAssemblySubvoxelsPerVoxel;
|
||||
if (Index > 0)
|
||||
{
|
||||
const std::int64_t RiseQ = std::abs(Column.groundHeightSubvoxels - PreviousGround);
|
||||
if (HorizontalQ == 0 || RiseQ * 65536 > HorizontalQ * GradeQ16)
|
||||
{
|
||||
bGradePass = false;
|
||||
}
|
||||
Candidate.Score += RiseQ * 65536 / std::max<std::int64_t>(1, HorizontalQ);
|
||||
}
|
||||
WorldAssemblyRoutePointV1 Point;
|
||||
Point.positionSubvoxels = {XQ, YQ, Column.groundHeightSubvoxels + HalfHeightQ};
|
||||
Point.radiusSubvoxels = RouteRadiusQ;
|
||||
Point.stableFeatureId = WorldAssemblyFeatureId(Recipe.seed, X, Y,
|
||||
0x52540000u + static_cast<std::uint32_t>(Index));
|
||||
Candidate.Points.push_back(Point);
|
||||
PreviousGround = Column.groundHeightSubvoxels;
|
||||
PreviousX = X;
|
||||
PreviousY = Y;
|
||||
}
|
||||
Candidate.Score /= 32;
|
||||
if (bGradePass)
|
||||
{
|
||||
const auto& Endpoint = DepartureRadiusQ > 0
|
||||
? Candidate.Points.back().positionSubvoxels
|
||||
: Candidate.Points.front().positionSubvoxels;
|
||||
const std::uint64_t EndpointDistanceQ = WorldAssemblyRoundedSqrt(
|
||||
static_cast<std::uint64_t>(Endpoint[0] * Endpoint[0]
|
||||
+ Endpoint[1] * Endpoint[1]));
|
||||
if (EndpointDistanceQ < BestGradeEndpointDistanceQ)
|
||||
{
|
||||
BestGradeEndpointDistanceQ = EndpointDistanceQ;
|
||||
BestGradeCandidate.CenterX = CenterX;
|
||||
BestGradeCandidate.CenterY = CenterY;
|
||||
BestGradeCandidate.Direction = DirectionIndex;
|
||||
BestGradeCandidate.HalfLength = HalfLength;
|
||||
}
|
||||
}
|
||||
const Heightfield2D CandidateField(Recipe, Stratum);
|
||||
if (AuditHeightfieldRoute(CandidateField).passed) return true;
|
||||
if (LiftQ >= MaximumRepairLiftQ) break;
|
||||
LiftQ += RepairStepQ;
|
||||
if (bGradePass && bPlacementPass) Candidates.push_back(std::move(Candidate));
|
||||
else
|
||||
{
|
||||
if (!bGradePass) ++FallbackGradeRejected;
|
||||
if (!bPlacementPass) ++FallbackPlacementRejected;
|
||||
}
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeSurfaceRouteCandidates] stage=bounded_single_mouth visited=%llu rejected_grade=%llu rejected_placement=%llu eligible=%llu max_half_length_vox=%d half_length_variants=%d best_grade_endpoint_q=%llu best_grade_center_x=%d best_grade_center_y=%d best_grade_direction=%d best_grade_half_length_vox=%d arrival_radius_q=%lld departure_radius_q=%lld"),
|
||||
static_cast<unsigned long long>(FallbackCandidatesVisited),
|
||||
static_cast<unsigned long long>(FallbackGradeRejected),
|
||||
static_cast<unsigned long long>(FallbackPlacementRejected),
|
||||
static_cast<unsigned long long>(Candidates.size()), maximumHalfLength,
|
||||
static_cast<int32>(HalfLengths.size()),
|
||||
static_cast<unsigned long long>(BestGradeEndpointDistanceQ),
|
||||
BestGradeCandidate.CenterX, BestGradeCandidate.CenterY,
|
||||
BestGradeCandidate.Direction, BestGradeCandidate.HalfLength,
|
||||
static_cast<long long>(ArrivalRadiusQ),
|
||||
static_cast<long long>(DepartureRadiusQ));
|
||||
if (TryCandidates(TEXT("bounded_single_mouth"))) return true;
|
||||
}
|
||||
Error = "SurfaceWorld route candidates failed the capsule and support sweep.";
|
||||
return false;
|
||||
@@ -370,6 +683,54 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
BottomVoxel, HeightVoxels));
|
||||
}
|
||||
|
||||
if (Definition != nullptr
|
||||
&& Definition->CoreGeometry.GeneratorRecipe == EVoxelForgeCoreGeneratorRecipe::ProfiledHeightfield)
|
||||
{
|
||||
const FVoxelForgeCoreGeometry& Geometry = Definition->CoreGeometry;
|
||||
WorldAssemblySurfaceProfileV1 Profile;
|
||||
Profile.version = 1;
|
||||
switch (Geometry.HeightfieldProfile)
|
||||
{
|
||||
case EVoxelForgeCoreHeightfieldProfile::PeriodicHills:
|
||||
Profile.kind = WorldAssemblySurfaceProfileKindV1::periodicHills;
|
||||
break;
|
||||
case EVoxelForgeCoreHeightfieldProfile::MirroredRelief:
|
||||
Profile.kind = WorldAssemblySurfaceProfileKindV1::mirroredRelief;
|
||||
break;
|
||||
case EVoxelForgeCoreHeightfieldProfile::FlatField:
|
||||
Profile.kind = WorldAssemblySurfaceProfileKindV1::flatField;
|
||||
break;
|
||||
}
|
||||
std::array<std::int64_t, 2> AmplitudeMicrometres{};
|
||||
std::array<std::int64_t, 2> WavelengthMicrometres{};
|
||||
std::int64_t BaseSubvoxels = 0;
|
||||
std::int64_t ClearanceSubvoxels = 0;
|
||||
if (!WorldAssemblyVectorToMicrometres(Geometry.HeightfieldAmplitudeMeters, AmplitudeMicrometres)
|
||||
|| !WorldAssemblyVectorToMicrometres(Geometry.HeightfieldWavelengthMeters, WavelengthMicrometres)
|
||||
|| !WorldAssemblyMetresToSubvoxels(Geometry.HeightfieldBaseMeters, CoreVoxelMicrometres, BaseSubvoxels)
|
||||
|| !WorldAssemblyMetresToSubvoxels(Geometry.MinimumArenaClearanceMeters,
|
||||
CoreVoxelMicrometres, ClearanceSubvoxels)
|
||||
|| !std::isfinite(Geometry.MirrorLambda))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d Core profiled-heightfield fields contain a non-finite or out-of-range metre value."),
|
||||
Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
Profile.h0Subvoxels = BaseSubvoxels;
|
||||
Profile.amplitudeXSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
AmplitudeMicrometres[0], CoreVoxelMicrometres);
|
||||
Profile.amplitudeYSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
AmplitudeMicrometres[1], CoreVoxelMicrometres);
|
||||
Profile.wavelengthXSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
WavelengthMicrometres[0], CoreVoxelMicrometres);
|
||||
Profile.wavelengthYSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
WavelengthMicrometres[1], CoreVoxelMicrometres);
|
||||
Profile.mirrorLambdaQ16 = WorldAssemblyQ16(Geometry.MirrorLambda);
|
||||
Profile.minimumArenaClearanceSubvoxels = ClearanceSubvoxels;
|
||||
ResolveWorldAssemblySurfaceProfilePhasesV1(Profile, Recipe.seed);
|
||||
Recipe.surfaceProfile = Profile;
|
||||
}
|
||||
|
||||
Stratum.strateIndex = Slot.StrateIndex;
|
||||
Stratum.topChunkZ = Slot.TopChunkZ;
|
||||
Stratum.bottomChunkZ = Slot.BottomChunkZ;
|
||||
@@ -476,7 +837,29 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
}
|
||||
}
|
||||
std::string RouteError;
|
||||
if (!BuildFiniteSurfaceRoute(Recipe, Stratum, RouteError))
|
||||
const auto OwnerVoxelsToCoreQ = [CoreVoxelMicrometres](double OwnerVoxels)
|
||||
{
|
||||
const double OwnerVoxelMicrometres = OwnerVoxelSizeCm * 10000.0;
|
||||
return static_cast<std::int64_t>(std::llround(OwnerVoxels * OwnerVoxelMicrometres
|
||||
* WorldAssemblySubvoxelsPerVoxel / std::max<std::uint32_t>(1, CoreVoxelMicrometres)));
|
||||
};
|
||||
std::int64_t ArrivalRadiusQ = 0;
|
||||
std::int64_t DepartureRadiusQ = 0;
|
||||
for (const FStrateSlot& Neighbor : StrateManager->GetLayout())
|
||||
{
|
||||
if (Neighbor.Definition == nullptr) continue;
|
||||
if (Neighbor.StrateIndex + 1 == Slot.StrateIndex)
|
||||
{
|
||||
ArrivalRadiusQ = std::max(OwnerVoxelsToCoreQ(Neighbor.Definition->PassageConfig.DistanceMin),
|
||||
OwnerVoxelsToCoreQ(Neighbor.Definition->PassageConfig.DistanceMax));
|
||||
}
|
||||
if (Neighbor.StrateIndex == Slot.StrateIndex + 1 && Definition != nullptr)
|
||||
{
|
||||
DepartureRadiusQ = std::max(OwnerVoxelsToCoreQ(Definition->PassageConfig.DistanceMin),
|
||||
OwnerVoxelsToCoreQ(Definition->PassageConfig.DistanceMax));
|
||||
}
|
||||
}
|
||||
if (!BuildFiniteSurfaceRoute(Recipe, Stratum, ArrivalRadiusQ, DepartureRadiusQ, RouteError))
|
||||
{
|
||||
OutError = UTF8_TO_TCHAR(RouteError.c_str());
|
||||
return false;
|
||||
@@ -836,6 +1219,349 @@ void WriteCoreJsonValue(FCoreJsonWriter& Writer, const TCHAR* Identifier,
|
||||
}
|
||||
}
|
||||
|
||||
bool ResolveAuthoredCoreGeometry(VoxelForgeCore::ResolvedLayoutSnapshotV3& Layout,
|
||||
const UVoxelStrateManager* StrateManager, std::int32_t Seed, FString& OutError)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
std::vector<ResolvedLayoutStampRequestV1> LegacyStamps, MotifStamps;
|
||||
std::vector<ResolvedCavernRoomV1> Rooms;
|
||||
std::vector<ResolvedCavernEdgeV1> Edges;
|
||||
TSet<FString> FeatureNames;
|
||||
TSet<int32> NativeGraphStrates;
|
||||
const auto StableId = [&FeatureNames, &OutError](const FString& Name, int32 StrateIndex,
|
||||
const TCHAR* Kind, std::string& OutId)
|
||||
{
|
||||
if (Name.IsEmpty() || Name != Name.TrimStartAndEnd())
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core %s on strate %d needs a non-empty stable name without surrounding whitespace."), Kind, StrateIndex);
|
||||
return false;
|
||||
}
|
||||
if (FeatureNames.Contains(Name))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core stable name '%s' is duplicated across this world."), *Name);
|
||||
return false;
|
||||
}
|
||||
FeatureNames.Add(Name);
|
||||
OutId = CoreGeometryStableId(StrateIndex, Name);
|
||||
return true;
|
||||
};
|
||||
const auto Microns = [&OutError](double Value, int32 StrateIndex, const TCHAR* Field, std::int64_t& Out)
|
||||
{
|
||||
if (WorldAssemblyMetresToMicrometres(Value, Out)) return true;
|
||||
OutError = FString::Printf(TEXT("Strate %d CoreGeometry.%s must be finite and bounded in metres."), StrateIndex, Field);
|
||||
return false;
|
||||
};
|
||||
const auto VectorMicrons = [&OutError](const FVector& Value, int32 StrateIndex, const TCHAR* Field,
|
||||
std::array<std::int64_t, 3>& Out)
|
||||
{
|
||||
if (WorldAssemblyVectorToMicrometres(Value, Out)) return true;
|
||||
OutError = FString::Printf(TEXT("Strate %d CoreGeometry.%s must contain finite, bounded metre coordinates."), StrateIndex, Field);
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
const UVoxelStrateDefinition* Definition = Slot.Definition;
|
||||
if (!Definition || !HasCoreGeometry(*Definition)) continue;
|
||||
const FVoxelForgeCoreGeometry& Geometry = Definition->CoreGeometry;
|
||||
if (Geometry.GeneratorRecipe == EVoxelForgeCoreGeneratorRecipe::NativeCaves
|
||||
|| Geometry.GeneratorRecipe == EVoxelForgeCoreGeneratorRecipe::NativeLattice)
|
||||
NativeGraphStrates.Add(Slot.StrateIndex);
|
||||
|
||||
for (const FVoxelForgeCoreStampDefinition& Authored : Geometry.Stamps)
|
||||
{
|
||||
ResolvedLayoutStampRequestV1 Request;
|
||||
if (!StableId(Authored.StableName, Slot.StrateIndex, TEXT("stamp"), Request.stableId)
|
||||
|| !VectorMicrons(Authored.AnchorPositionMeters, Slot.StrateIndex, TEXT("Stamps.AnchorPositionMeters"), Request.anchorPositionMicrometres)
|
||||
|| !VectorMicrons(Authored.HalfExtentsMeters, Slot.StrateIndex, TEXT("Stamps.HalfExtentsMeters"), Request.halfExtentsMicrometres)
|
||||
|| !Microns(Authored.PlacementRadiusMeters, Slot.StrateIndex, TEXT("Stamps.PlacementRadiusMeters"), Request.placementRadiusMicrometres)
|
||||
|| !Microns(Authored.SmoothWidthMeters, Slot.StrateIndex, TEXT("Stamps.SmoothWidthMeters"), Request.smoothWidthMicrometres)
|
||||
|| !Microns(Authored.IrregularityAmplitudeMeters, Slot.StrateIndex, TEXT("Stamps.IrregularityAmplitudeMeters"), Request.irregularityAmplitudeMicrometres)
|
||||
|| !Microns(Authored.IrregularityWavelengthMeters, Slot.StrateIndex, TEXT("Stamps.IrregularityWavelengthMeters"), Request.irregularityWavelengthMicrometres)
|
||||
|| !Microns(Authored.RoundingRadiusMeters, Slot.StrateIndex, TEXT("Stamps.RoundingRadiusMeters"), Request.roundingRadiusMicrometres)
|
||||
|| !Microns(Authored.MinimumSpacingMeters, Slot.StrateIndex, TEXT("Stamps.MinimumSpacingMeters"), Request.minimumSpacingMicrometres)
|
||||
|| !Microns(Authored.FloorMeters, Slot.StrateIndex, TEXT("Stamps.FloorMeters"), Request.floorMicrometres)) return false;
|
||||
Request.strateIndex = Slot.StrateIndex;
|
||||
switch (Authored.Kind)
|
||||
{
|
||||
case EVoxelForgeCoreStampKind::CarveEllipsoid: Request.kind = ResolvedSdfStampKindV1::carveEllipsoid; break;
|
||||
case EVoxelForgeCoreStampKind::CarveArch: Request.kind = ResolvedSdfStampKindV1::carveArch; break;
|
||||
case EVoxelForgeCoreStampKind::FillPillar: Request.kind = ResolvedSdfStampKindV1::fillPillar; break;
|
||||
case EVoxelForgeCoreStampKind::FillDais: Request.kind = ResolvedSdfStampKindV1::fillDais; break;
|
||||
case EVoxelForgeCoreStampKind::FillMonolith: Request.kind = ResolvedSdfStampKindV1::fillMonolith; break;
|
||||
case EVoxelForgeCoreStampKind::FillPlatform: Request.kind = ResolvedSdfStampKindV1::fillPlatform; break;
|
||||
}
|
||||
Request.attachmentPolicy = Authored.bAttachToFloor ? 1u : 0u;
|
||||
Request.yawQuarterTurns = Authored.YawQuarterTurns;
|
||||
if (Request.kind == ResolvedSdfStampKindV1::fillMonolith || Request.kind == ResolvedSdfStampKindV1::fillPlatform)
|
||||
MotifStamps.push_back(std::move(Request));
|
||||
else LegacyStamps.push_back(std::move(Request));
|
||||
}
|
||||
|
||||
TSet<FString> RoomNames;
|
||||
for (const FVoxelForgeCoreRoomDefinition& Authored : Geometry.Rooms)
|
||||
{
|
||||
ResolvedCavernRoomV1 Room;
|
||||
if (!StableId(Authored.StableName, Slot.StrateIndex, TEXT("room"), Room.stableId)
|
||||
|| !VectorMicrons(Authored.CenterMeters, Slot.StrateIndex, TEXT("Rooms.CenterMeters"), Room.centerMicrometres)
|
||||
|| !VectorMicrons(Authored.HalfExtentsMeters, Slot.StrateIndex, TEXT("Rooms.HalfExtentsMeters"), Room.halfExtentsMicrometres)
|
||||
|| !Microns(Authored.FloorMeters, Slot.StrateIndex, TEXT("Rooms.FloorMeters"), Room.floorMicrometres)
|
||||
|| !Microns(Authored.SmoothWidthMeters, Slot.StrateIndex, TEXT("Rooms.SmoothWidthMeters"), Room.smoothWidthMicrometres)
|
||||
|| !Microns(Authored.UnionWidthMeters, Slot.StrateIndex, TEXT("Rooms.UnionWidthMeters"), Room.unionWidthMicrometres)
|
||||
|| !Microns(Authored.IrregularityAmplitudeMeters, Slot.StrateIndex, TEXT("Rooms.IrregularityAmplitudeMeters"), Room.irregularityAmplitudeMicrometres)
|
||||
|| !Microns(Authored.IrregularityWavelengthMeters, Slot.StrateIndex, TEXT("Rooms.IrregularityWavelengthMeters"), Room.irregularityWavelengthMicrometres)) return false;
|
||||
Room.strateIndex = Slot.StrateIndex;
|
||||
RoomNames.Add(Authored.StableName);
|
||||
Rooms.push_back(std::move(Room));
|
||||
}
|
||||
|
||||
for (const FVoxelForgeCoreTunnelDefinition& Authored : Geometry.Tunnels)
|
||||
{
|
||||
ResolvedCavernEdgeV1 Edge;
|
||||
if (!StableId(Authored.StableName, Slot.StrateIndex, TEXT("tunnel"), Edge.stableId)) return false;
|
||||
if (Authored.StartAnchorStableName.IsEmpty() || Authored.EndRoomStableName.IsEmpty()
|
||||
|| !RoomNames.Contains(Authored.EndRoomStableName))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core tunnel '%s' needs a start-anchor stable name and an end-room name that identifies a room on strate %d."), *Authored.StableName, Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
FTCHARToUTF8 StartAnchorUtf8(*Authored.StartAnchorStableName);
|
||||
Edge.startAnchorStableId = StartAnchorUtf8.Get();
|
||||
Edge.endRoomStableId = CoreGeometryStableId(Slot.StrateIndex, Authored.EndRoomStableName);
|
||||
Edge.strateIndex = Slot.StrateIndex;
|
||||
const FVector ControlPoints[4] = { Authored.ControlPoint0Meters, Authored.ControlPoint1Meters,
|
||||
Authored.ControlPoint2Meters, Authored.ControlPoint3Meters };
|
||||
for (std::size_t Point = 0; Point < 4; ++Point)
|
||||
if (!WorldAssemblyVectorToMicrometres(ControlPoints[Point], Edge.controlPointsMicrometres[Point]))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core tunnel '%s' has a non-finite or out-of-range control point in metres."), *Authored.StableName);
|
||||
return false;
|
||||
}
|
||||
if (!Microns(Authored.BoreRadiusMeters, Slot.StrateIndex, TEXT("Tunnels.BoreRadiusMeters"), Edge.boreRadiusMicrometres)
|
||||
|| !Microns(Authored.BoreHeightMeters, Slot.StrateIndex, TEXT("Tunnels.BoreHeightMeters"), Edge.boreHeightMicrometres)
|
||||
|| !Microns(Authored.FloorSupportDepthMeters, Slot.StrateIndex, TEXT("Tunnels.FloorSupportDepthMeters"), Edge.floorSupportDepthMicrometres)
|
||||
|| !Microns(Authored.SmoothWidthMeters, Slot.StrateIndex, TEXT("Tunnels.SmoothWidthMeters"), Edge.smoothWidthMicrometres)
|
||||
|| !Microns(Authored.UnionWidthMeters, Slot.StrateIndex, TEXT("Tunnels.UnionWidthMeters"), Edge.unionWidthMicrometres)
|
||||
|| !Microns(Authored.IrregularityAmplitudeMeters, Slot.StrateIndex, TEXT("Tunnels.IrregularityAmplitudeMeters"), Edge.irregularityAmplitudeMicrometres)
|
||||
|| !Microns(Authored.IrregularityWavelengthMeters, Slot.StrateIndex, TEXT("Tunnels.IrregularityWavelengthMeters"), Edge.irregularityWavelengthMicrometres)) return false;
|
||||
for (const FVector& Point : Authored.PolylineMeters)
|
||||
{
|
||||
std::array<std::int64_t, 3> Converted{};
|
||||
if (!WorldAssemblyVectorToMicrometres(Point, Converted))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core tunnel '%s' has an invalid metre polyline point."), *Authored.StableName);
|
||||
return false;
|
||||
}
|
||||
Edge.polylineMicrometres.push_back(Converted);
|
||||
}
|
||||
Edges.push_back(std::move(Edge));
|
||||
}
|
||||
if ((!Geometry.Rooms.IsEmpty() || !Geometry.Tunnels.IsEmpty())
|
||||
&& Geometry.GeneratorRecipe != EVoxelForgeCoreGeneratorRecipe::NativeCaves
|
||||
&& Geometry.GeneratorRecipe != EVoxelForgeCoreGeneratorRecipe::NativeLattice)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d authors a cavern graph without selecting NativeCaves or NativeLattice."), Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (!Slot.Definition) continue;
|
||||
for (const FStrateLandmark& Landmark : Slot.Definition->Landmarks)
|
||||
if (!Landmark.CoreGeometryStableName.IsEmpty() && !FeatureNames.Contains(Landmark.CoreGeometryStableName))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Landmark on strate %d references unknown Core stable name '%s'."), Slot.StrateIndex, *Landmark.CoreGeometryStableName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (int32 StrateIndex : NativeGraphStrates)
|
||||
{
|
||||
const bool bHasRoom = std::any_of(Rooms.begin(), Rooms.end(), [StrateIndex](const auto& Room) { return Room.strateIndex == StrateIndex; });
|
||||
const bool bHasEdge = std::any_of(Edges.begin(), Edges.end(), [StrateIndex](const auto& Edge) { return Edge.strateIndex == StrateIndex; });
|
||||
if (!bHasRoom || !bHasEdge)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Native cave/lattice recipe on strate %d needs at least one authored room and one tunnel."), StrateIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!LegacyStamps.empty() || !MotifStamps.empty())
|
||||
{
|
||||
std::string CoreError;
|
||||
const ResolvedLayoutSnapshotV3 BaseLayout = Layout;
|
||||
if (!LegacyStamps.empty() && !ResolveResolvedLayoutStampsV1(Layout, Seed, LegacyStamps, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core authored stamp resolution failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (!MotifStamps.empty())
|
||||
{
|
||||
ResolvedLayoutSnapshotV3 MotifLayout = BaseLayout;
|
||||
if (!ResolveResolvedLayoutStampsV3(MotifLayout, Seed, MotifStamps, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core authored motif resolution failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (LegacyStamps.empty()) Layout = std::move(MotifLayout);
|
||||
else
|
||||
{
|
||||
Layout.stamps.insert(Layout.stamps.end(), MotifLayout.stamps.begin(), MotifLayout.stamps.end());
|
||||
Layout.stampVocabularyVersion = ResolvedLayoutStampVocabularyVersionV3;
|
||||
std::vector<ResolvedLayoutIssueV2> Issues;
|
||||
if (!Layout.finalize(CoreError) || !Layout.validate(ResolvedLayoutValidationModeV3::strict, Issues, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Combined Core stamp layout failed validation: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Rooms.empty() || !Edges.empty())
|
||||
{
|
||||
std::string CoreError;
|
||||
if (!ResolveResolvedCavernGraphV1(Layout, Seed, Rooms, Edges, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core authored cavern graph resolution failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AttachCoreGeometryOnlyLayout(const UVoxelStrateManager* StrateManager,
|
||||
double OwnerVoxelCm, double CoreVoxelCm, std::uint32_t LayoutChunkMicrometres,
|
||||
std::int64_t LayoutChunkSubvoxels, const TSharedRef<FJsonObject>& Root, FString& OutError)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
ResolvedLayoutSnapshotV3 Layout;
|
||||
Layout.worldIdentity = std::string("owner-world/seed/") + std::to_string(StrateManager->GetWorldSeed());
|
||||
Layout.ownerVoxelSizeMicrometres = static_cast<std::uint32_t>(std::llround(OwnerVoxelCm * 10000.0));
|
||||
Layout.coreVoxelSizeMicrometres = static_cast<std::uint32_t>(std::llround(CoreVoxelCm * 10000.0));
|
||||
Layout.strateLayoutChunkSizeMicrometres = LayoutChunkMicrometres;
|
||||
Layout.strateLayoutChunkWidthSubvoxels = LayoutChunkSubvoxels;
|
||||
const std::uint32_t CoreVoxelMicrometres = Layout.coreVoxelSizeMicrometres;
|
||||
const auto GeneratorName = [](ECaveGeneratorType Type) -> const char*
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork: return "TunnelNetwork";
|
||||
case ECaveGeneratorType::FlatPlain: return "FlatPlain";
|
||||
case ECaveGeneratorType::CrystalChamber: return "CrystalChamber";
|
||||
case ECaveGeneratorType::Maze: return "Maze";
|
||||
case ECaveGeneratorType::SurfaceWorld: return "SurfaceWorld";
|
||||
case ECaveGeneratorType::VerticalShafts: return "VerticalShafts";
|
||||
case ECaveGeneratorType::FloatingIslands: return "FloatingIslands";
|
||||
case ECaveGeneratorType::Underwater: return "Underwater";
|
||||
}
|
||||
return "Unknown";
|
||||
};
|
||||
const auto TransitionName = [](EVoxelStrateTransition Type) -> const char*
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case EVoxelStrateTransition::Gradient: return "Gradient";
|
||||
case EVoxelStrateTransition::Hard: return "Hard";
|
||||
case EVoxelStrateTransition::Interleaved: return "Interleaved";
|
||||
}
|
||||
return "Unknown";
|
||||
};
|
||||
std::vector<ResolvedLayoutPassageConfigV1> PassageConfigs;
|
||||
const std::int64_t OwnerVoxelMicrometres = Layout.ownerVoxelSizeMicrometres;
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (!Slot.Definition) continue;
|
||||
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, Slot.TopChunkZ));
|
||||
ResolvedLayoutStratumV2 Item;
|
||||
Item.strateIndex = Slot.StrateIndex;
|
||||
Item.generatorKind = GeneratorName(Type);
|
||||
Item.stableId = Layout.worldIdentity + "/strate/" + std::to_string(Slot.StrateIndex)
|
||||
+ "/generator/" + GeneratorName(Type);
|
||||
Item.recipeId = Type == ECaveGeneratorType::SurfaceWorld ? "SurfaceWorld.Heightfield2D"
|
||||
: Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField"
|
||||
: std::string("legacy.") + GeneratorName(Type);
|
||||
Item.recipeVersion = 0;
|
||||
if (!ConfigureCoreGeometryStratum(Slot, Type, false, Item, OutError)) return false;
|
||||
Item.voxelSizeMicrometres = CoreVoxelMicrometres;
|
||||
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
|
||||
Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
|
||||
Item.managerChunkWidthMicrometres = LayoutChunkMicrometres;
|
||||
Item.dispatchChunkWidthMicrometres = LayoutChunkMicrometres;
|
||||
Item.transitionMode = TransitionName(Slot.ResolvedTransitionType);
|
||||
Item.transitionBlendChunks = Slot.ResolvedTransitionBlendChunks;
|
||||
Item.transitionWidthMicrometres = Slot.TransitionWidthMicrometres;
|
||||
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
|
||||
? std::min(Item.transitionWidthMicrometres / 2, static_cast<std::int64_t>(LayoutChunkMicrometres)) : 0;
|
||||
Item.transitionInterpenetrationMicrometres = Item.transitionMode == "Interleaved"
|
||||
? Item.transitionWidthMicrometres : Item.transitionMode == "Gradient" ? Item.transitionWidthMicrometres / 4 : 0;
|
||||
Item.transitionFallsBackToHard = Item.transitionMode != "Hard" && Item.transitionWidthMicrometres <= 0;
|
||||
Item.transitionHonouredInCore = Item.transitionMode == "Hard" || !Item.transitionFallsBackToHard;
|
||||
Item.managerLookup = "UVoxelStrateManager::GetGeneratorTypeForChunk; upper strate owns transitions";
|
||||
Item.boundDispatchPath = "CoreGeometry-only resolved strate layout";
|
||||
Item.queryDispatchPath = "CoreGeometry-only native layout field";
|
||||
|
||||
const std::int64_t SealMicrometres = static_cast<std::int64_t>(std::llround(
|
||||
std::max(0.0f, StrateBoundarySealThickness(*Slot.Definition)) * OwnerVoxelMicrometres));
|
||||
if (SealMicrometres <= 0)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Strate %d has no positive BoundarySealThickness for the shared ceiling and bedrock bands."), Slot.StrateIndex);
|
||||
return false;
|
||||
}
|
||||
const std::int64_t SolidDensityQ16 = std::max<std::int64_t>(1,
|
||||
WorldAssemblyQ16(std::max(0.05f, StrateBoundarySolidDensity(*Slot.Definition))));
|
||||
Item.ceiling = { "ceiling", Item.interiorTopMicrometres - SealMicrometres,
|
||||
Item.interiorTopMicrometres, true, true, false, "solid ceiling from strate BoundarySealThickness",
|
||||
"interior-facing lower surface", "shared resolved-layout density band", SolidDensityQ16 };
|
||||
Item.bedrock = { "bedrock", Item.interiorBottomMicrometres,
|
||||
Item.interiorBottomMicrometres + SealMicrometres, true, true, false,
|
||||
"solid bedrock floor from strate BoundarySealThickness", "interior-facing upper surface",
|
||||
"shared resolved-layout density band", SolidDensityQ16 };
|
||||
Item.content = { "content", Item.interiorBottomMicrometres + SealMicrometres,
|
||||
Item.interiorTopMicrometres - SealMicrometres, true, true, false,
|
||||
"resolved generator interior", "none", "none" };
|
||||
Layout.strates.push_back(std::move(Item));
|
||||
|
||||
const FStratePassageConfig& Passage = Slot.ResolvedPassageConfig;
|
||||
ResolvedLayoutPassageConfigV1 Config;
|
||||
Config.upperStrateIndex = Slot.StrateIndex;
|
||||
Config.connections = Passage.Connections;
|
||||
Config.style = static_cast<std::uint32_t>(Passage.Style);
|
||||
Config.mouthRadiusSubvoxels = std::max(WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMouthRadiusMicrometres, CoreVoxelMicrometres),
|
||||
WorldAssemblyMicrometresToSubvoxels(36 * OwnerVoxelMicrometres, CoreVoxelMicrometres));
|
||||
Config.midRadiusSubvoxels = std::max(WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMidRadiusMicrometres, CoreVoxelMicrometres),
|
||||
WorldAssemblyMicrometresToSubvoxels(24 * OwnerVoxelMicrometres, CoreVoxelMicrometres));
|
||||
Config.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageReachMinimumMicrometres, CoreVoxelMicrometres);
|
||||
Config.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageReachMaximumMicrometres, CoreVoxelMicrometres);
|
||||
Config.distanceMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageDistanceMinimumMicrometres, CoreVoxelMicrometres);
|
||||
Config.distanceMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageDistanceMaximumMicrometres, CoreVoxelMicrometres);
|
||||
Config.wanderSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageWanderMicrometres, CoreVoxelMicrometres);
|
||||
Config.segments = static_cast<std::uint32_t>(FMath::Clamp(Passage.Segments, 1, 48));
|
||||
Config.verticalWobbleSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageVerticalWobbleMicrometres, CoreVoxelMicrometres);
|
||||
Config.spiralRadiusSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageSpiralRadiusMicrometres, CoreVoxelMicrometres);
|
||||
Config.spiralTurnsQ16 = static_cast<std::int64_t>(std::llround(static_cast<double>(Passage.SpiralTurns) * 65536.0));
|
||||
Config.cascadeSteps = static_cast<std::uint32_t>(FMath::Clamp(Passage.CascadeSteps, 1, 16));
|
||||
Config.cascadeLedgeSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageCascadeLedgeMicrometres, CoreVoxelMicrometres);
|
||||
PassageConfigs.push_back(Config);
|
||||
}
|
||||
std::string CoreError;
|
||||
if (!PlanResolvedLayoutPassagesV1(Layout, StrateManager->GetWorldSeed(), PassageConfigs, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Resolved inter-strate passage planning failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (!ResolveAuthoredCoreGeometry(Layout, StrateManager, StrateManager->GetWorldSeed(), OutError)) return false;
|
||||
if (!Layout.finalize(CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core resolved-layout export failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
const std::string Json = Layout.ToJson();
|
||||
Root->SetStringField(TEXT("resolved_layout_json"), UTF8_TO_TCHAR(Json.c_str()));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
const UVoxelStrateManager* StrateManager,
|
||||
double CoreVoxelSizeCm,
|
||||
@@ -876,6 +1602,7 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
Generated.recipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels;
|
||||
int32 SurfaceSlotCount = 0;
|
||||
int32 IslandSlotCount = 0;
|
||||
int32 CoreGeometrySlotCount = 0;
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
@@ -883,10 +1610,13 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
FIntVector(0, 0, Slot.TopChunkZ));
|
||||
SurfaceSlotCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0;
|
||||
IslandSlotCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0;
|
||||
CoreGeometrySlotCount += HasCoreGeometry(*Slot.Definition) ? 1 : 0;
|
||||
}
|
||||
if (SurfaceSlotCount == 0 && IslandSlotCount == 0)
|
||||
{
|
||||
return true;
|
||||
return CoreGeometrySlotCount == 0 ? true
|
||||
: AttachCoreGeometryOnlyLayout(StrateManager, OwnerVoxelSizeCm, CoreVoxelSizeCm,
|
||||
LayoutChunkSizeMicrometres, LayoutChunkWidthSubvoxels, Root, OutError);
|
||||
}
|
||||
if (SurfaceSlotCount == 0)
|
||||
{
|
||||
@@ -918,7 +1648,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
Generated.plan.strates.push_back(std::move(Stratum));
|
||||
++BuiltSurfaceCount;
|
||||
}
|
||||
else if (Type == ECaveGeneratorType::FloatingIslands)
|
||||
// Native floating-island evaluation remains an explicit imported-plan opt-in.
|
||||
// The default owner path uses the legacy generator for this slot; emitting an
|
||||
// automatic FI plan here silently switches density dispatch and changes the
|
||||
// shared passage mouth before the owner has opted into that field.
|
||||
else if (Type == ECaveGeneratorType::FloatingIslands && bImportedOverride)
|
||||
{
|
||||
if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated,
|
||||
CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError))
|
||||
@@ -991,10 +1725,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
}
|
||||
}
|
||||
if (Manifest->plan.strates.size() != static_cast<std::size_t>(SurfaceSlotCount)
|
||||
|| Manifest->floatingIslands.size() != static_cast<std::size_t>(FloatingIslandSlotCount))
|
||||
|| (!Manifest->floatingIslands.empty()
|
||||
&& Manifest->floatingIslands.size() != static_cast<std::size_t>(FloatingIslandSlotCount)))
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core world assembly has %d SurfaceWorld and %d floating-island plans for %d resolved SurfaceWorld and %d FloatingIslands strates."),
|
||||
TEXT("Core world assembly has %d SurfaceWorld and %d floating-island plans for %d resolved SurfaceWorld strates; an explicit FI plan must cover all %d FloatingIslands strates."),
|
||||
static_cast<int32>(Manifest->plan.strates.size()),
|
||||
static_cast<int32>(Manifest->floatingIslands.size()),
|
||||
SurfaceSlotCount, FloatingIslandSlotCount);
|
||||
@@ -1088,12 +1823,12 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMouthRadiusMicrometres, ExpectedCoreVoxelMicrons),
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||
36 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||
PassageConfig.midRadiusSubvoxels = std::max(
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMidRadiusMicrometres, ExpectedCoreVoxelMicrons),
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||
24 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||
PassageConfig.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageReachMinimumMicrometres, ExpectedCoreVoxelMicrons);
|
||||
PassageConfig.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
@@ -1128,6 +1863,10 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
: Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField"
|
||||
: std::string("legacy.") + generatorName(Type);
|
||||
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
|
||||
if (!ConfigureCoreGeometryStratum(Slot, Type, bImportedOverride, Item, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
|
||||
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
|
||||
Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
|
||||
@@ -1264,6 +2003,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
UTF8_TO_TCHAR(PassagePlanError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (!ResolveAuthoredCoreGeometry(Layout, StrateManager,
|
||||
StrateManager->GetWorldSeed(), OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::string LayoutError;
|
||||
if (!Layout.finalize(LayoutError))
|
||||
{
|
||||
@@ -1372,6 +2116,10 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0)));
|
||||
Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("core_voxel_size_microns"), CoreVoxelSizeMicrons);
|
||||
// Core import des longueurs owner avec le rapport physique resolu, pas un ratio suppose.
|
||||
// Core imports owner lengths with the resolved physical ratio, never an assumed scale.
|
||||
Root->SetNumberField(TEXT("owner_to_core_length_scale"),
|
||||
OwnerVoxelSizeCm / ResolvedCoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("seed"), Seed);
|
||||
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
|
||||
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||
|
||||
@@ -2590,8 +2590,8 @@ static float EvaluateInternal(const UVoxelGenerator& Generator,
|
||||
{
|
||||
EnsurePlan(Generator);
|
||||
}
|
||||
const bool bChunkResolved = Context != nullptr && Context->bChunkResolved
|
||||
|| ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved;
|
||||
const bool bChunkResolved = (Context != nullptr && Context->bChunkResolved)
|
||||
|| (ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved);
|
||||
const FIntVector Chunk = Context != nullptr && Context->bChunkResolved
|
||||
? Context->Chunk
|
||||
: ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved
|
||||
|
||||
@@ -1958,10 +1958,11 @@ namespace VoxelSeasonCompositionPrivate
|
||||
}
|
||||
|
||||
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
|
||||
FVoxelSeasonFixedStrate& OutFixed)
|
||||
double LayoutChunkMeters, FVoxelSeasonFixedStrate& OutFixed)
|
||||
{
|
||||
OutFixed = FVoxelSeasonFixedStrate();
|
||||
OutFixed.HeightInChunks = Definition.StrateHeightInChunks;
|
||||
OutFixed.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
|
||||
Definition.StrateHeightMeters / FMath::Max(0.001, LayoutChunkMeters)));
|
||||
OutFixed.Archetype = Definition.GeneratorType;
|
||||
OutFixed.SourceDefinitionPath = Definition.GetPathName();
|
||||
OutFixed.bUsesRecipe = false;
|
||||
@@ -1999,7 +2000,9 @@ namespace VoxelSeasonCompositionPrivate
|
||||
return false;
|
||||
}
|
||||
FVoxelSeasonFixedStrate Fixed;
|
||||
CopyDefinitionParams(*Definition, Fixed);
|
||||
const double LayoutChunkMeters = Settings.WorldSettings != nullptr
|
||||
? Settings.WorldSettings->StrateLayoutChunkSizeMeters : 8.0;
|
||||
CopyDefinitionParams(*Definition, LayoutChunkMeters, Fixed);
|
||||
Fixed.Seed = SeasonSeed;
|
||||
if (Fixed.SourceDefinitionPath.IsEmpty())
|
||||
{
|
||||
@@ -2974,7 +2977,9 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* Wo
|
||||
if (WorldSettings != nullptr)
|
||||
{
|
||||
Settings.SelectedStrateCount = WorldSettings->TotalStrates;
|
||||
Settings.InterStrateGapChunks = WorldSettings->InterStrateGapChunks;
|
||||
Settings.InterStrateGapChunks = FMath::Max(0, FMath::RoundToInt(
|
||||
WorldSettings->InterStrateGapMeters * 1000000.0
|
||||
/ FMath::Max(1.0, static_cast<double>(WorldSettings->StrateLayoutChunkSizeMeters) * 1000000.0)));
|
||||
Settings.OriginSpineRadius = WorldSettings->OriginSpineRadius;
|
||||
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
|
||||
Settings.SeasonNumber = WorldSettings->CurrentSeason;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "VoxelSettings.h"
|
||||
|
||||
#include "VoxelForgeAssetVersion.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelSeasonAsset.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -9,6 +11,74 @@
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include <string>
|
||||
|
||||
void UVoxelSettings::Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
|
||||
Super::Serialize(Ar);
|
||||
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
|
||||
< VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
|
||||
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
|
||||
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
|
||||
* LayoutChunkMeters;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetUpgrade] type=Settings asset=%s gap_chunks=%d gap_m=%.6f layout_chunk_m=%.6f"),
|
||||
*GetPathName(), InterStrateGapChunks, InterStrateGapMeters, LayoutChunkMeters);
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelSettings::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
||||
>= VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the legacy count against this settings asset's saved physical layout width.
|
||||
// Convertit les anciens comptes avec la largeur physique sauvegardee dans ces reglages.
|
||||
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
|
||||
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
|
||||
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
|
||||
* LayoutChunkMeters;
|
||||
|
||||
TSet<UVoxelStrateDefinition*> Migrated;
|
||||
const auto MigrateStrate = [this, LayoutChunkMeters, &Migrated](
|
||||
const TSoftObjectPtr<UVoxelStrateDefinition>& SoftDefinition)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = SoftDefinition.LoadSynchronous();
|
||||
if (Definition == nullptr || Migrated.Contains(Definition)
|
||||
|| Definition->GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
||||
>= VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Migrated.Add(Definition);
|
||||
const int32 OldHeightChunks = Definition->StrateHeightInChunks;
|
||||
const int32 OldTransitionChunks = Definition->TransitionBlendChunks;
|
||||
Definition->StrateHeightMeters = static_cast<double>(FMath::Max(1, OldHeightChunks))
|
||||
* LayoutChunkMeters;
|
||||
Definition->TransitionWidthMeters = static_cast<double>(FMath::Max(0, OldTransitionChunks))
|
||||
* LayoutChunkMeters;
|
||||
Definition->MarkPackageDirty();
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f effective_layout_chunk_m=%.6f"),
|
||||
*Definition->GetPathName(), OldHeightChunks, Definition->StrateHeightMeters,
|
||||
OldTransitionChunks, Definition->TransitionWidthMeters, LayoutChunkMeters);
|
||||
};
|
||||
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Definition : StratePool)
|
||||
{
|
||||
MigrateStrate(Definition);
|
||||
}
|
||||
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Definition : FixedStrates)
|
||||
{
|
||||
MigrateStrate(Definition.Value);
|
||||
}
|
||||
MarkPackageDirty();
|
||||
}
|
||||
|
||||
int32 UVoxelSettings::GetEffectiveWorldSeed() const
|
||||
{
|
||||
if (!Season.IsNull())
|
||||
|
||||
@@ -297,7 +297,11 @@ void RecordTile(const FTileSample& Sample)
|
||||
"\"submitted_t_s\":%s,\"worker_start_t_s\":%s,\"worker_end_t_s\":%s,\"worker_thread_id\":%u,"
|
||||
"\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu,"
|
||||
"\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,\"worker_task_s\":%s,"
|
||||
"\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
|
||||
"\"generation_s\":%s,"
|
||||
#if PLATFORM_LINUX
|
||||
"\"core_generate_s\":%s,\"mesh_copy_s\":%s,\"rmc_stream_build_s\":%s,"
|
||||
#endif
|
||||
"\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
|
||||
Sample.TileX, Sample.TileY, Sample.TileZ,
|
||||
Sample.Level, Sample.WorkPriority,
|
||||
static_cast<unsigned long long>(Sample.RequestOrdinal),
|
||||
@@ -312,8 +316,12 @@ void RecordTile(const FTileSample& Sample)
|
||||
static_cast<unsigned long long>(Sample.GeometryBytes),
|
||||
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
||||
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
||||
*Number(Sample.WorkerTaskSeconds),
|
||||
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds),
|
||||
*Number(Sample.WorkerTaskSeconds), *Number(Sample.GenerationSeconds),
|
||||
#if PLATFORM_LINUX
|
||||
*Number(Sample.CoreGenerateSeconds), *Number(Sample.MeshCopySeconds),
|
||||
*Number(Sample.StreamBuildSeconds),
|
||||
#endif
|
||||
*Number(Sample.ClassifySeconds),
|
||||
*Number(Sample.MeshSeconds), *Number(Sample.StreamSeconds), *Number(Sample.ApplySeconds)));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
// Serialized strate-definition migrations.
|
||||
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelForgeAssetVersion.h"
|
||||
|
||||
#include "Serialization/CustomVersion.h"
|
||||
#include "VoxelPassageGeometry.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// The field name is retained for package compatibility, but version 1 changes its meaning
|
||||
// from a raw tangent/gradient to authored degrees. This GUID is local to the plugin.
|
||||
const FGuid GVoxelForgeStrateSerializationVersion(
|
||||
0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
|
||||
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = 1;
|
||||
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = VoxelForgeAssetVersion::TunnelFloorSlopeDegrees;
|
||||
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
|
||||
GVoxelForgeStrateSerializationVersion,
|
||||
GVoxelForgeStrateGentleSlopeDegreesVersion,
|
||||
VoxelForgeAssetVersion::GUID,
|
||||
VoxelForgeAssetVersion::MetreAuthoringV1,
|
||||
TEXT("VoxelForgeStrateSerialization"));
|
||||
|
||||
float LegacyGradientToDegrees(float LegacyGradient)
|
||||
@@ -58,6 +55,31 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelStrateDefinition::Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
|
||||
Super::Serialize(Ar);
|
||||
|
||||
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
|
||||
< VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
// A stand-alone strate has no owning settings object during Serialize. Convert using the
|
||||
// current 8 m layout default here; UVoxelSettings corrects referenced old strates using
|
||||
// their saved effective width during its own load upgrade.
|
||||
// Une strate seule n'a pas ses reglages proprietaires ici; UVoxelSettings corrige ensuite
|
||||
// les references avec leur largeur physique sauvegardee.
|
||||
constexpr double LegacyDefaultLayoutChunkMeters = 8.0;
|
||||
StrateHeightMeters = static_cast<double>(FMath::Max(1, StrateHeightInChunks))
|
||||
* LegacyDefaultLayoutChunkMeters;
|
||||
TransitionWidthMeters = static_cast<double>(FMath::Max(0, TransitionBlendChunks))
|
||||
* LegacyDefaultLayoutChunkMeters;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f fallback_layout_chunk_m=%.6f"),
|
||||
*GetPathName(), StrateHeightInChunks, StrateHeightMeters,
|
||||
TransitionBlendChunks, TransitionWidthMeters, LegacyDefaultLayoutChunkMeters);
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelStrateDefinition::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
@@ -67,7 +89,7 @@ void UVoxelStrateDefinition::PostLoad()
|
||||
// is normalized to the ignored compatibility default before any runtime copy is made.
|
||||
GenerationParams.bTunnelFloorEnabled = true;
|
||||
|
||||
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion)
|
||||
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
||||
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -1255,7 +1255,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
? SeasonManifest.Strates.Num() : Settings->TotalStrates;
|
||||
const int32 LayoutGapChunks = bUseSeason
|
||||
? SeasonManifest.InterStrateGapChunks
|
||||
: FMath::Max(0, Settings->InterStrateGapChunks);
|
||||
: FMath::Max(0, FMath::RoundToInt(static_cast<double>(Settings->InterStrateGapMeters)
|
||||
* 1000000.0 / std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
||||
|
||||
//=========================================================================
|
||||
// STEP 1: Build shuffled pool (seed-based randomization)
|
||||
@@ -1369,7 +1370,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
|
||||
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
|
||||
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
|
||||
Slot.Definition->StrateHeightMeters = static_cast<double>(SeasonStrate.HeightInChunks)
|
||||
* StrateLayoutChunkSizeMicrometres / 1000000.0;
|
||||
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
|
||||
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
|
||||
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams;
|
||||
@@ -1426,7 +1428,9 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
|
||||
// Resolve the authored chunk height into an exact physical interval.
|
||||
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
||||
Slot.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
|
||||
Slot.Definition->StrateHeightMeters * 1000000.0
|
||||
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
||||
Slot.TopWorldMicrometres = CurrentTopWorldMicrometres;
|
||||
Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
@@ -1439,9 +1443,11 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
|
||||
Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
|
||||
Slot.ResolvedTransitionBlendChunks = Slot.Definition->TransitionBlendChunks;
|
||||
Slot.TransitionWidthMicrometres = static_cast<int64>(
|
||||
Slot.ResolvedTransitionBlendChunks) * StrateLayoutChunkSizeMicrometres;
|
||||
Slot.ResolvedTransitionBlendChunks = FMath::Max(0, FMath::RoundToInt(
|
||||
Slot.Definition->TransitionWidthMeters * 1000000.0
|
||||
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
||||
Slot.TransitionWidthMicrometres = static_cast<int64>(Slot.ResolvedTransitionBlendChunks)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig;
|
||||
Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.MouthRadius);
|
||||
|
||||
@@ -82,6 +82,17 @@
|
||||
#include <atomic>
|
||||
#include <unordered_set>
|
||||
|
||||
static FORCEINLINE int32 VF_ClipLevelScale(int32 Level)
|
||||
{
|
||||
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
|
||||
return static_cast<int32>(uint32{1} << SafeLevel);
|
||||
}
|
||||
static FORCEINLINE int32 VF_ClipLevelExtentVoxels(int32 Level)
|
||||
{
|
||||
return static_cast<int32>(static_cast<uint32>(CHUNK_SIZE)
|
||||
* static_cast<uint32>(VF_ClipLevelScale(Level)));
|
||||
}
|
||||
|
||||
static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile,
|
||||
const FVector& BoundsMin, const FVector& BoundsMax);
|
||||
static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B);
|
||||
@@ -227,11 +238,11 @@ namespace
|
||||
GVoxelForgeStreamingResultBacklogCap,
|
||||
TEXT("Maximum queued results plus admitted tile tasks; 0 disables the safety cap. This is separate from the worker-slot cap."));
|
||||
|
||||
int32 GVoxelForgeStreamingReadyQueueDepth = 8;
|
||||
int32 GVoxelForgeStreamingReadyQueueDepth = 16;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingReadyQueueDepth(
|
||||
TEXT("voxel.StreamingReadyQueueDepth"),
|
||||
GVoxelForgeStreamingReadyQueueDepth,
|
||||
TEXT("Extra admitted tile tasks kept ready beyond the worker slots, up to one worker-set; 0 disables the refill queue."));
|
||||
TEXT("Extra admitted tile tasks kept ready beyond worker slots, up to four worker-sets/64 tasks; 0 disables the refill queue."));
|
||||
|
||||
int32 GVoxelForgeStreamingBackgroundWorkerCap = 1;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingBackgroundWorkerCap(
|
||||
@@ -301,8 +312,8 @@ namespace
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingStartupApplyBoost(
|
||||
TEXT("voxel.StreamingStartupApplyBoost"),
|
||||
GVoxelForgeStreamingStartupApplyBoost,
|
||||
TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 12-mesh budget until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
|
||||
constexpr int32 StreamingStartupMaxMeshApplies = 12;
|
||||
TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 24-mesh cap until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
|
||||
constexpr int32 StreamingStartupMaxMeshApplies = 24;
|
||||
constexpr float StreamingStartupApplyBudgetMilliseconds = 14.0f;
|
||||
constexpr double StreamingStartupApplyMaxSeconds = 30.0;
|
||||
|
||||
@@ -2196,7 +2207,7 @@ void AVoxelWorld::BeginPlay()
|
||||
CommandLineStreamingReadyQueueDepth))
|
||||
{
|
||||
GVoxelForgeStreamingReadyQueueDepth = FMath::Clamp(
|
||||
CommandLineStreamingReadyQueueDepth, 0, 32);
|
||||
CommandLineStreamingReadyQueueDepth, 0, 64);
|
||||
}
|
||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingBackgroundWorkerCap="),
|
||||
CommandLineStreamingBackgroundWorkerCap))
|
||||
@@ -2623,7 +2634,8 @@ void AVoxelWorld::BeginPlay()
|
||||
FTaskGraphInterface& TaskGraph = FTaskGraphInterface::Get();
|
||||
LowLevelTasks::FScheduler& TaskScheduler = LowLevelTasks::FScheduler::Get();
|
||||
const int32 QueueDepth = FMath::Clamp(
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, GetMaxConcurrentStreamingTasks());
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0,
|
||||
FMath::Min(64, FMath::Max(1, GetMaxConcurrentStreamingTasks()) * 4));
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("worker_backend_config"), FString::Printf(
|
||||
TEXT("\"logical_cores\":%d,\"effective_tile_slots\":%d,"
|
||||
"\"task_api\":\"UE::Tasks::Launch\",\"task_scheduler_workers\":%u,"
|
||||
@@ -6240,17 +6252,19 @@ static void VF_AppendTouchingAncestors(
|
||||
const FVoxelTileKey& Tile, int32 AncestorLevel, CandidateArray& OutCandidates)
|
||||
{
|
||||
if (AncestorLevel <= Tile.Level) { return; }
|
||||
const int32 Divisor = 1 << (AncestorLevel - Tile.Level);
|
||||
const auto FloorDiv = [Divisor](int32 Value)
|
||||
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
|
||||
const auto FloorDiv = [Divisor](int64 Value)
|
||||
{
|
||||
return Value >= 0 ? Value / Divisor : -(((-Value) + Divisor - 1) / Divisor);
|
||||
const int64 Quotient = Value / Divisor;
|
||||
const int64 Remainder = Value % Divisor;
|
||||
return static_cast<int32>(Remainder < 0 ? Quotient - 1 : Quotient);
|
||||
};
|
||||
const auto AxisCandidates = [&FloorDiv, Divisor](int32 Value)
|
||||
{
|
||||
TArray<int32, TInlineAllocator<2>> Candidates;
|
||||
int32 First = FloorDiv(Value);
|
||||
if ((Value % Divisor) == 0) { --First; }
|
||||
const int32 Last = FloorDiv(Value + 1);
|
||||
const int32 Last = FloorDiv(static_cast<int64>(Value) + 1);
|
||||
for (int32 Coord = First; Coord <= Last; ++Coord) { Candidates.Add(Coord); }
|
||||
return Candidates;
|
||||
};
|
||||
@@ -6338,7 +6352,7 @@ void AVoxelWorld::UpdateVisualStalenessAudit()
|
||||
const FVector& WorldPosition, int32 Level, auto& OutCandidates)
|
||||
{
|
||||
const FVector LocalVoxel = WorldToLocalVoxel(WorldPosition);
|
||||
const float TileExtentVoxels = static_cast<float>(CHUNK_SIZE * (1 << Level));
|
||||
const float TileExtentVoxels = static_cast<float>(VF_ClipLevelExtentVoxels(Level));
|
||||
TArray<int32, TInlineAllocator<2>> XCandidates;
|
||||
TArray<int32, TInlineAllocator<2>> YCandidates;
|
||||
TArray<int32, TInlineAllocator<2>> ZCandidates;
|
||||
@@ -7182,7 +7196,7 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
{
|
||||
const FIntVector Min = SteadyLevelMins.FindChecked(Level);
|
||||
const FIntVector Max = SteadyLevelMaxs.FindChecked(Level);
|
||||
const int32 ExtentVoxels = CHUNK_SIZE << Level;
|
||||
const int32 ExtentVoxels = VF_ClipLevelExtentVoxels(Level);
|
||||
const double TileExtentMetres = static_cast<double>(ExtentVoxels) * VOXEL_SIZE / 100.0;
|
||||
const double SpanX = static_cast<double>(Max.X - Min.X + 1) * TileExtentMetres;
|
||||
const double SpanY = static_cast<double>(Max.Y - Min.Y + 1) * TileExtentMetres;
|
||||
@@ -7554,6 +7568,14 @@ bool AVoxelWorld::GetVoxelForgeCoreWorldAssembly(
|
||||
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutContentHash.IsEmpty();
|
||||
}
|
||||
|
||||
bool AVoxelWorld::GetVoxelForgeCoreResolvedLayout(
|
||||
FString& OutCanonicalJson, FString& OutStableIdentity) const
|
||||
{
|
||||
OutCanonicalJson = VoxelForgeCoreResolvedLayoutJson;
|
||||
OutStableIdentity = VoxelForgeCoreResolvedLayoutIdentity;
|
||||
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutStableIdentity.IsEmpty();
|
||||
}
|
||||
|
||||
bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const
|
||||
{
|
||||
OutDensity = 0.0f;
|
||||
@@ -7572,6 +7594,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
OutFailureReason.Reset();
|
||||
VoxelForgeCoreWorldAssemblyJson.Reset();
|
||||
VoxelForgeCoreWorldAssemblyHash.Reset();
|
||||
VoxelForgeCoreResolvedLayoutJson.Reset();
|
||||
VoxelForgeCoreResolvedLayoutIdentity.Reset();
|
||||
if (!bUseVoxelForgeCore) return true;
|
||||
if (!Settings || !StrateManager)
|
||||
{
|
||||
@@ -7604,6 +7628,17 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason);
|
||||
return false;
|
||||
}
|
||||
if (const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout = Snapshot->resolvedLayout())
|
||||
{
|
||||
const std::string CanonicalLayout = ResolvedLayout->ToJson();
|
||||
if (CanonicalLayout.empty())
|
||||
{
|
||||
OutFailureReason = TEXT("the resolved Core layout could not be serialized canonically.");
|
||||
return false;
|
||||
}
|
||||
VoxelForgeCoreResolvedLayoutJson = UTF8_TO_TCHAR(CanonicalLayout.c_str());
|
||||
VoxelForgeCoreResolvedLayoutIdentity = UTF8_TO_TCHAR(ResolvedLayout->stableIdentity.c_str());
|
||||
}
|
||||
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
|
||||
{
|
||||
const std::string CanonicalAssembly = Assembly->ToJson();
|
||||
@@ -7663,10 +7698,20 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
};
|
||||
for (const VoxelForgeCore::TileKey& Key : InitialDesired)
|
||||
{
|
||||
const int32 Extent = 32 << Key.lod;
|
||||
const int32 OriginX = Key.coordinate.x * Extent;
|
||||
const int32 OriginY = Key.coordinate.y * Extent;
|
||||
const int32 OriginZ = Key.coordinate.z * Extent;
|
||||
const int32 Extent = VF_ClipLevelExtentVoxels(Key.lod);
|
||||
const int64 OriginXWide = static_cast<int64>(Key.coordinate.x) * Extent;
|
||||
const int64 OriginYWide = static_cast<int64>(Key.coordinate.y) * Extent;
|
||||
const int64 OriginZWide = static_cast<int64>(Key.coordinate.z) * Extent;
|
||||
if (OriginXWide < MIN_int32 || OriginYWide < MIN_int32 || OriginZWide < MIN_int32
|
||||
|| OriginXWide + Extent > MAX_int32 || OriginYWide + Extent > MAX_int32
|
||||
|| OriginZWide + Extent > MAX_int32)
|
||||
{
|
||||
OutFailureReason = TEXT("Core initial clipmap exceeds the supported voxel-coordinate range.");
|
||||
return false;
|
||||
}
|
||||
const int32 OriginX = static_cast<int32>(OriginXWide);
|
||||
const int32 OriginY = static_cast<int32>(OriginYWide);
|
||||
const int32 OriginZ = static_cast<int32>(OriginZWide);
|
||||
IncludeExtent(ClipMin.x, ClipMax.x, OriginX, Extent);
|
||||
IncludeExtent(ClipMin.y, ClipMax.y, OriginY, Extent);
|
||||
IncludeExtent(ClipMin.z, ClipMax.z, OriginZ, Extent);
|
||||
@@ -7770,6 +7815,8 @@ void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString&
|
||||
}
|
||||
VoxelForgeCoreWorldAssemblyJson.Reset();
|
||||
VoxelForgeCoreWorldAssemblyHash.Reset();
|
||||
VoxelForgeCoreResolvedLayoutJson.Reset();
|
||||
VoxelForgeCoreResolvedLayoutIdentity.Reset();
|
||||
bUseVoxelForgeCore = false;
|
||||
// The Core adapter sets this before validation; legacy generation must not inherit it.
|
||||
// L'adaptateur Core l'active avant validation ; le générateur historique ne doit pas en hériter.
|
||||
@@ -9914,6 +9961,9 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
Sample.QueueWaitSeconds = WorkerQueueSeconds + ResultQueueSeconds;
|
||||
Sample.WorkerTaskSeconds = WorkerTaskSeconds;
|
||||
Sample.GenerationSeconds = GenerationSeconds;
|
||||
Sample.CoreGenerateSeconds = Result.CoreGenerateSeconds;
|
||||
Sample.MeshCopySeconds = Result.MeshCopySeconds;
|
||||
Sample.StreamBuildSeconds = Result.StreamBuildSeconds;
|
||||
Sample.ClassifySeconds = Result.ClassifySeconds;
|
||||
Sample.MeshSeconds = Result.MeshSeconds;
|
||||
Sample.StreamSeconds = Result.StreamSeconds;
|
||||
@@ -10074,8 +10124,11 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
||||
{
|
||||
BandChunkLo = MeshBandChunkLo;
|
||||
BandChunkHi = MeshBandChunkHi;
|
||||
BandVoxLo = MeshBandChunkLo * CHUNK_SIZE;
|
||||
BandVoxHi = (MeshBandChunkHi + 1) * CHUNK_SIZE - 1;
|
||||
BandVoxLo = static_cast<int32>(FMath::Clamp<int64>(
|
||||
static_cast<int64>(MeshBandChunkLo) * CHUNK_SIZE, MIN_int32, MAX_int32));
|
||||
BandVoxHi = static_cast<int32>(FMath::Clamp<int64>(
|
||||
(static_cast<int64>(MeshBandChunkHi) + 1) * CHUNK_SIZE - 1,
|
||||
MIN_int32, MAX_int32));
|
||||
}
|
||||
|
||||
FChunkResult Result;
|
||||
@@ -10238,7 +10291,7 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
ReadyToHide.Reserve(TileComponents.Num());
|
||||
auto GetAncestorAtLevel = [](const FVoxelTileKey& Tile, int32 AncestorLevel)
|
||||
{
|
||||
const int32 Divisor = 1 << (AncestorLevel - Tile.Level);
|
||||
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
|
||||
return FVoxelTileKey(VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord, Divisor), AncestorLevel);
|
||||
};
|
||||
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& Pair : TileComponents)
|
||||
@@ -10361,7 +10414,10 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
// Integer floor-division (correct for negatives), scalar + vector.
|
||||
static FORCEINLINE int32 VF_FloorDiv(int32 V, int32 D)
|
||||
{
|
||||
return V >= 0 ? (V / D) : -(((-V) + D - 1) / D);
|
||||
if (D <= 0) { return 0; }
|
||||
const int32 Quotient = V / D;
|
||||
const int32 Remainder = V % D;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
}
|
||||
static FORCEINLINE FIntVector VF_FloorDiv(const FIntVector& V, int32 D)
|
||||
{
|
||||
@@ -11441,13 +11497,13 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
|
||||
{
|
||||
VF_OuterShell(Settings, R, MaxLevelBase, MaxLevel, ROuter);
|
||||
}
|
||||
const int32 PowMax = 1 << MaxLevel;
|
||||
const int32 PowMax = VF_ClipLevelScale(MaxLevel);
|
||||
const FIntVector CMax = VF_FloorDiv(Center, PowMax);
|
||||
|
||||
FVector CV;
|
||||
if (bUseVoxelForgeCore)
|
||||
{
|
||||
const double TileExtentVoxels = static_cast<double>(CHUNK_SIZE << Tile.Level);
|
||||
const double TileExtentVoxels = static_cast<double>(VF_ClipLevelExtentVoxels(Tile.Level));
|
||||
CV = FVector(
|
||||
(static_cast<double>(Tile.Coord.X) + 0.5) * TileExtentVoxels,
|
||||
(static_cast<double>(Tile.Coord.Y) + 0.5) * TileExtentVoxels,
|
||||
@@ -11463,9 +11519,9 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
|
||||
VF_FloorDiv(FMath::FloorToInt(CV.Y), SizeMax),
|
||||
VF_FloorDiv(FMath::FloorToInt(CV.Z), SizeMax));
|
||||
|
||||
return FMath::Abs(TMax.X - CMax.X) <= ROuter
|
||||
&& FMath::Abs(TMax.Y - CMax.Y) <= ROuter
|
||||
&& FMath::Abs(TMax.Z - CMax.Z) <= ROuter;
|
||||
return FMath::Abs(static_cast<int64>(TMax.X) - CMax.X) <= ROuter
|
||||
&& FMath::Abs(static_cast<int64>(TMax.Y) - CMax.Y) <= ROuter
|
||||
&& FMath::Abs(static_cast<int64>(TMax.Z) - CMax.Z) <= ROuter;
|
||||
}
|
||||
|
||||
int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
||||
@@ -11818,11 +11874,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
}
|
||||
}
|
||||
const int32 MaxTasks = GetMaxConcurrentStreamingTasks();
|
||||
// Keep at most one worker-set of ready work so an admission pass can feed idle slots
|
||||
// without letting far work consume the near reserve. / Garder une file d'un ensemble
|
||||
// de workers pour nourrir les slots sans consommer la reserve proche.
|
||||
// Keep several bounded worker-sets ready so frame-paced admission can feed the
|
||||
// background lane continuously without moving mesh application onto the game thread.
|
||||
const int32 ReadyQueueDepth = FMath::Clamp(
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4));
|
||||
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
|
||||
const int32 AuthoredNearTaskReserve = GetNearTaskReserve(MaxTasks);
|
||||
const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition));
|
||||
@@ -12058,7 +12113,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
LevelSummaries.Num(), Leavers.Num(), CollisionOnlyTiles.Num()));
|
||||
for (const FDesiredLevelSummary& Summary : LevelSummaries)
|
||||
{
|
||||
const int32 ExtentVoxels = CHUNK_SIZE << Summary.Level;
|
||||
const int32 ExtentVoxels = VF_ClipLevelExtentVoxels(Summary.Level);
|
||||
const double TileExtentMetres = static_cast<double>(ExtentVoxels) * VOXEL_SIZE / 100.0;
|
||||
const double SpanX = static_cast<double>(Summary.Max.X - Summary.Min.X + 1) * TileExtentMetres;
|
||||
const double SpanY = static_cast<double>(Summary.Max.Y - Summary.Min.Y + 1) * TileExtentMetres;
|
||||
@@ -12109,8 +12164,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
for (const FVoxelTileKey& T : LoadedTiles)
|
||||
{
|
||||
if (T.Level < CutMin) continue;
|
||||
const int32 CLo = T.Coord.Z << T.Level;
|
||||
const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1;
|
||||
const int64 LevelScale = VF_ClipLevelScale(T.Level);
|
||||
const int64 CLo = static_cast<int64>(T.Coord.Z) * LevelScale;
|
||||
const int64 CHi = (static_cast<int64>(T.Coord.Z) + 1) * LevelScale - 1;
|
||||
const bool bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi;
|
||||
const bool bInNew = CLo >= NewLo && CHi <= NewHi;
|
||||
const bool bOutOld = CHi < MeshBandChunkLo || CLo > MeshBandChunkHi;
|
||||
@@ -12330,7 +12386,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
}
|
||||
Heading = Heading.GetSafeNormal();
|
||||
const FVector PlayerLocalCm = WorldToLocalCm(CenterPosition);
|
||||
const double TileSizeCm = static_cast<double>(CHUNK_SIZE << Level)
|
||||
const double TileSizeCm = static_cast<double>(VF_ClipLevelExtentVoxels(Level))
|
||||
* GetActiveVoxelSizeCm();
|
||||
LevelTiles.Sort([&](const FVoxelTileKey& A, const FVoxelTileKey& B)
|
||||
{
|
||||
@@ -12731,7 +12787,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
|
||||
const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); // Core and worker lane clamped. / Core et voie worker plafonnes.
|
||||
const int32 ReadyQueueDepth = FMath::Clamp(
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4));
|
||||
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
|
||||
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
|
||||
const int32 NearReserve = EffectiveNearTaskReserve >= 0
|
||||
@@ -12788,7 +12844,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
const int32 FullRes = GVoxelForgeTestFullResClipLevels >= 0
|
||||
? FMath::Clamp(GVoxelForgeTestFullResClipLevels, 1, 9)
|
||||
: (Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2);
|
||||
const int32 Extent = CHUNK_SIZE << Tile.Level;
|
||||
const int32 Extent = VF_ClipLevelExtentVoxels(Tile.Level);
|
||||
|
||||
const int32 Cells = (Tile.Level < FullRes)
|
||||
? CHUNK_SIZE
|
||||
@@ -12853,7 +12909,9 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
// et mélanges de matériaux qu'avant la coupe) — la tuile ne peut rendre que des artefacts
|
||||
// ⇒ on n'émet RIEN. Résultat vide via ProcessQueue (bookkeeping normal : PendingTiles,
|
||||
// LoadedTiles, epoch) ; jamais figé — le changement de bande re-queue via BandRemeshQueue.
|
||||
if (Step > (BandChunkHi - BandChunkLo + 1) * CHUNK_SIZE)
|
||||
const int64 BandHeightVoxels =
|
||||
(static_cast<int64>(BandChunkHi) - BandChunkLo + 1) * CHUNK_SIZE;
|
||||
if (static_cast<int64>(Step) > BandHeightVoxels)
|
||||
{
|
||||
FChunkResult Empty;
|
||||
Empty.Tile = Tile;
|
||||
@@ -13072,7 +13130,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
|
||||
const int32 MaxTasks = GetMaxConcurrentStreamingTasks();
|
||||
const int32 ReadyQueueDepth = FMath::Clamp(
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4));
|
||||
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
|
||||
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
|
||||
const int32 NearReserve = EffectiveNearTaskReserve >= 0
|
||||
@@ -13180,6 +13238,11 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
Result.WorkPriority = WorkPriority;
|
||||
Result.GenerationStartCycles = FPlatformTime::Cycles64();
|
||||
Result.GenerationOrdinal = NextStreamingGenerationOrdinal.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
#if PLATFORM_LINUX
|
||||
const bool bMeasureWorkerPhases = VoxelForgeStartupTrace::IsActive();
|
||||
#else
|
||||
constexpr bool bMeasureWorkerPhases = false;
|
||||
#endif
|
||||
|
||||
auto EnqueueObsolete = [&]()
|
||||
{
|
||||
@@ -13235,7 +13298,14 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
FReadScopeLock Lock(State->WorldLock);
|
||||
if (State->World)
|
||||
{
|
||||
const uint64 CoreGenerateStartCycles = bMeasureWorkerPhases
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
CoreMesh = State->World->generateTile(CoreKey, Options, CoreError);
|
||||
if (CoreGenerateStartCycles != 0)
|
||||
{
|
||||
Result.CoreGenerateSeconds = FPlatformTime::ToSeconds64(
|
||||
FPlatformTime::Cycles64() - CoreGenerateStartCycles);
|
||||
}
|
||||
}
|
||||
}
|
||||
Result.GenerationEndCycles = FPlatformTime::Cycles64();
|
||||
@@ -13277,6 +13347,8 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
|
||||
if (!Result.bEmpty)
|
||||
{
|
||||
const uint64 MeshCopyStartCycles = bMeasureWorkerPhases
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
FVoxelMeshData MeshData;
|
||||
MeshData.Vertices.Reserve(Result.NumVertices);
|
||||
MeshData.Normals.Reserve(Result.NumVertices);
|
||||
@@ -13297,8 +13369,20 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
MeshData.NumSurfaceVertices = static_cast<int32>(CoreMesh->groups.surfaceVertexCount);
|
||||
MeshData.NumCeilingTriangles = Result.NumCeilingTriangles;
|
||||
MeshData.NumSkirtTriangles = Result.NumSkirtTriangles;
|
||||
if (MeshCopyStartCycles != 0)
|
||||
{
|
||||
Result.MeshCopySeconds = FPlatformTime::ToSeconds64(
|
||||
FPlatformTime::Cycles64() - MeshCopyStartCycles);
|
||||
}
|
||||
const uint64 StreamBuildStartCycles = bMeasureWorkerPhases
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
|
||||
BuildTileStreamSet(*Result.Streams, MeshData);
|
||||
if (StreamBuildStartCycles != 0)
|
||||
{
|
||||
Result.StreamBuildSeconds = FPlatformTime::ToSeconds64(
|
||||
FPlatformTime::Cycles64() - StreamBuildStartCycles);
|
||||
}
|
||||
}
|
||||
Result.GenerationEndCycles = FPlatformTime::Cycles64();
|
||||
if (Cancellation->bObsolete.load(std::memory_order_acquire)
|
||||
@@ -13988,7 +14072,8 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
// Convertir les bornes physiques de tuile vers les strates owner, y compris les rapports fractionnaires.
|
||||
const double OwnerChunksPerCoreTile = static_cast<double>(GetActiveVoxelSizeCm())
|
||||
/ VOXEL_SIZE;
|
||||
const double TileExtentCoreVoxels = static_cast<double>(CHUNK_SIZE << Tile.Level);
|
||||
const double TileExtentCoreVoxels =
|
||||
static_cast<double>(VF_ClipLevelExtentVoxels(Tile.Level));
|
||||
const int32 MinOwnerChunkX = FMath::FloorToInt(
|
||||
Tile.Coord.X * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE);
|
||||
const int32 MinOwnerChunkY = FMath::FloorToInt(
|
||||
@@ -15084,6 +15169,7 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
}
|
||||
|
||||
FString AssemblyHash = VoxelForgeCoreWorldAssemblyHash;
|
||||
FString ResolvedLayoutIdentity = VoxelForgeCoreResolvedLayoutIdentity;
|
||||
if (AssemblyHash.IsEmpty())
|
||||
{
|
||||
FString AssemblyJson;
|
||||
@@ -15120,12 +15206,17 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
AssemblyHash = FString::Printf(TEXT("%016llx"),
|
||||
static_cast<unsigned long long>(Assembly->contentHash));
|
||||
}
|
||||
if (const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout = Snapshot->resolvedLayout())
|
||||
{
|
||||
ResolvedLayoutIdentity = UTF8_TO_TCHAR(ResolvedLayout->stableIdentity.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
|
||||
const uint32 CoreVoxelSizeMicrons = static_cast<uint32>(
|
||||
FMath::RoundToInt(GetActiveVoxelSizeCm() * 10000.0f));
|
||||
if (AssemblyHash.IsEmpty() && !bHybridMesher && CoreVoxelSizeMicrons == 250000)
|
||||
if (AssemblyHash.IsEmpty() && ResolvedLayoutIdentity.IsEmpty()
|
||||
&& !bHybridMesher && CoreVoxelSizeMicrons == 250000)
|
||||
{
|
||||
return SeasonHash;
|
||||
}
|
||||
@@ -15150,6 +15241,11 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
Payload += TEXT("|world_assembly=");
|
||||
Payload += AssemblyHash;
|
||||
}
|
||||
if (!ResolvedLayoutIdentity.IsEmpty())
|
||||
{
|
||||
Payload += TEXT("|resolved_layout=");
|
||||
Payload += ResolvedLayoutIdentity;
|
||||
}
|
||||
if (CoreVoxelSizeMicrons != 250000)
|
||||
{
|
||||
Payload += FString::Printf(TEXT("|core_voxel_size_microns=%u"), CoreVoxelSizeMicrons);
|
||||
@@ -15180,7 +15276,7 @@ void AVoxelWorld::RemeshDirtyChunks(const TArray<FIntVector>& DirtyCoords, const
|
||||
// BackgroundHigh; the old component remains visible until its post-edit replacement is applied.
|
||||
auto TileCoversDirtyCoord = [&DirtyCoords](const FVoxelTileKey& Tile) -> bool
|
||||
{
|
||||
const int32 Span = 1 << Tile.Level;
|
||||
const int32 Span = VF_ClipLevelScale(Tile.Level);
|
||||
const FIntVector MinChunk = Tile.Coord * Span;
|
||||
const FIntVector MaxChunk = MinChunk + FIntVector(Span, Span, Span);
|
||||
for (const FIntVector& Coord : DirtyCoords)
|
||||
|
||||
@@ -261,8 +261,12 @@ namespace VoxelHash
|
||||
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
|
||||
{
|
||||
uint32 H = Seed;
|
||||
H ^= Mix((uint32)(CellX + 0x7FFFFFFF));
|
||||
H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u);
|
||||
// Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for
|
||||
// positive cells and makes the world hash compiler-dependent.
|
||||
const uint32 BiasedX = static_cast<uint32>(CellX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedY = static_cast<uint32>(CellY) + 0x7FFFFFFFu;
|
||||
H ^= Mix(BiasedX);
|
||||
H ^= Mix(BiasedY * 2654435761u);
|
||||
return Mix(H);
|
||||
}
|
||||
|
||||
@@ -274,10 +278,14 @@ namespace VoxelHash
|
||||
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
|
||||
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
|
||||
uint32 H = Seed ^ 0xDEADBEEF;
|
||||
H ^= Mix((uint32)(MinX + 0x7FFFFFFF));
|
||||
H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u);
|
||||
H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u);
|
||||
H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u);
|
||||
const uint32 BiasedMinX = static_cast<uint32>(MinX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedMinY = static_cast<uint32>(MinY) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedMaxX = static_cast<uint32>(MaxX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedMaxY = static_cast<uint32>(MaxY) + 0x7FFFFFFFu;
|
||||
H ^= Mix(BiasedMinX);
|
||||
H ^= Mix(BiasedMinY * 2654435761u);
|
||||
H ^= Mix(BiasedMaxX * 374761393u);
|
||||
H ^= Mix(BiasedMaxY * 668265263u);
|
||||
return Mix(H);
|
||||
}
|
||||
|
||||
@@ -477,10 +485,13 @@ namespace VoxelNoise
|
||||
|
||||
// Hash the neighbor cell to get a feature point position [0,1)
|
||||
// Using three different hash mixes for X, Y, Z offsets
|
||||
const uint32 BiasedX = static_cast<uint32>(NX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedY = static_cast<uint32>(NY) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedZ = static_cast<uint32>(NZ) + 0x7FFFFFFFu;
|
||||
uint32 H = VoxelHash::Mix(
|
||||
(uint32)(NX + 0x7FFFFFFF)
|
||||
^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u)
|
||||
^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u)
|
||||
BiasedX
|
||||
^ VoxelHash::Mix(BiasedY * 2654435761u)
|
||||
^ VoxelHash::Mix(BiasedZ * 374761393u)
|
||||
);
|
||||
|
||||
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
|
||||
|
||||
@@ -340,6 +340,14 @@ private:
|
||||
float SuppressRadiusWorld = 0.0f;
|
||||
};
|
||||
|
||||
// Game-thread lookup copied from the immutable resolved Core layout.
|
||||
// Recherche game-thread copiee depuis la disposition Core resolue et immuable.
|
||||
struct FResolvedCoreGeometryAnchor
|
||||
{
|
||||
int32 StrateIndex = INDEX_NONE;
|
||||
FVector LocalPositionCm = FVector::ZeroVector;
|
||||
};
|
||||
|
||||
/** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle
|
||||
* (cheap, O(1)/column); other archetypes ray-march the density column. No UObject access except
|
||||
* Generator (thread-safe). Determinism-critical. Resolves the dominant biome PER COLUMN
|
||||
@@ -436,6 +444,7 @@ private:
|
||||
// (kept until the cell leaves range so the surface-find isn't repeated). Strate-bounded.
|
||||
// Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint.
|
||||
TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
|
||||
TMap<FString, FResolvedCoreGeometryAnchor> CoreGeometryAnchors;
|
||||
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
|
||||
|
||||
// Set in BeginDestroy; worker tasks check it before touching us.
|
||||
|
||||
@@ -21,6 +21,8 @@ class UVoxelSettings : public UPrimaryDataAsset
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
virtual void PostLoad() override;
|
||||
|
||||
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
|
||||
int32 GetEffectiveWorldSeed() const;
|
||||
@@ -411,14 +413,16 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
|
||||
int32 TotalStrates = 10;
|
||||
|
||||
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock.
|
||||
// 0 = strates touch (just the seals between them). Higher = a thick rock layer
|
||||
// between each biome that the player must dig through at (0,0), and that the
|
||||
// auto-carved passages tunnel across.
|
||||
// 0 → adjacent layers (default) · 1-2 → a real bedrock band · 4+ → deep separation
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "0", ClampMax = "16"))
|
||||
// Legacy count retained so pre-metre assets still deserialize during the upgrade.
|
||||
// Ancien compte conserve pour lire les assets precedant les metres.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (DeprecatedProperty, DeprecationMessage = "Use InterStrateGapMeters."))
|
||||
int32 InterStrateGapChunks = 0;
|
||||
|
||||
// Physical separation between strata; the layout remains aligned to whole layout chunks.
|
||||
// Separation physique; la disposition reste alignee sur des chunks entiers.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (Units = "m", ClampMin = "0", ClampMax = "1024"))
|
||||
double InterStrateGapMeters = 0.0;
|
||||
|
||||
// Physical width of one vertical strate-layout chunk. This is independent of the
|
||||
// Core tile width (32^3 voxels) and of the source generator's voxel resolution.
|
||||
// 8 m preserves the owner's current SurfaceWorld extent at 25 cm Core voxels.
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace VoxelForgeStartupTrace
|
||||
double ResultQueueSeconds = 0.0;
|
||||
double WorkerTaskSeconds = 0.0;
|
||||
double GenerationSeconds = 0.0;
|
||||
double CoreGenerateSeconds = 0.0;
|
||||
double MeshCopySeconds = 0.0;
|
||||
double StreamBuildSeconds = 0.0;
|
||||
double ClassifySeconds = 0.0;
|
||||
double MeshSeconds = 0.0;
|
||||
double StreamSeconds = 0.0;
|
||||
|
||||
@@ -48,6 +48,7 @@ class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
virtual void PostLoad() override;
|
||||
|
||||
//=========================================================================
|
||||
@@ -66,14 +67,14 @@ public:
|
||||
// DIMENSIONS
|
||||
//=========================================================================
|
||||
|
||||
// How many chunks tall this strate is.
|
||||
// Each strate can have a different height!
|
||||
// Human-scale defaults use 16 chunks = 64 m. This gives a 20 m+ vertical budget for
|
||||
// cathedral rooms, tall shafts, and the surface sky cap without changing the lateral gate.
|
||||
// 1 chunk = 32 voxels x 12.5cm = 4 m; 16 chunks preserves the 64 m authored default.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "256"))
|
||||
// Legacy serialized chunk count kept for Blueprint/package compatibility during the unit migration.
|
||||
// Ancien nombre de chunks conserve pour la compatibilite Blueprint/package pendant la migration.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (DeprecatedProperty, DeprecationMessage = "Use StrateHeightMeters."))
|
||||
int32 StrateHeightInChunks = 16;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (Units = "m", ClampMin = "1", ClampMax = "2048"))
|
||||
double StrateHeightMeters = 128.0;
|
||||
|
||||
//=========================================================================
|
||||
// BOUNDARY TRANSITION
|
||||
//=========================================================================
|
||||
@@ -99,9 +100,13 @@ public:
|
||||
// 2 → moderate (good default)
|
||||
// 4 → wide, gentle blend across many chunks
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
||||
meta = (ClampMin = "1", ClampMax = "8", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
|
||||
meta = (DeprecatedProperty, DeprecationMessage = "Use TransitionWidthMeters."))
|
||||
int32 TransitionBlendChunks = 2;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
||||
meta = (Units = "m", ClampMin = "0", ClampMax = "512", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
|
||||
double TransitionWidthMeters = 16.0;
|
||||
|
||||
//=========================================================================
|
||||
// GENERATOR TYPE
|
||||
//=========================================================================
|
||||
@@ -115,6 +120,11 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
|
||||
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
|
||||
|
||||
// Optional native Core authoring; leaving it empty keeps the established generator path.
|
||||
// Edition Core native facultative; laissez vide pour garder le generateur existant.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Core Geometry")
|
||||
FVoxelForgeCoreGeometry CoreGeometry;
|
||||
|
||||
/**
|
||||
* Authoritative per-strate worm switch. Existing assets default to enabled. The manager
|
||||
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
|
||||
|
||||
@@ -200,6 +200,208 @@ enum class ECaveGeneratorType : uint8
|
||||
Underwater UMETA(DisplayName = "Underwater (flooded cavern system)"),
|
||||
};
|
||||
|
||||
// Optional native-Core recipe selected independently for one strate.
|
||||
// Recette Core native facultative, choisie independamment pour chaque strate.
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelForgeCoreGeneratorRecipe : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "Off (legacy recipe)"),
|
||||
NativeCaves UMETA(DisplayName = "Native caves"),
|
||||
NativeLattice UMETA(DisplayName = "Native lattice"),
|
||||
ProfiledHeightfield UMETA(DisplayName = "Profiled heightfield")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelForgeCoreHeightfieldProfile : uint8
|
||||
{
|
||||
PeriodicHills,
|
||||
MirroredRelief,
|
||||
FlatField
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelForgeCoreStampKind : uint8
|
||||
{
|
||||
CarveEllipsoid,
|
||||
CarveArch,
|
||||
FillPillar,
|
||||
FillDais,
|
||||
FillMonolith,
|
||||
FillPlatform
|
||||
};
|
||||
|
||||
// Stable authored volume stamp. All distances and positions are metres at the asset boundary.
|
||||
// Volume stable, defini par son auteur. Toutes les positions et distances sont en metres.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreStampDefinition
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
EVoxelForgeCoreStampKind Kind = EVoxelForgeCoreStampKind::CarveEllipsoid;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector AnchorPositionMeters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double PlacementRadiusMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
||||
FVector HalfExtentsMeters = FVector(2.0, 2.0, 2.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double SmoothWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityAmplitudeMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityWavelengthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double RoundingRadiusMeters = 0.5;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double MinimumSpacingMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (ClampMin = "0", ClampMax = "3"))
|
||||
uint8 YawQuarterTurns = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
bool bAttachToFloor = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
double FloorMeters = 0.0;
|
||||
};
|
||||
|
||||
// Authored cavern chamber, in local world metres.
|
||||
// Salle cavernicole authored, en metres locaux du monde.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreRoomDefinition
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector CenterMeters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
||||
FVector HalfExtentsMeters = FVector(8.0, 8.0, 4.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
double FloorMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double SmoothWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double UnionWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityAmplitudeMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityWavelengthMeters = 0.0;
|
||||
};
|
||||
|
||||
// Authored cubic tunnel between a stable start anchor and a stable room name.
|
||||
// Tunnel cubique authored entre une ancre stable et une salle nommee.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreTunnelDefinition
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StartAnchorStableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString EndRoomStableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint0Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint1Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint2Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint3Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
||||
double BoreRadiusMeters = 2.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "1"))
|
||||
double BoreHeightMeters = 4.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double FloorSupportDepthMeters = 0.5;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double SmoothWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double UnionWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityAmplitudeMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityWavelengthMeters = 0.0;
|
||||
|
||||
// Lattice tunnels require an explicit bounded polyline; cave tunnels use the cubic curve.
|
||||
// Les tunnels en treillis exigent une polyligne bornee; les tunnels caves utilisent la courbe cubique.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVector> PolylineMeters;
|
||||
};
|
||||
|
||||
// Optional Core authored recipe. Empty arrays and None preserve the prior asset output.
|
||||
// Recette Core authored facultative. None et les tableaux vides conservent la sortie historique.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreGeometry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
EVoxelForgeCoreGeneratorRecipe GeneratorRecipe = EVoxelForgeCoreGeneratorRecipe::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
EVoxelForgeCoreHeightfieldProfile HeightfieldProfile = EVoxelForgeCoreHeightfieldProfile::PeriodicHills;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
|
||||
double HeightfieldBaseMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
|
||||
FVector2D HeightfieldAmplitudeMeters = FVector2D(20.0, 20.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
|
||||
FVector2D HeightfieldWavelengthMeters = FVector2D(256.0, 256.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (ClampMin = "0.01", ClampMax = "4.0"))
|
||||
double MirrorLambda = 1.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
|
||||
double MinimumArenaClearanceMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVoxelForgeCoreStampDefinition> Stamps;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVoxelForgeCoreRoomDefinition> Rooms;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVoxelForgeCoreTunnelDefinition> Tunnels;
|
||||
};
|
||||
|
||||
/**
|
||||
* EVoxelStrateTransition — How two adjacent strates meet at their shared boundary.
|
||||
*
|
||||
@@ -2289,6 +2491,11 @@ struct VOXELFORGE_API FStrateLandmark
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
|
||||
ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice;
|
||||
|
||||
// Optional stable Core feature binding; an empty value keeps lattice/mouth placement unchanged.
|
||||
// Liaison facultative vers une ancre Core stable; vide, le placement existant reste identique.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Core Geometry")
|
||||
FString CoreGeometryStableName;
|
||||
|
||||
// ----- Hash lattice (AnchorMode == HashLattice — scattered placement; cheap at any radius) -----
|
||||
|
||||
// Average spacing between instances, IN CHUNKS = the lattice cell size (one candidate per cell, so cost
|
||||
|
||||
@@ -332,16 +332,33 @@ struct FVoxelTileKey
|
||||
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
|
||||
|
||||
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
|
||||
int32 StepVoxels() const { return 1 << Level; }
|
||||
int32 ExtentVoxels() const { return CHUNK_SIZE << Level; }
|
||||
int32 StepVoxels() const
|
||||
{
|
||||
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
|
||||
return static_cast<int32>(uint32{1} << SafeLevel);
|
||||
}
|
||||
int32 ExtentVoxels() const
|
||||
{
|
||||
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
|
||||
return static_cast<int32>(static_cast<uint32>(CHUNK_SIZE) * (uint32{1} << SafeLevel));
|
||||
}
|
||||
|
||||
// Min-corner of the tile in VOXEL coords (what the mesher takes).
|
||||
FIntVector OriginVoxels() const { return Coord * (CHUNK_SIZE << Level); }
|
||||
FIntVector OriginVoxels() const
|
||||
{
|
||||
const int64 Extent = ExtentVoxels();
|
||||
const auto ScaleAxis = [Extent](int32 Axis)
|
||||
{
|
||||
return static_cast<int32>(FMath::Clamp<int64>(
|
||||
static_cast<int64>(Axis) * Extent, MIN_int32, MAX_int32));
|
||||
};
|
||||
return FIntVector(ScaleAxis(Coord.X), ScaleAxis(Coord.Y), ScaleAxis(Coord.Z));
|
||||
}
|
||||
|
||||
// Tile centre in world cm (for distance sorting / LOD selection).
|
||||
FVector CenterCm() const
|
||||
{
|
||||
const double Ext = (double)(CHUNK_SIZE << Level) * (double)VOXEL_SIZE;
|
||||
const double Ext = static_cast<double>(ExtentVoxels()) * static_cast<double>(VOXEL_SIZE);
|
||||
return FVector((Coord.X + 0.5) * Ext, (Coord.Y + 0.5) * Ext, (Coord.Z + 0.5) * Ext);
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,10 @@ struct FChunkResult
|
||||
// per-tile profiling remains opt-in.
|
||||
uint64 GenerationStartCycles = 0;
|
||||
uint64 GenerationEndCycles = 0;
|
||||
// Optional phase timings recorded only while the bounded startup trace is active.
|
||||
double CoreGenerateSeconds = 0.0;
|
||||
double MeshCopySeconds = 0.0;
|
||||
double StreamBuildSeconds = 0.0;
|
||||
uint64 ApplyStartCycles = 0;
|
||||
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
|
||||
// Ordinals make admitted order and actual worker start order auditable across MPSC results.
|
||||
@@ -614,6 +618,7 @@ public:
|
||||
FVector GetVoxelForgeCoreDefaultCenterWorld() const;
|
||||
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
|
||||
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
|
||||
bool GetVoxelForgeCoreResolvedLayout(FString& OutCanonicalJson, FString& OutStableIdentity) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
|
||||
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
||||
@@ -778,6 +783,8 @@ private:
|
||||
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
|
||||
FString VoxelForgeCoreWorldAssemblyJson;
|
||||
FString VoxelForgeCoreWorldAssemblyHash;
|
||||
FString VoxelForgeCoreResolvedLayoutJson;
|
||||
FString VoxelForgeCoreResolvedLayoutIdentity;
|
||||
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||
bool bCoreRegionCompilePending = false;
|
||||
TArray<FVoxelTileKey> PendingCoreRegionTiles;
|
||||
|
||||
@@ -7,6 +7,13 @@ public class VoxelForge : ModuleRules
|
||||
{
|
||||
public VoxelForge(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
if (Target.Platform == UnrealTargetPlatform.Linux)
|
||||
{
|
||||
// Test translation units have independent anonymous-namespace helpers that collide in
|
||||
// Linux Unity batches. Keep Linux compilation one source per TU; Windows remains unchanged.
|
||||
bUseUnity = false;
|
||||
}
|
||||
|
||||
// PCH = Precompiled Headers - speeds up compilation
|
||||
// UseExplicitOrSharedPCHs is the modern recommended setting
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
@@ -39,6 +39,13 @@ public class VoxelForgeCore : ModuleRules
|
||||
|
||||
PublicDependencyModuleNames.Add("Core");
|
||||
CppStandard = CppStandardVersion.Cpp20;
|
||||
if (Target.Platform == UnrealTargetPlatform.Linux)
|
||||
{
|
||||
// The compatibility include directory contains Unreal-shaped placeholders. On Linux,
|
||||
// let generated module TUs pass those names through to UE while Core shims opt in.
|
||||
PrivateDefinitions.Add("VFCORE_COMPAT_FORWARD_HEADERS=1");
|
||||
MinCpuArchX64 = MinimumCpuArchitectureX64.AVX2;
|
||||
}
|
||||
// Canonical Standalone code predates UE's stricter warning defaults; keep this narrow to legal local shadowing.
|
||||
CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off;
|
||||
FPSemantics = FPSemanticsMode.Precise;
|
||||
|
||||
@@ -878,6 +878,8 @@ struct FExploreArguments
|
||||
bool bPassageFloorWritesOverride = false;
|
||||
int32 PassageFloorWrites = 1;
|
||||
bool bStandaloneWorldExport = false;
|
||||
bool bOwnerFourStratePlan = false;
|
||||
bool bSkipStandaloneCoreAdapterParity = false;
|
||||
bool bCoreWorldTest = false;
|
||||
bool bCoreWorldStreamTraceOnly = false;
|
||||
bool bRenderCameraOverride = false;
|
||||
@@ -922,6 +924,12 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
int32 StandaloneWorldExport = 0;
|
||||
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
||||
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
|
||||
int32 OwnerFourStratePlan = 0;
|
||||
FParse::Value(*Params, TEXT("ownerfourstrateplan="), OwnerFourStratePlan);
|
||||
OutArguments.bOwnerFourStratePlan = OwnerFourStratePlan != 0;
|
||||
int32 SkipStandaloneCoreAdapterParity = 0;
|
||||
FParse::Value(*Params, TEXT("skipstandalonecoreadapterparity="), SkipStandaloneCoreAdapterParity);
|
||||
OutArguments.bSkipStandaloneCoreAdapterParity = SkipStandaloneCoreAdapterParity != 0;
|
||||
int32 CoreWorldTest = 0;
|
||||
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
|
||||
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
|
||||
@@ -1266,12 +1274,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
return false;
|
||||
}
|
||||
|
||||
// The historical explorer contract needs two adjacent mouths around an interior slot. A
|
||||
// parity export can instead target the game's authored single-strate layout, which has no
|
||||
// such mouths and is valid for export-only mode. Build() still validates the slot against
|
||||
// the loaded settings layout.
|
||||
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig ? 0 : 1;
|
||||
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig
|
||||
// Passage walks need both mouths; a render/export-only generation can inspect an end slot.
|
||||
// Parity exports also accept the game's authored single-strate layout.
|
||||
const bool bRequiresInteriorSlot = OutArguments.bWalk;
|
||||
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot ? 0 : 1;
|
||||
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot
|
||||
? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2;
|
||||
if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot)
|
||||
{
|
||||
@@ -1284,8 +1291,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
else
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("slot must be an interior slot in [1,%d] so arrival and departure mouths exist."),
|
||||
MaxSyntheticStrates - 2);
|
||||
TEXT("slot must be in [0,%d]; walk mode requires an interior slot in [1,%d] for both passage mouths."),
|
||||
MaxSyntheticStrates - 1, MaxSyntheticStrates - 2);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1498,6 +1505,17 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.bOwnerFourStratePlan
|
||||
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
|
||||
{
|
||||
OutError = TEXT("ownerfourstrateplan requires settingsref and must omit strateref.");
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.bSkipStandaloneCoreAdapterParity && !OutArguments.bStandaloneWorldExport)
|
||||
{
|
||||
OutError = TEXT("skipstandalonecoreadapterparity requires standaloneworldexport.");
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.bCoreWorldTest
|
||||
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
|
||||
{
|
||||
@@ -1702,6 +1720,66 @@ struct FExploreMeshBvhNode
|
||||
}
|
||||
};
|
||||
|
||||
bool ApplyOwnerFourStratePlan(UVoxelSettings* Settings,
|
||||
TArray<TStrongObjectPtr<UVoxelStrateDefinition>>& OutDefinitions,
|
||||
bool bDuplicateDefinitions, FString& OutError)
|
||||
{
|
||||
if (Settings == nullptr)
|
||||
{
|
||||
OutError = TEXT("The transient owner plan has no settings object.");
|
||||
return false;
|
||||
}
|
||||
if (!Settings->Season.IsNull())
|
||||
{
|
||||
OutError = TEXT("The transient four-strate owner plan cannot replace an assigned season manifest.");
|
||||
return false;
|
||||
}
|
||||
|
||||
static const TCHAR* AssetPaths[] = {
|
||||
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
|
||||
};
|
||||
Settings->TotalStrates = UE_ARRAY_COUNT(AssetPaths);
|
||||
Settings->StratePool.Reset();
|
||||
Settings->FixedStrates.Reset();
|
||||
OutDefinitions.Reset();
|
||||
OutDefinitions.Reserve(UE_ARRAY_COUNT(AssetPaths));
|
||||
|
||||
for (int32 Index = 0; Index < UE_ARRAY_COUNT(AssetPaths); ++Index)
|
||||
{
|
||||
UVoxelStrateDefinition* AuthoredDefinition = LoadObject<UVoxelStrateDefinition>(
|
||||
nullptr, AssetPaths[Index]);
|
||||
if (AuthoredDefinition == nullptr)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Could not load staged owner strate asset '%s'."), AssetPaths[Index]);
|
||||
return false;
|
||||
}
|
||||
UVoxelStrateDefinition* Definition = AuthoredDefinition;
|
||||
if (bDuplicateDefinitions)
|
||||
{
|
||||
Definition = DuplicateObject<UVoxelStrateDefinition>(
|
||||
AuthoredDefinition, GetTransientPackage(),
|
||||
FName(*FString::Printf(TEXT("VoxelForgeOwnerPlan_Strate_%d"), Index)));
|
||||
if (Definition == nullptr)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Could not make a transient copy of owner strate asset '%s'."), AssetPaths[Index]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Settings->FixedStrates.Add(Index,
|
||||
TSoftObjectPtr<UVoxelStrateDefinition>(Definition));
|
||||
OutDefinitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeOwnerPlan] transient fixed slots=4 assets=DA_Strate1,DA_Strate2,DA_Strate3,DA_Strate4 settings_asset_untouched=1"));
|
||||
return true;
|
||||
}
|
||||
|
||||
struct FExploreWorld
|
||||
{
|
||||
TStrongObjectPtr<UVoxelSettings> Settings;
|
||||
@@ -1834,7 +1912,7 @@ struct FExploreWorld
|
||||
Settings->InterStrateGapChunks, Settings->SkirtCells);
|
||||
}
|
||||
|
||||
const int32 NumStrates = bUseAuthoredSettings
|
||||
const int32 NumStrates = Arguments.bOwnerFourStratePlan ? 4 : bUseAuthoredSettings
|
||||
? (Arguments.bGameRuntimeConfig
|
||||
? FMath::Max(Settings->TotalStrates, 1)
|
||||
: FMath::Max(Settings->TotalStrates, Arguments.Slot + 2))
|
||||
@@ -1864,64 +1942,73 @@ struct FExploreWorld
|
||||
// loaded package remains untouched.
|
||||
Settings->TotalStrates = NumStrates;
|
||||
|
||||
if (bUseAuthoredStrate)
|
||||
if (Arguments.bOwnerFourStratePlan)
|
||||
{
|
||||
// The owner’s snapshot states that every slot uses DA_Strate3. Clone it per slot so
|
||||
// commandlet overrides never mutate a loaded package or leak between slots.
|
||||
Settings->StratePool.Reset();
|
||||
Settings->FixedStrates.Reset();
|
||||
}
|
||||
Definitions.Reserve(NumStrates);
|
||||
for (int32 Index = 0; Index < NumStrates; ++Index)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = bUseAuthoredStrate
|
||||
? DuplicateObject<UVoxelStrateDefinition>(
|
||||
AuthoredDefinition, GetTransientPackage())
|
||||
: NewObject<UVoxelStrateDefinition>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
if (Definition == nullptr)
|
||||
if (!ApplyOwnerFourStratePlan(Settings.Get(), Definitions, true, OutError))
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Could not create transient strate definition for slot %d."), Index);
|
||||
return false;
|
||||
}
|
||||
if (!bUseAuthoredStrate)
|
||||
}
|
||||
else
|
||||
{
|
||||
if (bUseAuthoredStrate)
|
||||
{
|
||||
Definition->StrateName = FText::FromString(FString::Printf(
|
||||
TEXT("VoxelForgeExplore_%s_slot%d"),
|
||||
ArchetypeName(Index == Arguments.Slot
|
||||
// Keep the historical single-asset interior probe isolated from loaded packages.
|
||||
Settings->StratePool.Reset();
|
||||
Settings->FixedStrates.Reset();
|
||||
}
|
||||
Definitions.Reserve(NumStrates);
|
||||
for (int32 Index = 0; Index < NumStrates; ++Index)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = bUseAuthoredStrate
|
||||
? DuplicateObject<UVoxelStrateDefinition>(
|
||||
AuthoredDefinition, GetTransientPackage())
|
||||
: NewObject<UVoxelStrateDefinition>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
if (Definition == nullptr)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Could not create transient strate definition for slot %d."), Index);
|
||||
return false;
|
||||
}
|
||||
if (!bUseAuthoredStrate)
|
||||
{
|
||||
Definition->StrateName = FText::FromString(FString::Printf(
|
||||
TEXT("VoxelForgeExplore_%s_slot%d"),
|
||||
ArchetypeName(Index == Arguments.Slot
|
||||
? Arguments.Archetype
|
||||
: ECaveGeneratorType::TunnelNetwork),
|
||||
Index));
|
||||
Definition->StrateDescription = FText::FromString(
|
||||
TEXT("Transient, deterministic commandlet definition."));
|
||||
// The commandlet's acceptance sweep uses the owner-facing base spacing:
|
||||
// 8 chunks * 32 voxels * 0.125 m = 32 m per strate. Production assets may be taller;
|
||||
// this keeps the measured tunnel arithmetic tied to the stated ~32 m descent.
|
||||
Definition->StrateHeightInChunks = 8;
|
||||
Definition->TransitionType = EVoxelStrateTransition::Hard;
|
||||
Definition->GeneratorType = Index == Arguments.Slot
|
||||
? Arguments.Archetype
|
||||
: ECaveGeneratorType::TunnelNetwork),
|
||||
Index));
|
||||
Definition->StrateDescription = FText::FromString(
|
||||
TEXT("Transient, deterministic commandlet definition."));
|
||||
// The commandlet's acceptance sweep uses the owner-facing base spacing:
|
||||
// 8 chunks * 32 voxels * 0.125 m = 32 m per strate. Production assets may be taller;
|
||||
// this keeps the measured tunnel arithmetic tied to the stated ~32 m descent.
|
||||
Definition->StrateHeightInChunks = 8;
|
||||
Definition->TransitionType = EVoxelStrateTransition::Hard;
|
||||
Definition->GeneratorType = Index == Arguments.Slot
|
||||
? Arguments.Archetype
|
||||
: ECaveGeneratorType::TunnelNetwork;
|
||||
if (Arguments.bWormsEnabledOverride)
|
||||
{
|
||||
Definition->bEnableWorms = Arguments.bWormsEnabled;
|
||||
: ECaveGeneratorType::TunnelNetwork;
|
||||
if (Arguments.bWormsEnabledOverride)
|
||||
{
|
||||
Definition->bEnableWorms = Arguments.bWormsEnabled;
|
||||
}
|
||||
if (Arguments.bWormStrengthOverride
|
||||
&& (Definition->GeneratorType == ECaveGeneratorType::TunnelNetwork
|
||||
|| Definition->GeneratorType == ECaveGeneratorType::Underwater))
|
||||
{
|
||||
Definition->GenerationParams.WormStrength = Arguments.WormStrength;
|
||||
}
|
||||
}
|
||||
if (Arguments.bWormStrengthOverride
|
||||
&& (Definition->GeneratorType == ECaveGeneratorType::TunnelNetwork
|
||||
|| Definition->GeneratorType == ECaveGeneratorType::Underwater))
|
||||
{
|
||||
Definition->GenerationParams.WormStrength = Arguments.WormStrength;
|
||||
}
|
||||
}
|
||||
|
||||
const TSoftObjectPtr<UVoxelStrateDefinition> SoftDefinition(Definition);
|
||||
Settings->FixedStrates.Add(Index, SoftDefinition);
|
||||
if (!bUseAuthoredStrate)
|
||||
{
|
||||
Settings->StratePool.Add(SoftDefinition);
|
||||
const TSoftObjectPtr<UVoxelStrateDefinition> SoftDefinition(Definition);
|
||||
Settings->FixedStrates.Add(Index, SoftDefinition);
|
||||
if (!bUseAuthoredStrate)
|
||||
{
|
||||
Settings->StratePool.Add(SoftDefinition);
|
||||
}
|
||||
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
|
||||
}
|
||||
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
|
||||
}
|
||||
|
||||
auto ApplyTargetOverrides = [&Arguments](UVoxelStrateDefinition* Definition) -> bool
|
||||
@@ -13472,14 +13559,16 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||
|
||||
const std::array<std::uint64_t, 4> Actual = StandaloneCoreAggregateHashes(Rows);
|
||||
const std::array<std::uint64_t, 4> Expected{
|
||||
0xdb2991c73215af4cull, 0x8236c5edd7fc8e4full,
|
||||
0x4c48fa75a543d58eull, 0xbea22e76b34a4dfcull};
|
||||
0xde345030348f2c5dull, 0xcbf0888004618b87ull,
|
||||
0x7a11e28e052d69a8ull, 0x52b88607e8dbcbfdull};
|
||||
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
|
||||
const bool bPassageWalkabilityPassed = Stats.walkSamples == 5642 && Stats.walkFailures == 0;
|
||||
if (Rows.size() != 2226 || Actual != Expected || !bPassageWalkabilityPassed)
|
||||
constexpr std::uint64_t ExpectedWalkSamples = 21425;
|
||||
const bool bPassageWalkabilityPassed = Stats.walkSamples == ExpectedWalkSamples
|
||||
&& Stats.walkFailures == 0;
|
||||
if (Rows.size() != 2578 || Actual != Expected || !bPassageWalkabilityPassed)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected=5642/5642"),
|
||||
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected_walk=%llu"),
|
||||
static_cast<unsigned long long>(WorkerLimit),
|
||||
static_cast<unsigned long long>(Rows.size()),
|
||||
static_cast<unsigned long long>(Actual[0]),
|
||||
@@ -13487,12 +13576,13 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||
static_cast<unsigned long long>(Actual[2]),
|
||||
static_cast<unsigned long long>(Actual[3]),
|
||||
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
|
||||
static_cast<unsigned long long>(Stats.walkSamples));
|
||||
static_cast<unsigned long long>(Stats.walkSamples),
|
||||
static_cast<unsigned long long>(ExpectedWalkSamples));
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreAdapter] pass schema=1 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=5642/5642"),
|
||||
TEXT("[VoxelForgeCoreAdapter] pass schema=2 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu"),
|
||||
static_cast<unsigned long long>(WorkerLimit),
|
||||
OwnerClipRadius,
|
||||
OwnerMaxLod,
|
||||
@@ -13500,7 +13590,9 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||
static_cast<unsigned long long>(Actual[0]),
|
||||
static_cast<unsigned long long>(Actual[1]),
|
||||
static_cast<unsigned long long>(Actual[2]),
|
||||
static_cast<unsigned long long>(Actual[3]));
|
||||
static_cast<unsigned long long>(Actual[3]),
|
||||
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
|
||||
static_cast<unsigned long long>(ExpectedWalkSamples));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -13515,6 +13607,24 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
*Arguments.SettingsReference);
|
||||
return false;
|
||||
}
|
||||
TStrongObjectPtr<UVoxelSettings> OwnerPlanSettings;
|
||||
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
|
||||
UVoxelSettings* ExportSettings = AuthoredSettings;
|
||||
if (Arguments.bOwnerFourStratePlan)
|
||||
{
|
||||
OwnerPlanSettings = TStrongObjectPtr<UVoxelSettings>(
|
||||
DuplicateObject<UVoxelSettings>(AuthoredSettings, GetTransientPackage()));
|
||||
if (!OwnerPlanSettings.IsValid())
|
||||
{
|
||||
OutError = TEXT("Could not make a transient copy of owner settings for the four-strate plan.");
|
||||
return false;
|
||||
}
|
||||
ExportSettings = OwnerPlanSettings.Get();
|
||||
if (!ApplyOwnerFourStratePlan(ExportSettings, OwnerPlanDefinitions, false, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
UVoxelStrateManager* Manager = NewObject<UVoxelStrateManager>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
int32 Seed = AuthoredSettings->Seed;
|
||||
@@ -13526,7 +13636,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
OriginSpineRadius = Season->GetOriginSpineRadius();
|
||||
WorldRadiusVoxels = Season->GetWorldRadiusVoxels();
|
||||
}
|
||||
if (!Manager->Initialize(AuthoredSettings, Seed))
|
||||
if (!Manager->Initialize(ExportSettings, Seed))
|
||||
{
|
||||
OutError = TEXT("Owner settings did not produce a valid resolved strate layout.");
|
||||
return false;
|
||||
@@ -13534,7 +13644,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
Seed = Manager->GetWorldSeed();
|
||||
UVoxelGenerator* Generator = NewObject<UVoxelGenerator>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
Generator->InitializeSettings(AuthoredSettings);
|
||||
Generator->InitializeSettings(ExportSettings);
|
||||
Generator->SetStrateManager(Manager);
|
||||
|
||||
TArray<FStandaloneExportSample> Samples;
|
||||
@@ -13668,25 +13778,25 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export"));
|
||||
Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm);
|
||||
Writer->WriteValue(TEXT("seed"), Seed);
|
||||
Writer->WriteValue(TEXT("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry);
|
||||
Writer->WriteValue(TEXT("open_surface_entry"), ExportSettings->bOpenSurfaceEntry);
|
||||
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||
Writer->WriteValue(TEXT("world_radius_voxels"), WorldRadiusVoxels);
|
||||
Writer->WriteValue(TEXT("edge_seal_thickness"), AuthoredSettings->EdgeSealThickness);
|
||||
Writer->WriteValue(TEXT("edge_seal_thickness"), ExportSettings->EdgeSealThickness);
|
||||
Writer->WriteValue(TEXT("total_strates"), Manager->GetNumStrates());
|
||||
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), AuthoredSettings->InterStrateGapChunks);
|
||||
Writer->WriteValue(TEXT("clip_radius"), AuthoredSettings->ClipRadius);
|
||||
Writer->WriteValue(TEXT("max_clip_level"), AuthoredSettings->MaxClipLevel);
|
||||
Writer->WriteValue(TEXT("full_res_clip_levels"), AuthoredSettings->FullResClipLevels);
|
||||
Writer->WriteValue(TEXT("coarse_tile_cells"), AuthoredSettings->CoarseTileCells);
|
||||
Writer->WriteValue(TEXT("render_distance_chunks"), AuthoredSettings->RenderDistanceChunks);
|
||||
Writer->WriteValue(TEXT("generate_skirts"), AuthoredSettings->bGenerateSkirts);
|
||||
Writer->WriteValue(TEXT("skirt_cells"), AuthoredSettings->SkirtCells);
|
||||
Writer->WriteValue(TEXT("lod_octave_drop"), AuthoredSettings->LODOctaveDrop);
|
||||
Writer->WriteValue(TEXT("strate_content_cut_min_level"), AuthoredSettings->GetEffectiveStrateContentCutMinLevel());
|
||||
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), ExportSettings->InterStrateGapChunks);
|
||||
Writer->WriteValue(TEXT("clip_radius"), ExportSettings->ClipRadius);
|
||||
Writer->WriteValue(TEXT("max_clip_level"), ExportSettings->MaxClipLevel);
|
||||
Writer->WriteValue(TEXT("full_res_clip_levels"), ExportSettings->FullResClipLevels);
|
||||
Writer->WriteValue(TEXT("coarse_tile_cells"), ExportSettings->CoarseTileCells);
|
||||
Writer->WriteValue(TEXT("render_distance_chunks"), ExportSettings->RenderDistanceChunks);
|
||||
Writer->WriteValue(TEXT("generate_skirts"), ExportSettings->bGenerateSkirts);
|
||||
Writer->WriteValue(TEXT("skirt_cells"), ExportSettings->SkirtCells);
|
||||
Writer->WriteValue(TEXT("lod_octave_drop"), ExportSettings->LODOctaveDrop);
|
||||
Writer->WriteValue(TEXT("strate_content_cut_min_level"), ExportSettings->GetEffectiveStrateContentCutMinLevel());
|
||||
TSet<const UObject*> ActiveObjects;
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"),
|
||||
StandaloneSerializeStruct(AuthoredSettings->GetClass(), AuthoredSettings,
|
||||
ActiveObjects, AuthoredSettings));
|
||||
StandaloneSerializeStruct(ExportSettings->GetClass(), ExportSettings,
|
||||
ActiveObjects, ExportSettings));
|
||||
|
||||
Writer->WriteArrayStart(TEXT("strates"));
|
||||
for (const FStrateSlot& Slot : Manager->GetLayout())
|
||||
@@ -13756,6 +13866,68 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
Writer->WriteObjectEnd();
|
||||
Writer->Close();
|
||||
|
||||
// Export the exact Core manifest and resolved layout that the Unreal adapter just
|
||||
// generated from these live settings and slots. The standalone research tools consume
|
||||
// these snapshots instead of maintaining a second copy of the planning rules.
|
||||
FString RuntimeSnapshotJson;
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
ExportSettings, Manager, 3, 8, StandaloneCoreVoxelSizeCm,
|
||||
RuntimeSnapshotJson, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FTCHARToUTF8 RuntimeSnapshotUtf8(*RuntimeSnapshotJson);
|
||||
std::string CoreSnapshotError;
|
||||
const std::optional<VoxelForgeCore::ResolvedWorldSnapshot> CoreSnapshot =
|
||||
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
|
||||
std::string_view(RuntimeSnapshotUtf8.Get(),
|
||||
static_cast<std::size_t>(RuntimeSnapshotUtf8.Length())), CoreSnapshotError);
|
||||
if (!CoreSnapshot || CoreSnapshot->worldAssembly() == nullptr
|
||||
|| CoreSnapshot->resolvedLayout() == nullptr)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core adapter export did not produce a complete manifest and resolved layout: %s"),
|
||||
UTF8_TO_TCHAR(CoreSnapshotError.c_str()));
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string CoreAssemblyText = CoreSnapshot->worldAssembly()->ToJson();
|
||||
const std::string CoreLayoutText = CoreSnapshot->resolvedLayout()->ToJson();
|
||||
if (CoreAssemblyText.empty() || CoreLayoutText.empty())
|
||||
{
|
||||
OutError = TEXT("Core adapter export produced an empty canonical manifest or layout.");
|
||||
return false;
|
||||
}
|
||||
const FString CanonicalAssemblyJson = UTF8_TO_TCHAR(CoreAssemblyText.c_str());
|
||||
const FString CanonicalLayoutJson = UTF8_TO_TCHAR(CoreLayoutText.c_str());
|
||||
TSharedPtr<FJsonObject> ExportRoot;
|
||||
const TSharedRef<TJsonReader<>> ExportReader = TJsonReaderFactory<>::Create(Json);
|
||||
if (!FJsonSerializer::Deserialize(ExportReader, ExportRoot) || !ExportRoot.IsValid())
|
||||
{
|
||||
OutError = TEXT("Could not reopen the owner export to attach the Core plan snapshots.");
|
||||
return false;
|
||||
}
|
||||
TSharedPtr<FJsonObject> AssemblyObject;
|
||||
const TSharedRef<TJsonReader<>> AssemblyReader =
|
||||
TJsonReaderFactory<>::Create(CanonicalAssemblyJson);
|
||||
if (!FJsonSerializer::Deserialize(AssemblyReader, AssemblyObject) || !AssemblyObject.IsValid())
|
||||
{
|
||||
OutError = TEXT("Could not parse the Core adapter's canonical manifest for export.");
|
||||
return false;
|
||||
}
|
||||
ExportRoot->SetObjectField(TEXT("world_assembly"), AssemblyObject.ToSharedRef());
|
||||
ExportRoot->SetStringField(TEXT("core_world_assembly_json"), CanonicalAssemblyJson);
|
||||
ExportRoot->SetStringField(TEXT("resolved_layout_json"), CanonicalLayoutJson);
|
||||
FString ExportedJson;
|
||||
const TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> ExportWriter =
|
||||
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&ExportedJson);
|
||||
if (!FJsonSerializer::Serialize(ExportRoot.ToSharedRef(), ExportWriter))
|
||||
{
|
||||
OutError = TEXT("Could not serialize the owner export with its canonical Core plan.");
|
||||
return false;
|
||||
}
|
||||
Json = MoveTemp(ExportedJson);
|
||||
|
||||
const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json"));
|
||||
if (!FFileHelper::SaveStringToFile(
|
||||
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
@@ -13764,11 +13936,16 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
return false;
|
||||
}
|
||||
if (OutJson) *OutJson = Json;
|
||||
if (!VerifyStandaloneCoreAdapter(Json, OutError))
|
||||
if (!Arguments.bSkipStandaloneCoreAdapterParity && !VerifyStandaloneCoreAdapter(Json, OutError))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
|
||||
return false;
|
||||
}
|
||||
if (Arguments.bSkipStandaloneCoreAdapterParity)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreAdapter] skipped legacy W1/W8/W16 fixture parity for this export-only update."));
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
|
||||
Seed, StandaloneCoreVoxelSizeCm, Manager->GetNumStrates(), Manager->GetPassages().Num(),
|
||||
@@ -14808,7 +14985,7 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
||||
UVoxelSettings* Source, int32 Seed,
|
||||
TStrongObjectPtr<UVoxelSettings>& OutSettings,
|
||||
TStrongObjectPtr<UVoxelStrateDefinition>& OutSurfaceDefinition,
|
||||
FString& OutError)
|
||||
FString& OutError, bool bCompactTracePlan = false)
|
||||
{
|
||||
if (Source == nullptr)
|
||||
{
|
||||
@@ -14824,11 +15001,10 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
||||
nullptr, TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"));
|
||||
if (SurfaceDefinition == nullptr
|
||||
|| SurfaceDefinition->GeneratorType != ECaveGeneratorType::SurfaceWorld
|
||||
|| SurfaceDefinition->StrateHeightInChunks <= 0
|
||||
|| Source->TotalStrates != 1)
|
||||
|| SurfaceDefinition->StrateHeightInChunks <= 0)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("The staged owner assets do not describe the logged single SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."),
|
||||
TEXT("The staged owner assets do not contain a usable DA_Strate1 SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."),
|
||||
SurfaceDefinition != nullptr ? 1 : 0,
|
||||
SurfaceDefinition != nullptr ? static_cast<int32>(SurfaceDefinition->GeneratorType) : INDEX_NONE,
|
||||
SurfaceDefinition != nullptr ? SurfaceDefinition->StrateHeightInChunks : INDEX_NONE,
|
||||
@@ -14843,9 +15019,24 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
||||
return false;
|
||||
}
|
||||
OutSettings = TStrongObjectPtr<UVoxelSettings>(SettingsCopy);
|
||||
if (bCompactTracePlan)
|
||||
{
|
||||
SurfaceDefinition = DuplicateObject<UVoxelStrateDefinition>(SurfaceDefinition,
|
||||
GetTransientPackage(), TEXT("VoxelForgeCompactSurfaceTraceDefinition"));
|
||||
if (SurfaceDefinition == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not make a transient DA_Strate1 copy for the compact common-plan trace.");
|
||||
return false;
|
||||
}
|
||||
// L14's historical fixed-length SurfaceWorld route search cannot place the
|
||||
// new trace fixture even at 16 chunks. Use the same simple, walkable FlatPlain
|
||||
// plan in both host revisions so the streaming comparison reaches AVoxelWorld.
|
||||
SurfaceDefinition->GeneratorType = ECaveGeneratorType::FlatPlain;
|
||||
SurfaceDefinition->StrateHeightInChunks = 16;
|
||||
}
|
||||
OutSurfaceDefinition = TStrongObjectPtr<UVoxelStrateDefinition>(SurfaceDefinition);
|
||||
SettingsCopy->Seed = Seed;
|
||||
SettingsCopy->TotalStrates = Source->TotalStrates;
|
||||
SettingsCopy->TotalStrates = 1;
|
||||
SettingsCopy->StratePool.Reset();
|
||||
SettingsCopy->FixedStrates.Reset();
|
||||
SettingsCopy->FixedStrates.Add(0,
|
||||
@@ -14855,8 +15046,10 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
||||
SettingsCopy->bUseWorldAssemblyManifestOverride = false;
|
||||
const int32 BottomChunkZ = 1 - SurfaceDefinition->StrateHeightInChunks;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 seed=%d strata=1 bounds=[0,%d] height_chunks=%d gap=%d imported_override=cleared"),
|
||||
Seed, BottomChunkZ, SurfaceDefinition->StrateHeightInChunks, SettingsCopy->InterStrateGapChunks);
|
||||
TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 generator=%d seed=%d strata=1 bounds=[0,%d] height_chunks=%d compact_trace=%d gap=%d imported_override=cleared"),
|
||||
static_cast<int32>(SurfaceDefinition->GeneratorType), Seed, BottomChunkZ,
|
||||
SurfaceDefinition->StrateHeightInChunks,
|
||||
bCompactTracePlan ? 1 : 0, SettingsCopy->InterStrateGapChunks);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16432,7 +16625,15 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
}
|
||||
float StagedWalkSpeedCmPerSecond = 0.0f;
|
||||
float StagedSprintSpeedCmPerSecond = 0.0f;
|
||||
if (!ReadStagedPlayerSpeeds(
|
||||
if (Arguments.bCoreWorldStreamTraceOnly)
|
||||
{
|
||||
StagedWalkSpeedCmPerSecond = 600.0f;
|
||||
StagedSprintSpeedCmPerSecond = 6000.0f;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgePawnSpeedFixture] source=trace_fixture_defaults walk_cm_s=%.3f sprint_cm_s=%.3f"),
|
||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond);
|
||||
}
|
||||
else if (!ReadStagedPlayerSpeeds(
|
||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
||||
{
|
||||
return false;
|
||||
@@ -17654,8 +17855,16 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
|
||||
float StagedWalkSpeedCmPerSecond = 0.0f;
|
||||
float StagedSprintSpeedCmPerSecond = 0.0f;
|
||||
if (!ReadStagedPlayerSpeeds(
|
||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
||||
if (Arguments.bCoreWorldStreamTraceOnly)
|
||||
{
|
||||
StagedWalkSpeedCmPerSecond = 600.0f;
|
||||
StagedSprintSpeedCmPerSecond = 6000.0f;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgePawnSpeedFixture] source=trace_fixture_defaults walk_cm_s=%.3f sprint_cm_s=%.3f"),
|
||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond);
|
||||
}
|
||||
else if (!ReadStagedPlayerSpeeds(
|
||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -17663,8 +17872,26 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings);
|
||||
TStrongObjectPtr<UVoxelSettings> OwnerSurfaceSettings;
|
||||
TStrongObjectPtr<UVoxelStrateDefinition> OwnerSurfaceDefinition;
|
||||
if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed,
|
||||
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError))
|
||||
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
|
||||
if (Arguments.bOwnerFourStratePlan)
|
||||
{
|
||||
UVoxelSettings* PlanSettings = DuplicateObject<UVoxelSettings>(
|
||||
AuthoredSettings, GetTransientPackage(), TEXT("VoxelForgeOwnerFourStrateTraceSettings"));
|
||||
if (PlanSettings == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not duplicate owner settings for the transient four-strate Core trace.");
|
||||
return false;
|
||||
}
|
||||
PlanSettings->Seed = OwnerSeed;
|
||||
if (!ApplyOwnerFourStratePlan(PlanSettings, OwnerPlanDefinitions, true, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OwnerSurfaceSettings = TStrongObjectPtr<UVoxelSettings>(PlanSettings);
|
||||
}
|
||||
else if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed,
|
||||
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError,
|
||||
Arguments.bCoreWorldStreamTraceOnly))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -17822,6 +18049,25 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
|
||||
return false;
|
||||
}
|
||||
FVector StartupTraceSpawnCm = FVector::ZeroVector;
|
||||
FString StartupTraceSpawnText;
|
||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceSpawnCm="),
|
||||
StartupTraceSpawnText, false))
|
||||
{
|
||||
TArray<FString> Components;
|
||||
StartupTraceSpawnText.ParseIntoArray(Components, TEXT(","), true);
|
||||
if (Components.Num() != 3
|
||||
|| !LexTryParseString(StartupTraceSpawnCm.X, *Components[0])
|
||||
|| !LexTryParseString(StartupTraceSpawnCm.Y, *Components[1])
|
||||
|| !LexTryParseString(StartupTraceSpawnCm.Z, *Components[2])
|
||||
|| !FMath::IsFinite(StartupTraceSpawnCm.X)
|
||||
|| !FMath::IsFinite(StartupTraceSpawnCm.Y)
|
||||
|| !FMath::IsFinite(StartupTraceSpawnCm.Z))
|
||||
{
|
||||
OutError = TEXT("voxel.StartupTraceSpawnCm must be finite X,Y,Z centimetres.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient);
|
||||
if (PawnRoot == nullptr)
|
||||
{
|
||||
@@ -17830,7 +18076,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
}
|
||||
PlayerPawn->SetRootComponent(PawnRoot);
|
||||
PawnRoot->RegisterComponent();
|
||||
PlayerPawn->SetActorLocation(FVector::ZeroVector, false, nullptr, ETeleportType::TeleportPhysics);
|
||||
PlayerPawn->SetActorLocation(StartupTraceSpawnCm, false, nullptr, ETeleportType::TeleportPhysics);
|
||||
PlayerController->Possess(PlayerPawn);
|
||||
if (TestWorld->GetFirstPlayerController() != PlayerController
|
||||
|| PlayerController->GetPawn() != PlayerPawn)
|
||||
@@ -17859,7 +18105,10 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1);
|
||||
Actor->CoreClipRadius = 3;
|
||||
Actor->CoreMaxClipLevel = 8;
|
||||
Actor->bCaptureVoxelForgeCoreHashesForValidation = true;
|
||||
// A timing trace must use the production tile path. Core density hashing disables the
|
||||
// native heightfield band shortcut and measures a different workload; hash validation
|
||||
// remains enabled in the dedicated parity tests below.
|
||||
Actor->bCaptureVoxelForgeCoreHashesForValidation = false;
|
||||
Actor->FinishSpawning(ActorTransform);
|
||||
TestWorld->InitializeActorsForPlay(FURL());
|
||||
TestWorld->BeginPlay();
|
||||
@@ -17869,6 +18118,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
return false;
|
||||
}
|
||||
Actor->SetActorTickEnabled(true);
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("trace_spawn_location"), FString::Printf(
|
||||
TEXT("\"location_cm\":[%s,%s,%s]"),
|
||||
*FString::SanitizeFloat(StartupTraceSpawnCm.X),
|
||||
*FString::SanitizeFloat(StartupTraceSpawnCm.Y),
|
||||
*FString::SanitizeFloat(StartupTraceSpawnCm.Z)));
|
||||
|
||||
auto DesiredTilesLoaded = [](const AVoxelWorld* World)
|
||||
{
|
||||
@@ -18023,6 +18277,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
const double CarveBrushRadiusCoreVoxels =
|
||||
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
||||
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
|
||||
FIntVector ColdStartCollisionCenter = FIntVector::ZeroValue;
|
||||
uint64 ExpectedEditDigest = 0;
|
||||
std::array<std::uint64_t, 4> InitialAggregates{};
|
||||
std::array<std::uint64_t, 4> TravelAggregates{};
|
||||
@@ -18208,10 +18463,17 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
BiomeProbeSpanVoxels);
|
||||
|
||||
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
|
||||
const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({});
|
||||
// Match the actor's cold-start fallback camera when the transient world has no pawn.
|
||||
// Aligner la reference sur la camera de repli de l'acteur sans pawn au demarrage.
|
||||
const VoxelForgeCore::WorldConfiguration& ReferenceConfiguration =
|
||||
ReferenceWorld.configuration();
|
||||
const VoxelForgeCore::Vec3d InitialCameraVoxels = ReferenceConfiguration.viewerStartFound
|
||||
? ReferenceConfiguration.viewerStartVoxels : VoxelForgeCore::Vec3d{};
|
||||
const std::vector<VoxelForgeCore::TileKey> Desired =
|
||||
ReferenceWorld.desiredTiles(InitialCameraVoxels);
|
||||
if (Desired.empty())
|
||||
{
|
||||
OutError = TEXT("Standalone Core returned an empty reference clipmap at the origin.");
|
||||
OutError = TEXT("Standalone Core returned an empty reference clipmap at the cold-start camera.");
|
||||
return false;
|
||||
}
|
||||
ExpectedInitialTileCount = Desired.size();
|
||||
@@ -18258,7 +18520,23 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
ReferenceWorld.retainTileRegions(Desired);
|
||||
// Match the runtime collision probe around the same cold-start viewer tile.
|
||||
// Reproduire la sonde de collision autour de la meme tuile de depart a froid.
|
||||
ColdStartCollisionCenter = FIntVector(
|
||||
FMath::FloorToInt(InitialCameraVoxels.x / 32.0),
|
||||
FMath::FloorToInt(InitialCameraVoxels.y / 32.0),
|
||||
FMath::FloorToInt(InitialCameraVoxels.z / 32.0));
|
||||
std::vector<VoxelForgeCore::TileKey> ColdStartCollisionTiles;
|
||||
ColdStartCollisionTiles.reserve(static_cast<std::size_t>(2 * CollisionRadius + 1)
|
||||
* static_cast<std::size_t>(2 * CollisionRadius + 1)
|
||||
* static_cast<std::size_t>(2 * CollisionRadius + 1));
|
||||
for (int32 Z = -CollisionRadius; Z <= CollisionRadius; ++Z)
|
||||
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
||||
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
|
||||
ColdStartCollisionTiles.push_back({
|
||||
{ColdStartCollisionCenter.X + X,
|
||||
ColdStartCollisionCenter.Y + Y,
|
||||
ColdStartCollisionCenter.Z + Z}, 0});
|
||||
|
||||
if (Arguments.bCoreHybridCliffOnly)
|
||||
{
|
||||
@@ -18332,6 +18610,47 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
||||
else
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
||||
|
||||
// Keep collision hashes outside ExpectedInitialDesiredSet; the collision cube is an
|
||||
// additional runtime request even where it overlaps the visual clipmap.
|
||||
// Garder les hachages de collision hors de l'ensemble visuel initial, meme s'ils se
|
||||
// recoupent avec le clipmap.
|
||||
std::vector<VoxelForgeCore::TileKey> RetainedReferenceTiles = Desired;
|
||||
RetainedReferenceTiles.insert(RetainedReferenceTiles.end(),
|
||||
ColdStartCollisionTiles.begin(), ColdStartCollisionTiles.end());
|
||||
ReferenceWorld.retainTileRegions(RetainedReferenceTiles);
|
||||
if (!ReferenceWorld.compileTileRegion(ColdStartCollisionTiles, 16, Scheduler, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Standalone collision reference compile failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
std::size_t CollisionBodyTiles = 0;
|
||||
for (const VoxelForgeCore::TileKey& Key : ColdStartCollisionTiles)
|
||||
{
|
||||
const std::optional<VoxelForgeCore::TileMesh> Mesh =
|
||||
ReferenceWorld.generateTile(Key, Options, CoreError);
|
||||
if (!Mesh)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Standalone collision tile generation failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
FVoxelForgeCoreTileHashRecord Record;
|
||||
Record.bEmpty = Mesh->indices.empty();
|
||||
Record.bHasHashes = Mesh->hasDensityHash && Mesh->hasMeshHash
|
||||
&& Mesh->hasPayloadHash && Mesh->hasTopologyHash;
|
||||
if (!Record.bEmpty) ++CollisionBodyTiles;
|
||||
ExpectedHashes.Add(FVoxelTileKey(
|
||||
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod), Record);
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] standalone_collision_reference=1 center=(%d,%d,%d) tiles=%d body=%llu no_body=%llu"),
|
||||
ColdStartCollisionCenter.X, ColdStartCollisionCenter.Y, ColdStartCollisionCenter.Z,
|
||||
static_cast<int32>(ColdStartCollisionTiles.size()),
|
||||
static_cast<unsigned long long>(CollisionBodyTiles),
|
||||
static_cast<unsigned long long>(ColdStartCollisionTiles.size() - CollisionBodyTiles));
|
||||
|
||||
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
|
||||
AggregateRows.reserve(Desired.size());
|
||||
bool bFoundCarveVertex = false;
|
||||
@@ -19367,7 +19686,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
||||
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
|
||||
{
|
||||
const FVoxelTileKey Key(FIntVector(X, Y, Z), 0);
|
||||
const FVoxelTileKey Key(ColdStartCollisionCenter + FIntVector(X, Y, Z), 0);
|
||||
ExpectedCollisionSet.Add(Key);
|
||||
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
|
||||
if (Record == nullptr)
|
||||
@@ -21361,6 +21680,44 @@ int32 RunBatch(const FString& Params)
|
||||
|
||||
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
{
|
||||
if (FParse::Param(*Params, TEXT("coreassetupgradeaudit")))
|
||||
{
|
||||
// Load the untouched BuildHost copies and emit their in-memory conversions; never save a package.
|
||||
// Charge les copies BuildHost intactes et affiche les conversions en memoire, sans sauver de package.
|
||||
UVoxelSettings* SettingsAsset = LoadObject<UVoxelSettings>(nullptr,
|
||||
TEXT("/Game/VoxelForge/DA_Settings.DA_Settings"));
|
||||
if (SettingsAsset == nullptr)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing DA_Settings copy"));
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_gap_chunks=%d new_gap_m=%.6f layout_chunk_m=%.6f"),
|
||||
*SettingsAsset->GetPathName(), SettingsAsset->InterStrateGapChunks,
|
||||
SettingsAsset->InterStrateGapMeters, SettingsAsset->StrateLayoutChunkSizeMeters);
|
||||
|
||||
const TCHAR* StratePaths[] = {
|
||||
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
|
||||
};
|
||||
for (const TCHAR* Path : StratePaths)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = LoadObject<UVoxelStrateDefinition>(nullptr, Path);
|
||||
if (Definition == nullptr)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing strate copy %s"), Path);
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_height_chunks=%d new_height_m=%.6f old_transition_chunks=%d new_transition_m=%.6f"),
|
||||
*Definition->GetPathName(), Definition->StrateHeightInChunks,
|
||||
Definition->StrateHeightMeters, Definition->TransitionBlendChunks,
|
||||
Definition->TransitionWidthMeters);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (FParse::Param(*Params, TEXT("step2f")))
|
||||
{
|
||||
return RunStep2f(Params);
|
||||
|
||||
@@ -13,6 +13,8 @@ COMPILER="clang++-19"
|
||||
VARIANT="clang19"
|
||||
FP_CONTRACT="off"
|
||||
JOBS=12
|
||||
SANITIZERS=""
|
||||
EXCLUDE_UNSIGNED_OVERFLOW=0
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
@@ -20,6 +22,8 @@ while (($#)); do
|
||||
--variant=*) VARIANT="${1#*=}" ;;
|
||||
--fp-contract=*) FP_CONTRACT="${1#*=}" ;;
|
||||
--jobs=*) JOBS="${1#*=}" ;;
|
||||
--sanitize=*) SANITIZERS="${1#*=}" ;;
|
||||
--exclude-unsigned-integer-overflow) EXCLUDE_UNSIGNED_OVERFLOW=1 ;;
|
||||
*) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
@@ -60,11 +64,22 @@ INCLUDES=(
|
||||
"-I$ROOT/Source/VoxelForge/Public"
|
||||
"-I$ROOT/Source/VoxelForge/Private"
|
||||
)
|
||||
OPT_LEVEL=-O2
|
||||
LINK_FLAGS=()
|
||||
if [[ -n "$SANITIZERS" ]]; then
|
||||
OPT_LEVEL=-O1
|
||||
LINK_FLAGS+=("-fsanitize=$SANITIZERS" -fno-sanitize-recover=undefined)
|
||||
if ((EXCLUDE_UNSIGNED_OVERFLOW)); then
|
||||
LINK_FLAGS+=(-fno-sanitize=unsigned-integer-overflow)
|
||||
fi
|
||||
fi
|
||||
|
||||
FLAGS=(
|
||||
-std=c++20 -O2 -mavx2 -mfma -fno-fast-math
|
||||
-std=c++20 "$OPT_LEVEL" -mavx2 -mfma -fno-fast-math
|
||||
-fexceptions -pthread "${INCLUDES[@]}"
|
||||
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\""
|
||||
)
|
||||
FLAGS+=("${LINK_FLAGS[@]}")
|
||||
if [[ "$FP_CONTRACT" != "default" ]]; then
|
||||
FLAGS+=("-ffp-contract=$FP_CONTRACT")
|
||||
fi
|
||||
@@ -131,7 +146,7 @@ PIDS=()
|
||||
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
|
||||
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
|
||||
OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT"
|
||||
"$COMPILER" -pthread "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
|
||||
"$COMPILER" -pthread "${LINK_FLAGS[@]}" "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
|
||||
CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT"
|
||||
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
|
||||
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" -I"$ROOT/Source" \
|
||||
@@ -145,4 +160,5 @@ if [[ "$POST_BUILD_STAMP" != "$SOURCE_STAMP" ]]; then
|
||||
fi
|
||||
printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT"
|
||||
printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_OUTPUT"
|
||||
printf 'Compiler: %s | variant: %s | fp-contract: %s | jobs: %s\n' "$COMPILER" "$VARIANT" "$FP_CONTRACT" "$JOBS"
|
||||
printf 'Compiler: %s | variant: %s | fp-contract: %s | sanitizers: %s | jobs: %s\n' \
|
||||
"$COMPILER" "$VARIANT" "$FP_CONTRACT" "${SANITIZERS:-off}" "$JOBS"
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <AssetRegistry/AssetData.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <AssetRegistry/AssetRegistryModule.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Async/ParallelFor.h>
|
||||
#endif
|
||||
@@ -1,5 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <CoreMinimal.h>
|
||||
#else
|
||||
|
||||
// Deliberately small UE-style surface used by the copied VoxelForge CPU sources.
|
||||
// This is a value/container compatibility layer; it does not start or link Unreal.
|
||||
#include <algorithm>
|
||||
@@ -45,6 +50,9 @@
|
||||
#define GENERATED_UCLASS_BODY()
|
||||
#define VOXELFORGE_API
|
||||
#define FORCEINLINE inline
|
||||
#ifdef WITH_EDITOR
|
||||
#undef WITH_EDITOR
|
||||
#endif
|
||||
#define WITH_EDITOR 0
|
||||
#define TRACE_CPUPROFILER_EVENT_SCOPE(...)
|
||||
#define PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS
|
||||
@@ -705,3 +713,4 @@ struct FParse
|
||||
struct FCrc { static uint32 MemCrc32(const void*Data,int32 Length,uint32 Crc=0){const auto*P=static_cast<const uint8*>(Data);uint32 V=~Crc;for(int32 I=0;I<Length;++I){V^=P[I];for(int J=0;J<8;++J)V=(V>>1)^(0xEDB88320u&static_cast<uint32>(-static_cast<int32>(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast<int32>(std::strlen(S)),Crc);} };
|
||||
template<class T> using TAtomic = std::atomic<T>;
|
||||
class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomic<int32>Value{0};};
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Dom/JsonObject.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Dom/JsonValue.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Engine/DataAsset.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <GameplayTagContainer.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/CriticalSection.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/Event.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/FileManager.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/IConsoleManager.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/PlatformProcess.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/PlatformTime.h>
|
||||
#endif
|
||||
@@ -1,5 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Json.h>
|
||||
#else
|
||||
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
@@ -162,3 +167,4 @@ namespace VFJson
|
||||
return Parser(std::move(Text)).Parse(Out, Error);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Math/RandomStream.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/CommandLine.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/Crc.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/FileHelper.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/Parse.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/Paths.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/ScopeLock.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/ScopeRWLock.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/SecureHash.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Modules/ModuleManager.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <ProfilingDebugging/CpuProfilerTrace.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Serialization/CustomVersion.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Serialization/JsonSerializer.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Serialization/JsonWriter.h>
|
||||
#endif
|
||||
@@ -1,4 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Stats/Stats.h>
|
||||
#else
|
||||
#define STATCAT_Advanced 0
|
||||
#define DECLARE_STATS_GROUP(Name,Group,Category) constexpr int32 Group=0
|
||||
#define DECLARE_DWORD_COUNTER_STAT_EXTERN(Name,Stat,Group,Api) constexpr int32 Stat=0
|
||||
@@ -8,3 +13,4 @@
|
||||
#define SET_DWORD_STAT(Stat,Value) ((void)0)
|
||||
#define SCOPE_CYCLE_COUNTER(Stat) ((void)0)
|
||||
#define QUICK_SCOPE_CYCLE_COUNTER(Stat) ((void)0)
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Templates/SubclassOf.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Templates/UniquePtr.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <UObject/EnumProperty.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <UObject/FieldIterator.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <UObject/Object.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <UObject/UObjectGlobals.h>
|
||||
#endif
|
||||
@@ -1 +1,6 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <UObject/UnrealType.h>
|
||||
#endif
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "VoxelForgeCore/CpuCycleCounter.h"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
@@ -26,9 +27,12 @@ namespace VoxelForgeCore::floating_island_detail
|
||||
static constexpr std::uint64_t SampleMask=255;
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> sampledFieldCalls{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> fieldCycles{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> fieldNanoseconds{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> sampledNativeCalls{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> nativeCycles{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> nativeNanoseconds{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> exclusiveCycles{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> exclusiveNanoseconds{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> scopeSamples{};
|
||||
std::uint64_t traceSampledMisses=0,traceSeenBefore=0,traceEvictions=0,
|
||||
traceReusedEvictions=0;
|
||||
@@ -118,7 +122,11 @@ namespace VoxelForgeCore::floating_island_detail
|
||||
&FIslandFieldAttributionInstrumentation::SampleMask;
|
||||
sampled=(((calls+phase)&FIslandFieldAttributionInstrumentation::SampleMask)==0);
|
||||
}
|
||||
if(sampled)start=VoxelForgeCoreShared::ReadCpuCycles();
|
||||
if(sampled)
|
||||
{
|
||||
start=VoxelForgeCoreShared::ReadCpuCycles();
|
||||
wallStart=std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
~FScopedIslandAttributionTimer(){Stop();}
|
||||
std::uint64_t Stop() noexcept
|
||||
@@ -128,12 +136,18 @@ namespace VoxelForgeCore::floating_island_detail
|
||||
const std::uint64_t raw=VoxelForgeCoreShared::ReadCpuCycles()-start;
|
||||
const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead();
|
||||
const std::uint64_t cycles=raw>overhead?raw-overhead:0;
|
||||
const auto elapsed=std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now()-wallStart).count();
|
||||
metrics->exclusiveCycles[bucketIndex]+=cycles;
|
||||
metrics->exclusiveNanoseconds[bucketIndex]+=elapsed>0
|
||||
?static_cast<std::uint64_t>(elapsed):0;
|
||||
++metrics->scopeSamples[bucketIndex];
|
||||
if(bucketIndex==static_cast<std::size_t>(EIslandAttributionBucket::Field))
|
||||
{
|
||||
const std::size_t callerIndex=static_cast<std::size_t>(ActiveIslandFieldCaller());
|
||||
metrics->fieldCycles[callerIndex]+=cycles;
|
||||
metrics->fieldNanoseconds[callerIndex]+=elapsed>0
|
||||
?static_cast<std::uint64_t>(elapsed):0;
|
||||
++metrics->sampledFieldCalls[callerIndex];
|
||||
}
|
||||
return cycles;
|
||||
@@ -141,6 +155,7 @@ namespace VoxelForgeCore::floating_island_detail
|
||||
FIslandFieldAttributionInstrumentation* metrics=nullptr;
|
||||
std::size_t bucketIndex=0;
|
||||
std::uint64_t start=0;
|
||||
std::chrono::steady_clock::time_point wallStart{};
|
||||
bool sampled=false;
|
||||
};
|
||||
|
||||
@@ -160,6 +175,7 @@ namespace VoxelForgeCore::floating_island_detail
|
||||
sampledCaller=caller;
|
||||
sampled=true;
|
||||
start=VoxelForgeCoreShared::ReadCpuCycles();
|
||||
wallStart=std::chrono::steady_clock::now();
|
||||
++attribution->sampledNativeCalls[callerIndex];
|
||||
}
|
||||
~FScopedIslandAttributionSample()
|
||||
@@ -168,12 +184,17 @@ namespace VoxelForgeCore::floating_island_detail
|
||||
const std::uint64_t raw=VoxelForgeCoreShared::ReadCpuCycles()-start;
|
||||
const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead();
|
||||
const std::uint64_t duration=raw>overhead?raw-overhead:0;
|
||||
const auto elapsed=std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now()-wallStart).count();
|
||||
const std::size_t callerIndex=static_cast<std::size_t>(sampledCaller);
|
||||
sampledAttribution->nativeCycles[callerIndex]+=duration;
|
||||
sampledAttribution->nativeNanoseconds[callerIndex]+=elapsed>0
|
||||
?static_cast<std::uint64_t>(elapsed):0;
|
||||
}
|
||||
FIslandFieldAttributionInstrumentation* sampledAttribution=nullptr;
|
||||
EIslandFieldCaller sampledCaller=EIslandFieldCaller::Other;
|
||||
std::uint64_t start=0;
|
||||
std::chrono::steady_clock::time_point wallStart{};
|
||||
bool sampled=false;
|
||||
};
|
||||
|
||||
|
||||
@@ -131,9 +131,41 @@ namespace VoxelForgeCore
|
||||
std::uint64_t mixedBoxes = 0;
|
||||
std::uint64_t unknownBoxes = 0;
|
||||
std::uint64_t fieldCalls = 0;
|
||||
// Opt-in composer diagnostics. These stay zero during ordinary generation.
|
||||
bool passageComposerProfiled = false;
|
||||
std::uint64_t passageTileFeatureCount = 0;
|
||||
std::uint64_t passageTileRouteSegmentCandidates = 0;
|
||||
std::uint64_t passageTileMouthSegmentCandidates = 0;
|
||||
std::uint64_t passageComposerQueries = 0;
|
||||
std::uint64_t passageIterations = 0;
|
||||
std::uint64_t passageUpperMouthFilterChecks = 0;
|
||||
std::uint64_t passageColumnCandidates = 0;
|
||||
std::uint64_t passageColumnDistanceCalls = 0;
|
||||
std::uint64_t passageNegativeBoundChecks = 0;
|
||||
std::uint64_t passageNegativeBoundRejects = 0;
|
||||
std::uint64_t passagePositiveBoundChecks = 0;
|
||||
std::uint64_t passagePositiveBoundRejects = 0;
|
||||
std::uint64_t passageCarveSegmentVisits = 0;
|
||||
std::uint64_t passageSmoothCarveJoins = 0;
|
||||
std::uint64_t passageSupportSegmentVisits = 0;
|
||||
std::uint64_t passageSmoothSupportJoins = 0;
|
||||
std::uint64_t passageSupportCapsuleChecks = 0;
|
||||
std::uint64_t passageLaterBoreChecks = 0;
|
||||
std::uint64_t passageOtherCapsuleChecks = 0;
|
||||
std::uint64_t passageMouthSupportCalls = 0;
|
||||
std::uint64_t passageMouthRouteSegmentChecks = 0;
|
||||
std::uint64_t passageReliefFieldCalls = 0;
|
||||
std::uint64_t passageSmoothstepEvaluations = 0;
|
||||
std::uint64_t floatFieldCalls = 0;
|
||||
std::uint64_t latticeFieldCalls = 0;
|
||||
std::uint64_t unclampedFieldCalls = 0;
|
||||
std::uint64_t densityPointCalls = 0;
|
||||
std::uint64_t densityFloatPointCalls = 0;
|
||||
std::uint64_t densityLatticePointCalls = 0;
|
||||
std::uint64_t densityUnclampedPointCalls = 0;
|
||||
std::uint64_t densityX8PointCalls = 0;
|
||||
std::uint64_t densityFieldEvalNanoseconds = 0;
|
||||
std::uint64_t densityFieldEvalSamplePoints = 0;
|
||||
std::uint64_t columnCacheHits = 0;
|
||||
std::uint64_t columnCacheMisses = 0;
|
||||
std::uint64_t columnCacheConflictMisses = 0;
|
||||
@@ -220,6 +252,9 @@ namespace VoxelForgeCore
|
||||
std::uint32_t farHorizonVersion = 0;
|
||||
bool farShellRepairSkipped = false;
|
||||
bool tileWasClassified = false;
|
||||
std::uint8_t tileProofVetoReason = 0;
|
||||
bool tileProofSafe = false;
|
||||
bool tileLegacyBlockProofRelevant = false;
|
||||
bool meshingSkipped = false;
|
||||
bool featureRefinedResolution = false;
|
||||
bool heightfieldBandLimited = false;
|
||||
@@ -400,7 +435,10 @@ namespace VoxelForgeCore
|
||||
// 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 = 209;
|
||||
// Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges.
|
||||
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
|
||||
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 213;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
@@ -1004,13 +1042,11 @@ namespace VoxelForgeCore
|
||||
&&stratum.recipeVersion==1;
|
||||
}
|
||||
|
||||
inline constexpr std::uint32_t NativeLatticeFieldRecipeVersion=2;
|
||||
|
||||
inline bool IsNativeLatticeFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
|
||||
{
|
||||
return (stratum.generatorKind=="Maze"||stratum.generatorKind=="VerticalShafts")
|
||||
&&stratum.recipeId=="Core.Lattice.PrimitiveField"
|
||||
&&stratum.recipeVersion==NativeLatticeFieldRecipeVersion;
|
||||
&&stratum.recipeVersion==1;
|
||||
}
|
||||
|
||||
inline bool HasNativeGraphField(const ResolvedLayoutStratumV2& stratum) noexcept
|
||||
@@ -1726,6 +1762,9 @@ namespace VoxelForgeCore
|
||||
std::int32_t bandMinZVoxel = -2147483647 - 1;
|
||||
std::int32_t bandMaxZVoxel = 2147483647;
|
||||
bool hashDensity = false;
|
||||
// Falsifier mode: retain mesh/topology hashes while permitting proven empty tiles
|
||||
// to skip their density lattice. The density hash of such a skipped tile is empty.
|
||||
bool allowUniformSkipsWhenHashing = false;
|
||||
bool captureDensitySamples = false;
|
||||
// Diagnostic only; materialize the full density lattice even when block proofs
|
||||
// establish that the tile has no surface.
|
||||
@@ -1744,6 +1783,8 @@ namespace VoxelForgeCore
|
||||
// Separate cold pass only; samples the MC inner stages without changing output.
|
||||
// Passe froide separee seulement; mesure les etapes internes MC sans changer la sortie.
|
||||
bool profileMesherStages = false;
|
||||
// Research-only: count passage-composer work per generated tile.
|
||||
bool profilePassageComposer = false;
|
||||
// Research-only: collect tile-drain kernel counters and floating-island cache metrics.
|
||||
// Inactive on ordinary generation so the hot path only pays the option check.
|
||||
bool instrumentTileDrain = false;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
// Core implementation TUs opt into the standalone Unreal-shaped compatibility headers. UE's
|
||||
// Linux-generated module TUs leave this unset so those same header names forward to the engine.
|
||||
#define VFCORE_USE_COMPAT 1
|
||||
|
||||
// Every standard or platform header used by the copied implementation must be included before
|
||||
// its wrapper opens vfcore::detail. This prevents system declarations from being nested in the
|
||||
// private namespace when the implementation includes its Unreal-shaped compatibility headers.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
@@ -1901,8 +1902,7 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
// The shared JSON value type stores numbers as IEEE-754 doubles. Keep the
|
||||
// serialized seed in the exact integer range, as the stamp vocabulary does.
|
||||
room.irregularitySeed=(isNativeLatticeStrate(room.strateIndex)
|
||||
?LayoutLatticeFeatureSeed(seed,room.strateIndex,
|
||||
NativeLatticeFieldRecipeVersion,room.stableId)
|
||||
?LayoutLatticeFeatureSeed(seed,room.strateIndex,1,room.stableId)
|
||||
:LayoutStampSeed(seed,room.strateIndex,room.stableId))
|
||||
&0x001fffffffffffffull;
|
||||
const std::int64_t pad=room.smoothWidthMicrometres
|
||||
@@ -1919,8 +1919,7 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{
|
||||
const bool nativeLattice=isNativeLatticeStrate(edge.strateIndex);
|
||||
edge.irregularitySeed=(nativeLattice
|
||||
?LayoutLatticeFeatureSeed(seed,edge.strateIndex,
|
||||
NativeLatticeFieldRecipeVersion,edge.stableId)
|
||||
?LayoutLatticeFeatureSeed(seed,edge.strateIndex,1,edge.stableId)
|
||||
:LayoutStampSeed(seed,edge.strateIndex,edge.stableId))
|
||||
&0x001fffffffffffffull;
|
||||
using FCurvePoint=std::array<double,3>;
|
||||
@@ -2327,8 +2326,22 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
const std::int64_t midRadius=std::max<std::int64_t>(
|
||||
static_cast<std::int64_t>(std::llround((upperIsSurfaceWorld?3.5:1.5)*qPerMetre)),
|
||||
cfg?cfg->midRadiusSubvoxels:mouthRadius);
|
||||
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 requestedReachMinimum=cfg
|
||||
?std::max<std::int64_t>(0,cfg->reachMinimumSubvoxels):8*qPerMetre;
|
||||
const std::int64_t requestedReachMaximum=cfg
|
||||
?std::max<std::int64_t>(requestedReachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre;
|
||||
// Keep the landing several metres inside the stratum's authored content.
|
||||
// Near a band edge, the sampled native terrain can exist at the centerline
|
||||
// while the full capsule footprint has no supporting ground.
|
||||
const std::int64_t mouthClearance=std::max(mouthRadius,midRadius)+2*qPerMetre;
|
||||
const auto usableReach=[&](const ResolvedLayoutStratumV2& stratum)
|
||||
{
|
||||
const std::int64_t depth=contentZQ(stratum.content.topMicrometres)
|
||||
-contentZQ(stratum.content.bottomMicrometres);
|
||||
return std::max<std::int64_t>(0,depth-mouthClearance);
|
||||
};
|
||||
const std::int64_t upperReachLimit=usableReach(upper);
|
||||
const std::int64_t lowerReachLimit=usableReach(lower);
|
||||
const std::uint32_t style=cfg?std::min<std::uint32_t>(cfg->style,3u):1u;
|
||||
const std::uint32_t segments=cfg?std::clamp<std::uint32_t>(cfg->segments,16u,48u):20u;
|
||||
const std::int64_t wander=cfg?std::max<std::int64_t>(0,cfg->wanderSubvoxels):3*qPerMetre;
|
||||
@@ -2364,16 +2377,30 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
if(!hasUpperRoute)
|
||||
{
|
||||
upperMouth[0]=xy[0];upperMouth[1]=xy[1];
|
||||
const auto reach=reachValue(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||
connection,0x55505248u),reachMinimum,reachMaximum);
|
||||
const auto requestedReach=reachValue(PassageSeed(seed,upper.strateIndex,
|
||||
lower.strateIndex,connection,0x55505248u),
|
||||
requestedReachMinimum,requestedReachMaximum);
|
||||
const auto reach=std::min(requestedReach,upperReachLimit);
|
||||
upperMouth[2]=contentZQ(upper.content.bottomMicrometres)+reach;
|
||||
if(requestedReach>upperReachLimit)
|
||||
std::printf("[ResolvedPassageReachClamp] id=%s side=upper sampled_m=%.3f usable_max_m=%.3f selected_m=%.3f status=CLAMPED\n",
|
||||
passage.stableId.c_str(),requestedReach/static_cast<double>(qPerMetre),
|
||||
upperReachLimit/static_cast<double>(qPerMetre),
|
||||
reach/static_cast<double>(qPerMetre));
|
||||
}
|
||||
if(!hasLowerRoute)
|
||||
{
|
||||
lowerMouth[0]=xy[0];lowerMouth[1]=xy[1];
|
||||
const auto reach=reachValue(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||
connection,0x4c4f5752u),reachMinimum,reachMaximum);
|
||||
const auto requestedReach=reachValue(PassageSeed(seed,upper.strateIndex,
|
||||
lower.strateIndex,connection,0x4c4f5752u),
|
||||
requestedReachMinimum,requestedReachMaximum);
|
||||
const auto reach=std::min(requestedReach,lowerReachLimit);
|
||||
lowerMouth[2]=contentZQ(lower.content.topMicrometres)-reach;
|
||||
if(requestedReach>lowerReachLimit)
|
||||
std::printf("[ResolvedPassageReachClamp] id=%s side=lower sampled_m=%.3f usable_max_m=%.3f selected_m=%.3f status=CLAMPED\n",
|
||||
passage.stableId.c_str(),requestedReach/static_cast<double>(qPerMetre),
|
||||
lowerReachLimit/static_cast<double>(qPerMetre),
|
||||
reach/static_cast<double>(qPerMetre));
|
||||
}
|
||||
}
|
||||
if(!radialWithin(upperMouth,placementRadius)||!radialWithin(lowerMouth,placementRadius))
|
||||
@@ -2433,18 +2460,29 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
}
|
||||
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
||||
if(hasUpperRoute)
|
||||
// Authored tangents and island anchors already determine the phase.
|
||||
// A generated mouth in a native cave uses its radial approach; an
|
||||
// island landing uses the tangent fit below to ease off the crater rim.
|
||||
const bool generatedIslandWormEntry=!hasUpperRoute&&!hasUpperTangent
|
||||
&&!anchoredSpiral&&upper.generatorKind=="FloatingIslands";
|
||||
if(hasUpperRoute||(anchoredIslandPassage&&!anchoredSpiral)
|
||||
||(!hasUpperRoute&&!anchoredIslandPassage&&!anchoredSpiral
|
||||
&&upper.generatorKind!="FloatingIslands"))
|
||||
{
|
||||
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
|
||||
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]);
|
||||
:(hasUpperTangent?upperTangent[0]:upperMouth[0]
|
||||
-(anchoredIslandPassage&&passageAnchor
|
||||
?passageAnchor->values[0]:0));
|
||||
const std::int64_t phaseY=style==0&&anchoredIslandPassage
|
||||
&&!anchoredSpiral&&passageAnchor
|
||||
?upperMouth[1]-passageAnchor->values[1]
|
||||
:(hasUpperTangent?upperTangent[1]:upperMouth[1]);
|
||||
:(hasUpperTangent?upperTangent[1]:upperMouth[1]
|
||||
-(anchoredIslandPassage&&passageAnchor
|
||||
?passageAnchor->values[1]:0));
|
||||
for(std::uint32_t offset=0;offset<16;++offset)
|
||||
{
|
||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||
@@ -2464,23 +2502,6 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{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,
|
||||
connection,0x44495245u)&1u)!=0;
|
||||
if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor)
|
||||
@@ -2502,7 +2523,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
clockwise=clockwiseTangentX*upperTangent[0]
|
||||
+clockwiseTangentY*upperTangent[1]>=0.0;
|
||||
}
|
||||
const int direction=clockwise?1:-1;
|
||||
int direction=clockwise?1:-1;
|
||||
constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree helix contract.
|
||||
const std::int64_t wormRadialWobble=std::min<std::int64_t>(
|
||||
std::max<std::int64_t>(0,wander/3),2*qPerMetre);
|
||||
@@ -2571,6 +2592,267 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
static_cast<std::int64_t>(std::llround(centerX+radius*directions[directionIndex][0]/65536.0)),
|
||||
static_cast<std::int64_t>(std::llround(centerY+radius*directions[directionIndex][1]/65536.0))};
|
||||
};
|
||||
if(generatedIslandWormEntry)
|
||||
{
|
||||
struct FPhaseScore
|
||||
{
|
||||
std::uint32_t phase=0;
|
||||
int direction=1;
|
||||
std::uint64_t routeLength=0;
|
||||
double clearance=0.0;
|
||||
double tangent=0.0;
|
||||
};
|
||||
std::array<FPhaseScore,32> phaseScores{};
|
||||
std::size_t phaseScoreCount=0;
|
||||
const std::uint32_t hashedPhase=phase;
|
||||
const int hashedDirection=direction;
|
||||
const std::int64_t entryVerticalDistance=std::abs(
|
||||
lowerMouth[2]-upperMouth[2]);
|
||||
const std::int64_t entryRouteGradeQ16=profiledSurfaceJoin?8000
|
||||
:style==0&&anchoredIslandPassage&&!anchoredSpiral?15500
|
||||
:TargetGradeQ16;
|
||||
const auto candidateRouteLength=[&](std::uint32_t candidate,int candidateDirection)
|
||||
{
|
||||
std::vector<std::array<std::int64_t,2>> candidateXY;
|
||||
const auto append=[&](std::array<std::int64_t,2> point)
|
||||
{if(candidateXY.empty()||candidateXY.back()!=point)
|
||||
candidateXY.push_back(point);};
|
||||
append({upperMouth[0],upperMouth[1]});
|
||||
append(ringPoint(candidate));
|
||||
const auto measure=[&]()
|
||||
{
|
||||
std::uint64_t length=0;
|
||||
for(std::size_t i=1;i<candidateXY.size();++i)
|
||||
length+=distanceQ(candidateXY[i][0]-candidateXY[i-1][0],
|
||||
candidateXY[i][1]-candidateXY[i-1][1]);
|
||||
length+=distanceQ(lowerMouth[0]-candidateXY.back()[0],
|
||||
lowerMouth[1]-candidateXY.back()[1]);
|
||||
return length;
|
||||
};
|
||||
std::uint32_t loops=0;
|
||||
std::uint64_t length=0;
|
||||
while(loops<256)
|
||||
{
|
||||
length=measure();
|
||||
if(length*static_cast<std::uint64_t>(entryRouteGradeQ16)
|
||||
>=static_cast<std::uint64_t>(entryVerticalDistance)*65536ull)break;
|
||||
for(int step=1;step<=16;++step)
|
||||
{
|
||||
int directionIndex=static_cast<int>(candidate)
|
||||
+candidateDirection*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
append(ringPoint(static_cast<std::uint32_t>(directionIndex),
|
||||
loops+(step==16?1u:0u)));
|
||||
}
|
||||
++loops;
|
||||
}
|
||||
if(loops>=256)return std::uint64_t{0};
|
||||
const std::int64_t mouthPlanSeparation=distanceQ(
|
||||
lowerMouth[0]-upperMouth[0],lowerMouth[1]-upperMouth[1]);
|
||||
if(!hasLowerTangent&&!anchoredSpiral&&pathStyle==1u&&loops<=1
|
||||
&&mouthPlanSeparation<=2*mouthRadius
|
||||
&&entryVerticalDistance<=2*mouthRadius+qPerMetre/2)
|
||||
{
|
||||
double bestExitScore=-std::numeric_limits<double>::infinity();
|
||||
std::uint32_t lowerPhase=candidate;
|
||||
const auto firstRing=ringPoint(candidate);
|
||||
for(std::uint32_t exitCandidate=0;exitCandidate<16;++exitCandidate)
|
||||
{
|
||||
if(exitCandidate==candidate)continue;
|
||||
const std::uint32_t previous=static_cast<std::uint32_t>(
|
||||
(static_cast<int>(exitCandidate)-candidateDirection+16)%16);
|
||||
const auto terminal=ringPoint(exitCandidate,loops);
|
||||
const auto beforeTerminal=ringPoint(previous,loops);
|
||||
const double incomingX=static_cast<double>(terminal[0]-beforeTerminal[0]);
|
||||
const double incomingY=static_cast<double>(terminal[1]-beforeTerminal[1]);
|
||||
const double outgoingX=static_cast<double>(lowerMouth[0]-terminal[0]);
|
||||
const double outgoingY=static_cast<double>(lowerMouth[1]-terminal[1]);
|
||||
const double incomingLength=std::hypot(incomingX,incomingY);
|
||||
const double outgoingLength=std::hypot(outgoingX,outgoingY);
|
||||
if(incomingLength<=1.0e-6||outgoingLength<=1.0e-6)continue;
|
||||
const double alignment=(incomingX*outgoingX+incomingY*outgoingY)
|
||||
/(incomingLength*outgoingLength);
|
||||
const double separation=std::hypot(
|
||||
static_cast<double>(terminal[0]-firstRing[0]),
|
||||
static_cast<double>(terminal[1]-firstRing[1]));
|
||||
const double score=alignment+0.20*std::clamp(
|
||||
separation/(2.0*qPerMetre),0.0,2.0);
|
||||
if(score>bestExitScore)
|
||||
{bestExitScore=score;lowerPhase=exitCandidate;}
|
||||
}
|
||||
for(int step=1;step<=16;++step)
|
||||
{
|
||||
int directionIndex=static_cast<int>(candidate)
|
||||
+candidateDirection*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
append(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
|
||||
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
|
||||
}
|
||||
}
|
||||
return measure();
|
||||
};
|
||||
for(std::uint32_t offset=0;offset<16;++offset)
|
||||
{
|
||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||
(hashedPhase+16-offset)%16);
|
||||
const auto point=ringPoint(candidate);
|
||||
const double entryX=static_cast<double>(point[0]-upperMouth[0]);
|
||||
const double entryY=static_cast<double>(point[1]-upperMouth[1]);
|
||||
const double entryLength=std::hypot(entryX,entryY);
|
||||
if(entryLength<=1.0e-6)continue;
|
||||
const auto entryDistance=distanceQ(point[0]-upperMouth[0],
|
||||
point[1]-upperMouth[1]);
|
||||
for(const int candidateDirection:{hashedDirection,-hashedDirection})
|
||||
{
|
||||
const std::uint64_t routeLength=candidateRouteLength(candidate,
|
||||
candidateDirection);
|
||||
if(routeLength==0)continue;
|
||||
const std::uint32_t next=static_cast<std::uint32_t>(
|
||||
(static_cast<int>(candidate)+candidateDirection+16)%16);
|
||||
const auto nextPoint=ringPoint(next);
|
||||
const double tangentX=static_cast<double>(nextPoint[0]-point[0]);
|
||||
const double tangentY=static_cast<double>(nextPoint[1]-point[1]);
|
||||
const double tangentLength=std::hypot(tangentX,tangentY);
|
||||
if(tangentLength<=1.0e-6)continue;
|
||||
const double tangentAlignment=(entryX*tangentX+entryY*tangentY)
|
||||
/(entryLength*tangentLength);
|
||||
const std::int64_t firstAxisZ=upperMouth[2]+mouthRadius
|
||||
-playerCenterAboveFloorQ;
|
||||
const std::int64_t secondAxisZ=firstAxisZ+PassageRoundRatio(
|
||||
lowerMouth[2]-upperMouth[2],static_cast<std::int64_t>(entryDistance),
|
||||
static_cast<std::int64_t>(routeLength));
|
||||
double candidateTreadClearance=std::numeric_limits<double>::infinity();
|
||||
const int entrySamples=std::max(1,static_cast<int>(std::ceil(
|
||||
entryLength/(0.25*qPerMetre))));
|
||||
for(int sample=0;sample<=entrySamples;++sample)
|
||||
{
|
||||
const double t=static_cast<double>(sample)/entrySamples;
|
||||
const double qx=upperMouth[0]+t*entryX;
|
||||
const double qy=upperMouth[1]+t*entryY;
|
||||
const double qz=firstAxisZ+t*(secondAxisZ-firstAxisZ)
|
||||
-mouthRadius+0.20*qPerMetre;
|
||||
for(const auto& previousPassage:layout.passages)
|
||||
{
|
||||
if(previousPassage.stableId==passage.stableId
|
||||
||previousPassage.controlPoints.size()<2)continue;
|
||||
for(std::size_t previousSegment=0;
|
||||
previousSegment+1<previousPassage.controlPoints.size();
|
||||
++previousSegment)
|
||||
{
|
||||
const auto& previousA=previousPassage.controlPoints[previousSegment];
|
||||
const auto& previousB=previousPassage.controlPoints[previousSegment+1];
|
||||
const double ax=static_cast<double>(previousA.positionSubvoxels[0]);
|
||||
const double ay=static_cast<double>(previousA.positionSubvoxels[1]);
|
||||
const double az=static_cast<double>(previousA.positionSubvoxels[2]);
|
||||
const double dx=static_cast<double>(previousB.positionSubvoxels[0])-ax;
|
||||
const double dy=static_cast<double>(previousB.positionSubvoxels[1])-ay;
|
||||
const double dz=static_cast<double>(previousB.positionSubvoxels[2])-az;
|
||||
const double horizontalLengthSquared=dx*dx+dy*dy;
|
||||
const double projection=horizontalLengthSquared>0.0
|
||||
?std::clamp(((qx-ax)*dx+(qy-ay)*dy)
|
||||
/horizontalLengthSquared,0.0,1.0):0.0;
|
||||
const double centerX=ax+projection*dx;
|
||||
const double centerY=ay+projection*dy;
|
||||
const double centerZ=az+projection*dz
|
||||
-(previousA.radiusSubvoxels+projection*(
|
||||
static_cast<double>(previousB.radiusSubvoxels)
|
||||
-previousA.radiusSubvoxels))
|
||||
+1.13*qPerMetre;
|
||||
const double vertical=std::max(0.0,
|
||||
std::abs(qz-centerZ)-0.54*qPerMetre);
|
||||
const double clearance=std::sqrt(
|
||||
(qx-centerX)*(qx-centerX)+(qy-centerY)*(qy-centerY)
|
||||
+vertical*vertical)-0.34*qPerMetre;
|
||||
candidateTreadClearance=std::min(candidateTreadClearance,
|
||||
clearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
double previousTreadClearance=std::numeric_limits<double>::infinity();
|
||||
const double entryXQ=entryX,entryYQ=entryY;
|
||||
const double entryZQ=static_cast<double>(secondAxisZ-firstAxisZ);
|
||||
const double entryLengthSquared=entryXQ*entryXQ+entryYQ*entryYQ
|
||||
+entryZQ*entryZQ;
|
||||
const double mouthStartT=std::clamp(
|
||||
mouthRadius/entryLength,0.0,1.0);
|
||||
for(const auto& previousPassage:layout.passages)
|
||||
{
|
||||
if(previousPassage.stableId==passage.stableId
|
||||
||previousPassage.controlPoints.size()<2)continue;
|
||||
for(std::size_t previousSegment=0;
|
||||
previousSegment+1<previousPassage.controlPoints.size();
|
||||
++previousSegment)
|
||||
{
|
||||
const auto& previousA=previousPassage.controlPoints[previousSegment];
|
||||
const auto& previousB=previousPassage.controlPoints[previousSegment+1];
|
||||
const double ax=static_cast<double>(previousA.positionSubvoxels[0]);
|
||||
const double ay=static_cast<double>(previousA.positionSubvoxels[1]);
|
||||
const double az=static_cast<double>(previousA.positionSubvoxels[2]);
|
||||
const double dx=static_cast<double>(previousB.positionSubvoxels[0])-ax;
|
||||
const double dy=static_cast<double>(previousB.positionSubvoxels[1])-ay;
|
||||
const double dz=static_cast<double>(previousB.positionSubvoxels[2])-az;
|
||||
for(int sample=0;sample<=8;++sample)
|
||||
{
|
||||
const double t=static_cast<double>(sample)/8.0;
|
||||
const double px=ax+t*dx;
|
||||
const double py=ay+t*dy;
|
||||
const double pz=az+t*dz
|
||||
-(previousA.radiusSubvoxels+t*(
|
||||
static_cast<double>(previousB.radiusSubvoxels)
|
||||
-previousA.radiusSubvoxels))
|
||||
+0.20*qPerMetre;
|
||||
const double projection=entryLengthSquared>0.0
|
||||
?std::clamp(((px-upperMouth[0])*entryXQ
|
||||
+(py-upperMouth[1])*entryYQ
|
||||
+(pz-firstAxisZ)*entryZQ)/entryLengthSquared,
|
||||
mouthStartT,1.0):mouthStartT;
|
||||
const double ex=px-(upperMouth[0]+projection*entryXQ);
|
||||
const double ey=py-(upperMouth[1]+projection*entryYQ);
|
||||
const double ez=pz-(firstAxisZ+projection*entryZQ);
|
||||
const double clearance=std::sqrt(ex*ex+ey*ey+ez*ez)
|
||||
-mouthRadius;
|
||||
previousTreadClearance=std::min(previousTreadClearance,
|
||||
clearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
const double clearanceScore=std::isfinite(candidateTreadClearance)
|
||||
&&std::isfinite(previousTreadClearance)
|
||||
?std::min(candidateTreadClearance,previousTreadClearance)/qPerMetre
|
||||
:0.0;
|
||||
if(phaseScoreCount<phaseScores.size())
|
||||
phaseScores[phaseScoreCount++]={candidate,candidateDirection,
|
||||
routeLength,clearanceScore,tangentAlignment};
|
||||
}
|
||||
}
|
||||
if(phaseScoreCount>0)
|
||||
{
|
||||
double bestClearance=-std::numeric_limits<double>::infinity();
|
||||
for(std::size_t index=0;index<phaseScoreCount;++index)
|
||||
bestClearance=std::max(bestClearance,phaseScores[index].clearance);
|
||||
constexpr double ClearanceToleranceMetres=0.10;
|
||||
const FPhaseScore* selected=nullptr;
|
||||
for(std::size_t index=0;index<phaseScoreCount;++index)
|
||||
{
|
||||
const auto& candidate=phaseScores[index];
|
||||
if(candidate.clearance+ClearanceToleranceMetres<bestClearance)continue;
|
||||
const bool shorter=!selected||candidate.routeLength<selected->routeLength;
|
||||
const bool stableLengthTie=selected
|
||||
&&candidate.routeLength==selected->routeLength
|
||||
&&(candidate.clearance>selected->clearance+1.0e-6
|
||||
||(std::abs(candidate.clearance-selected->clearance)<=1.0e-6
|
||||
&&candidate.tangent>selected->tangent));
|
||||
if(shorter||stableLengthTie)selected=&candidate;
|
||||
}
|
||||
if(selected)
|
||||
{
|
||||
phase=selected->phase;
|
||||
direction=selected->direction;
|
||||
clockwise=direction>0;
|
||||
}
|
||||
}
|
||||
}
|
||||
direction=clockwise?1:-1;
|
||||
std::vector<std::array<std::int64_t,2>> xy;
|
||||
const auto pushXY=[&](std::array<std::int64_t,2> p)
|
||||
{if(xy.empty()||xy.back()!=p)xy.push_back(p);};
|
||||
@@ -2705,6 +2987,12 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
}
|
||||
++loops;
|
||||
}
|
||||
const std::int64_t mouthPlanSeparation=distanceQ(
|
||||
lowerMouth[0]-upperMouth[0],lowerMouth[1]-upperMouth[1]);
|
||||
const bool shortGeneratedClosure=!hasLowerTangent&&!hasAnchorHelix
|
||||
&&pathStyle==1u&&loops<=1
|
||||
&&mouthPlanSeparation<=2*mouthRadius
|
||||
&&verticalDistance<=2*mouthRadius+qPerMetre/2;
|
||||
if(hasLowerTangent)
|
||||
{
|
||||
std::uint32_t lowerPhase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
@@ -2763,6 +3051,48 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
}
|
||||
pushXY(ringPoint(lowerPhase));
|
||||
}
|
||||
else if(shortGeneratedClosure)
|
||||
{
|
||||
// A one-turn generated route whose mouths share a plan location
|
||||
// must leave and rejoin from different ring tangents. Returning on
|
||||
// the entry chord creates a hairpin that overlaps its own tread.
|
||||
double bestExitScore=-std::numeric_limits<double>::infinity();
|
||||
std::uint32_t lowerPhase=phase;
|
||||
const auto firstRing=ringPoint(phase,loops);
|
||||
for(std::uint32_t candidate=0;candidate<16;++candidate)
|
||||
{
|
||||
if(candidate==phase)continue;
|
||||
const std::uint32_t previous=static_cast<std::uint32_t>(
|
||||
(static_cast<int>(candidate)-direction+16)%16);
|
||||
const auto terminal=ringPoint(candidate,loops);
|
||||
const auto beforeTerminal=ringPoint(previous,loops);
|
||||
const double incomingX=static_cast<double>(terminal[0]-beforeTerminal[0]);
|
||||
const double incomingY=static_cast<double>(terminal[1]-beforeTerminal[1]);
|
||||
const double outgoingX=static_cast<double>(lowerMouth[0]-terminal[0]);
|
||||
const double outgoingY=static_cast<double>(lowerMouth[1]-terminal[1]);
|
||||
const double incomingLength=std::hypot(incomingX,incomingY);
|
||||
const double outgoingLength=std::hypot(outgoingX,outgoingY);
|
||||
if(incomingLength<=1.0e-6||outgoingLength<=1.0e-6)continue;
|
||||
const double tangentAlignment=(incomingX*outgoingX+incomingY*outgoingY)
|
||||
/(incomingLength*outgoingLength);
|
||||
const double separation=std::hypot(
|
||||
static_cast<double>(terminal[0]-firstRing[0]),
|
||||
static_cast<double>(terminal[1]-firstRing[1]));
|
||||
const double score=tangentAlignment
|
||||
+0.20*std::clamp(separation/(2.0*qPerMetre),0.0,2.0);
|
||||
if(score>bestExitScore)
|
||||
{bestExitScore=score;lowerPhase=candidate;}
|
||||
}
|
||||
for(int step=1;step<=16;++step)
|
||||
{
|
||||
int directionIndex=static_cast<int>(phase)+direction*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
|
||||
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
|
||||
}
|
||||
std::printf("[ResolvedPassageGeneratedExitApproach] id=%s turns=%u phase=%u exit_phase=%u tangent_score=%.3f status=PASS\n",
|
||||
passage.stableId.c_str(),loops,phase,lowerPhase,bestExitScore);
|
||||
}
|
||||
pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
|
||||
std::size_t lowerMouthIndex=xy.size()-1;
|
||||
std::vector<std::array<std::int64_t,3>> lowerLandingPoints;
|
||||
@@ -2841,7 +3171,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
for(std::size_t i=0;i<xy.size();++i)
|
||||
{
|
||||
std::int64_t radius=(i<=1||i==lowerMouthIndex
|
||||
||i+1>=xy.size()-1)?mouthRadius:midRadius;
|
||||
||(i>lowerMouthIndex&&i+1>=xy.size()-1))?mouthRadius:midRadius;
|
||||
if(surfaceGroundDeparture&&i<lowerMouthIndex)
|
||||
{
|
||||
const double t=std::clamp(static_cast<double>(cumulative[i])
|
||||
@@ -2850,6 +3180,24 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
radius=surfacePortalRadius+static_cast<std::int64_t>(std::llround(
|
||||
(midRadius-surfacePortalRadius)*smooth));
|
||||
}
|
||||
// Grow the lower bore into its mouth over fixed route distance. Assigning
|
||||
// the full mouth radius to the penultimate point creates a segment-length
|
||||
// flare whose cubic-detrended profile can read as a short-period wall beat.
|
||||
if(i<=lowerMouthIndex)
|
||||
{
|
||||
constexpr std::int64_t LowerMouthRadiusBlendLengthMetres=12;
|
||||
const std::uint64_t blendLength=static_cast<std::uint64_t>(
|
||||
std::max<std::int64_t>(LowerMouthRadiusBlendLengthMetres*qPerMetre,
|
||||
2*mouthRadius));
|
||||
const std::uint64_t distanceToLowerMouth=mouthHorizontal-cumulative[i];
|
||||
if(distanceToLowerMouth<blendLength)
|
||||
{
|
||||
const double t=static_cast<double>(distanceToLowerMouth)/blendLength;
|
||||
const double smooth=t*t*(3.0-2.0*t);
|
||||
radius=midRadius+static_cast<std::int64_t>(std::llround(
|
||||
(mouthRadius-midRadius)*(1.0-smooth)));
|
||||
}
|
||||
}
|
||||
std::int64_t playerZ=0;
|
||||
if(i>lowerMouthIndex)
|
||||
playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2];
|
||||
|
||||
@@ -2162,6 +2162,17 @@ namespace VoxelForgeCore
|
||||
TileMesh out;out.key=key;
|
||||
const auto CopyIslandCacheMetrics=[&]()
|
||||
{
|
||||
if(options.instrumentTileDrain)
|
||||
{
|
||||
const auto& QueryCounts=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||
out.diagnostics.densityPointCalls=QueryCounts.DensityPointCalls;
|
||||
out.diagnostics.densityFloatPointCalls=QueryCounts.DensityFloatPointCalls;
|
||||
out.diagnostics.densityLatticePointCalls=QueryCounts.DensityLatticePointCalls;
|
||||
out.diagnostics.densityUnclampedPointCalls=QueryCounts.DensityUnclampedPointCalls;
|
||||
out.diagnostics.densityX8PointCalls=QueryCounts.DensityX8PointCalls;
|
||||
out.diagnostics.densityFieldEvalNanoseconds=QueryCounts.DensityFieldEvalNanoseconds;
|
||||
out.diagnostics.densityFieldEvalSamplePoints=QueryCounts.DensityFieldEvalSamplePoints;
|
||||
}
|
||||
if(!options.instrumentTileDrain)return;
|
||||
islandCacheInstrumentation.tileCycles=VoxelForgeCoreShared::ReadCpuCycles()
|
||||
-islandCacheInstrumentation.tileStartCycles;
|
||||
@@ -2200,16 +2211,25 @@ namespace VoxelForgeCore
|
||||
line<<" native_"<<CallerNames[i]<<"="<<a.sampledNativeCalls[i]*256<<"/"
|
||||
<<a.sampledNativeCalls[i]<<"/"
|
||||
<<Estimate(a.nativeCycles[i],a.sampledNativeCalls[i]*256,
|
||||
a.sampledNativeCalls[i])
|
||||
<<" native_"<<CallerNames[i]<<"_ns_est="
|
||||
<<Estimate(a.nativeNanoseconds[i],a.sampledNativeCalls[i]*256,
|
||||
a.sampledNativeCalls[i]);
|
||||
line<<" field_"<<CallerNames[i]<<"="<<a.sampledFieldCalls[i]*256<<"/"
|
||||
<<a.sampledFieldCalls[i]<<"/"
|
||||
<<Estimate(a.fieldCycles[i],a.sampledFieldCalls[i]*256,
|
||||
a.sampledFieldCalls[i])
|
||||
<<" field_"<<CallerNames[i]<<"_ns_est="
|
||||
<<Estimate(a.fieldNanoseconds[i],a.sampledFieldCalls[i]*256,
|
||||
a.sampledFieldCalls[i]);
|
||||
}
|
||||
for(std::size_t i=0;i<BucketNames.size();++i)
|
||||
line<<" comp_"<<BucketNames[i]<<"="
|
||||
<<Estimate(a.exclusiveCycles[i],a.scopeSamples[i]*256,
|
||||
a.scopeSamples[i])
|
||||
<<" comp_"<<BucketNames[i]<<"_ns_est="
|
||||
<<Estimate(a.exclusiveNanoseconds[i],a.scopeSamples[i]*256,
|
||||
a.scopeSamples[i])
|
||||
<<"/"<<a.scopeSamples[i]<<"/"<<a.scopeSamples[i]*256;
|
||||
const auto& shared=islandCacheInstrumentation.shared;
|
||||
line<<" local_probe_calls="<<(islandCacheInstrumentation.localHits
|
||||
@@ -2323,6 +2343,7 @@ namespace VoxelForgeCore
|
||||
&&bHasNativeSurfaceHorizonPlan&&!bHasIslandPlanInTile;
|
||||
const bool bSkipFarShellRepair=key.lod>=7&&bUseNativeSurfaceHorizon;
|
||||
FStandaloneShapeWorld::ResetThreadClassificationStats();
|
||||
FStandaloneShapeWorld::SetThreadDensityQueryProfileEnabled(options.instrumentTileDrain);
|
||||
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(
|
||||
options.captureColumnCoordinates);
|
||||
if(options.captureColumnCoordinates)
|
||||
@@ -2335,6 +2356,7 @@ namespace VoxelForgeCore
|
||||
static_cast<std::int64_t>(origin.Y+extent)*SubvoxelsPerVoxel);
|
||||
}
|
||||
bool bNativeSurfaceTile=false;
|
||||
bool bTileProofSkipSafe=false;
|
||||
bool bNativeSurfaceBandSafe=false;
|
||||
double classificationColumnComputeMilliseconds=0.0;
|
||||
if(bHasWorldAssembly)
|
||||
@@ -2342,10 +2364,15 @@ namespace VoxelForgeCore
|
||||
const double columnMillisecondsBeforeClassification=
|
||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
||||
const auto Classification=tileShape.ClassifyBoxDetailed(origin,step,cells);
|
||||
out.diagnostics.tileProofVetoReason=Classification.LegacyProofVeto;
|
||||
out.diagnostics.tileProofSafe=Classification.bSkipSafe;
|
||||
out.diagnostics.tileLegacyBlockProofRelevant=
|
||||
Classification.bLegacyBlockProofRelevant;
|
||||
classificationColumnComputeMilliseconds=std::max(0.0,
|
||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds
|
||||
-columnMillisecondsBeforeClassification);
|
||||
bNativeSurfaceTile=Classification.bNativeSurface;
|
||||
bTileProofSkipSafe=Classification.bSkipSafe;
|
||||
if(options.profileMesherStages)
|
||||
out.diagnostics.nativeSurfaceTile=bNativeSurfaceTile;
|
||||
// NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its
|
||||
@@ -2353,7 +2380,7 @@ namespace VoxelForgeCore
|
||||
// unknown; edits and cross-strate tiles are still excluded below.
|
||||
// NativeSurfaceDensityAt ne consomme que le champ et les volumes bornes du manifeste.
|
||||
bNativeSurfaceBandSafe=bNativeSurfaceTile;
|
||||
if(bNativeSurfaceTile)
|
||||
if(bNativeSurfaceTile||bTileProofSkipSafe)
|
||||
{
|
||||
out.diagnostics.tileWasClassified=true;
|
||||
switch(Classification.bUnknown?EVoxelTileClass::Mixed:Classification.Verdict)
|
||||
@@ -2362,12 +2389,30 @@ namespace VoxelForgeCore
|
||||
case EVoxelTileClass::AllAir:out.diagnostics.tileClass=TileClass::allAir;break;
|
||||
case EVoxelTileClass::Mixed:out.diagnostics.tileClass=TileClass::mixed;break;
|
||||
}
|
||||
if((out.diagnostics.tileClass==TileClass::allSolid
|
||||
if(bTileProofSkipSafe&&(out.diagnostics.tileClass==TileClass::allSolid
|
||||
||out.diagnostics.tileClass==TileClass::allAir)
|
||||
&&!options.hashDensity&&!options.captureDensitySamples&&!bNeedsFarShell
|
||||
&&(!options.hashDensity||options.allowUniformSkipsWhenHashing)
|
||||
&&!options.captureDensitySamples&&!bNeedsFarShell
|
||||
&&!options.auditUniformBlockProofs)
|
||||
{
|
||||
out.diagnostics.meshingSkipped=true;
|
||||
if(options.hashDensity)
|
||||
{
|
||||
const FVoxelMeshData EmptyMesh;
|
||||
out.densityHash=14695981039346656037ull;
|
||||
out.meshHash=HashViewerMesh(EmptyMesh);
|
||||
out.payloadHash=HashPayload(EmptyMesh);
|
||||
out.topologyHash=HashTopology(EmptyMesh);
|
||||
out.materialHash=HashMaterialSamples(out.vertices);
|
||||
if(key.lod>=4)
|
||||
{
|
||||
out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion);
|
||||
out.payloadHash=SaltFarHorizonHash(out.payloadHash,options.farHorizonVersion);
|
||||
out.topologyHash=SaltFarHorizonHash(out.topologyHash,options.farHorizonVersion);
|
||||
}
|
||||
out.hasDensityHash=out.hasMeshHash=out.hasPayloadHash=out.hasTopologyHash=true;
|
||||
out.hasMaterialHash=true;
|
||||
}
|
||||
const auto Counts=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
|
||||
out.diagnostics.solidBoxes=Counts.SolidBoxes;
|
||||
@@ -2401,10 +2446,12 @@ namespace VoxelForgeCore
|
||||
out.densitySampleCount=0;
|
||||
return out;
|
||||
}
|
||||
// Native SurfaceWorld classifies each 8-cell block against its exact sample
|
||||
// lattice. This lets the mesher avoid filling density grids for proven air/rock.
|
||||
mesher.bUseBlockEarlyOut=true;
|
||||
}
|
||||
// Preserve the committed native-field block path. Legacy block certificates
|
||||
// remain audit-only until their cost is repaid against the full-grid path.
|
||||
if(bHasWorldAssembly&&step<=2
|
||||
&&(bNativeSurfaceTile||options.auditUniformBlockProofs))
|
||||
mesher.bUseBlockEarlyOut=true;
|
||||
}
|
||||
const auto MeshingStart=std::chrono::steady_clock::now();
|
||||
int32 MeshBandMinZVoxel=options.bandMinZVoxel;
|
||||
@@ -2620,7 +2667,22 @@ namespace VoxelForgeCore
|
||||
// GenerateMesh n'échantillonne que origin..origin+extent et la marge de normale d'un pas ;
|
||||
// queryMinimum/queryMaximum ci-dessus couvrent exactement ce domaine inclusif.
|
||||
const FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope(
|
||||
tileShape,queryMinimum,queryMaximum);
|
||||
tileShape,queryMinimum,queryMaximum,
|
||||
options.profilePassageComposer?&out.diagnostics:nullptr);
|
||||
if(options.profilePassageComposer)
|
||||
{
|
||||
out.diagnostics.passageComposerProfiled=true;
|
||||
out.diagnostics.passageTileFeatureCount=passageFeatureScope.featureCount();
|
||||
if(impl_->resolvedLayout)
|
||||
{
|
||||
out.diagnostics.passageTileRouteSegmentCandidates=
|
||||
passageFeatureScope.routeSegmentCandidateCount();
|
||||
for(std::size_t passageIndex=0;
|
||||
passageIndex<impl_->resolvedLayout->passages.size();++passageIndex)
|
||||
out.diagnostics.passageTileMouthSegmentCandidates+=
|
||||
passageFeatureScope.mouthSupportSegments(passageIndex).size();
|
||||
}
|
||||
}
|
||||
const bool bDiagnosticColumnSampling=options.diagnosticGlobalOctaveDrop!=0
|
||||
||options.diagnosticColumnInterpolationStride>1;
|
||||
const std::uint32_t PreviousOctaveDrop=bDiagnosticColumnSampling
|
||||
@@ -2959,6 +3021,74 @@ namespace VoxelForgeCore
|
||||
const auto& values=options.captureDensitySamples?out.debugDensitySamples:densityValues;
|
||||
for(const float value:values)
|
||||
{ densityHash^=std::bit_cast<std::uint32_t>(value);densityHash*=1099511628211ull; }
|
||||
|
||||
// A stream-hash run is the full-grid reference path. Record the exact sign
|
||||
// uniformity ceiling from the same captured lattice that feeds Marching Cubes.
|
||||
// Blocks are disjoint sample cubes; partial cubes at tile edges are included.
|
||||
const std::size_t gridWidth=static_cast<std::size_t>(
|
||||
std::clamp(MeshCells,2,CHUNK_SIZE)+3);
|
||||
const std::size_t xySamples=gridWidth*gridWidth;
|
||||
const std::size_t zSamples=xySamples>0&&values.size()%xySamples==0
|
||||
?values.size()/xySamples:0;
|
||||
bool tileHasSign=false,tileUniform=true;
|
||||
bool tileAir=false;
|
||||
for(const float value:values)
|
||||
{
|
||||
if(!std::isfinite(value)||value==0.0f)
|
||||
{tileUniform=false;continue;}
|
||||
const bool air=value>0.0f;
|
||||
if(!tileHasSign){tileHasSign=true;tileAir=air;}
|
||||
else if(tileAir!=air)tileUniform=false;
|
||||
}
|
||||
const auto CountUniformBlocks=[&](std::size_t blockWidth)
|
||||
{
|
||||
std::uint64_t uniformBlocks=0,uniformSamples=0;
|
||||
if(zSamples==0)return std::pair{uniformBlocks,uniformSamples};
|
||||
for(std::size_t z0=0;z0<zSamples;z0+=blockWidth)
|
||||
for(std::size_t y0=0;y0<gridWidth;y0+=blockWidth)
|
||||
for(std::size_t x0=0;x0<gridWidth;x0+=blockWidth)
|
||||
{
|
||||
const std::size_t z1=std::min(zSamples,z0+blockWidth);
|
||||
const std::size_t y1=std::min(gridWidth,y0+blockWidth);
|
||||
const std::size_t x1=std::min(gridWidth,x0+blockWidth);
|
||||
bool hasSign=false,uniform=true,air=false;
|
||||
for(std::size_t z=z0;z<z1&&uniform;++z)
|
||||
for(std::size_t y=y0;y<y1&&uniform;++y)
|
||||
for(std::size_t x=x0;x<x1;++x)
|
||||
{
|
||||
const float value=values[(z*gridWidth+y)*gridWidth+x];
|
||||
if(!std::isfinite(value)||value==0.0f)
|
||||
{uniform=false;break;}
|
||||
const bool sampleAir=value>0.0f;
|
||||
if(!hasSign){hasSign=true;air=sampleAir;}
|
||||
else if(air!=sampleAir){uniform=false;break;}
|
||||
}
|
||||
if(uniform&&hasSign)
|
||||
{
|
||||
++uniformBlocks;
|
||||
uniformSamples+=static_cast<std::uint64_t>(z1-z0)
|
||||
*static_cast<std::uint64_t>(y1-y0)
|
||||
*static_cast<std::uint64_t>(x1-x0);
|
||||
}
|
||||
}
|
||||
return std::pair{uniformBlocks,uniformSamples};
|
||||
};
|
||||
const auto [uniformBlocks8,uniformSamples8]=CountUniformBlocks(8);
|
||||
const auto [uniformBlocks4,uniformSamples4]=CountUniformBlocks(4);
|
||||
const double centerZ=(static_cast<double>(origin.Z)
|
||||
+static_cast<double>(extent)*0.5)*static_cast<double>(world.CoreVoxelSizeMicrons)
|
||||
/10000.0;
|
||||
const int32 stratum=world.Manager
|
||||
?world.Manager->GetStrateIndex(static_cast<float>(centerZ)):-1;
|
||||
std::printf("[UniformGridCeiling] strate=%d lod=%d key=(%d,%d,%d) samples=%zu uniform=%d air=%d blocks8=%llu samples8=%llu blocks4=%llu samples4=%llu proof_safe=%d proof_veto=%u\n",
|
||||
stratum,key.lod,key.coordinate.x,key.coordinate.y,key.coordinate.z,values.size(),
|
||||
tileUniform&&tileHasSign?1:0,tileUniform&&tileHasSign&&tileAir?1:0,
|
||||
static_cast<unsigned long long>(uniformBlocks8),
|
||||
static_cast<unsigned long long>(uniformSamples8),
|
||||
static_cast<unsigned long long>(uniformBlocks4),
|
||||
static_cast<unsigned long long>(uniformSamples4),
|
||||
out.diagnostics.tileProofSafe?1:0,
|
||||
static_cast<unsigned>(out.diagnostics.tileProofVetoReason));
|
||||
out.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh);
|
||||
out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh);
|
||||
out.materialHash=HashMaterialSamples(out.vertices);
|
||||
|
||||
@@ -394,9 +394,9 @@ VF_FIELD_INLINE float VF_FieldOwnerCellular(float x,float y,float z)
|
||||
{
|
||||
const VF_FieldI32 nx=cellX+dx,ny=cellY+dy,nz=cellZ+dz;
|
||||
const VF_FieldU32 h=VF_FieldVoxelHashMix(
|
||||
VF_FIELD_CAST_U32(nx+0x7FFFFFFF)
|
||||
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(ny+0x7FFFFFFF)*2654435761u)
|
||||
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(nz+0x7FFFFFFF)*374761393u));
|
||||
(VF_FIELD_CAST_U32(nx)+0x7FFFFFFFu)
|
||||
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(ny)+0x7FFFFFFFu)*2654435761u)
|
||||
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(nz)+0x7FFFFFFFu)*374761393u));
|
||||
const float fpx=VF_FIELD_CAST_F32(dx)
|
||||
+VF_FIELD_CAST_F32(h&0xFFFFFFu)*(1.0f/16777216.0f)-fracX;
|
||||
const VF_FieldU32 hy=VF_FieldVoxelHashMix(h^0x12345678u);
|
||||
|
||||
@@ -4,9 +4,11 @@ import argparse
|
||||
import importlib.util
|
||||
import math
|
||||
from collections import defaultdict, deque
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")) / "baselines"
|
||||
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH)
|
||||
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||
@@ -502,8 +504,8 @@ def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=ROOT / "Saved/Linux/l37")
|
||||
parser.add_argument("--baseline-out", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/no-tread-control")
|
||||
parser.add_argument("--control-out", type=Path, default=ROOT / "Saved/Linux/l36")
|
||||
default=BASELINES / "mouth/l37-no-tread")
|
||||
parser.add_argument("--control-out", type=Path, default=BASELINES / "mouth/l36")
|
||||
parser.add_argument("--scene", choices=tuple(SCENES) + ("all",), default="all")
|
||||
args = parser.parse_args()
|
||||
out = args.out if args.out.is_absolute() else ROOT / args.out
|
||||
|
||||
@@ -15,6 +15,7 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT",
|
||||
os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")))
|
||||
BASELINES = OUT / "baselines"
|
||||
BIN = OUT / "bin"
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||
OUT / "captures"))
|
||||
@@ -23,10 +24,12 @@ HEADLESS = Path(os.environ.get("VOXELFORGE_L37_HEADLESS",
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||
BIN / "VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||
PNG_DIR = OUT / "capture-png"
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
RECIPE_INPUTS = (
|
||||
("owner-fi", ROOT / "Standalone/data/owner_world.json",
|
||||
("owner-fi", OWNER,
|
||||
"floating-islands", "FloatingIslands", "upper"),
|
||||
("owner-sw", ROOT / "Standalone/data/owner_world.json",
|
||||
("owner-sw", OWNER,
|
||||
"surface-world", "SurfaceWorld", "lower"),
|
||||
("style-override-upper-sw-fixture-style0",
|
||||
OUT / "fixtures/style-override-upper-sw-fixture-style0.json",
|
||||
@@ -136,7 +139,7 @@ def fmt(values):
|
||||
|
||||
|
||||
def load_reference_cameras():
|
||||
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
log = BASELINES / "capture/l34-standard-set-cameras.baseline"
|
||||
pattern = re.compile(
|
||||
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"""Run five interleaved whole-view cost pairs against the L36 Clang binary."""
|
||||
import csv
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
@@ -10,6 +11,8 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
BINARIES = {
|
||||
"l36": ROOT / "Saved/Linux/l36/bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"l37": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
@@ -30,7 +33,7 @@ for recipe in ("surface-world", "floating-islands"):
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(BINARIES[build]),
|
||||
str(ROOT / "Standalone/data/owner_world.json"),
|
||||
str(OWNER),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check L25, 24 m crossing and upper-SW fixture determinism for L37."""
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
@@ -9,7 +10,8 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||
import re
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -48,7 +49,8 @@ def run(name, binary, data, recipe, extra, checks):
|
||||
failed |= not ok
|
||||
|
||||
|
||||
owner = ROOT / "Standalone/data/owner_world.json"
|
||||
owner = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
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}"
|
||||
|
||||
@@ -8,9 +8,11 @@ import json
|
||||
import math
|
||||
import re
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")) / "baselines"
|
||||
BASE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37/RunMouthFragmentAuditL37.py"
|
||||
SPEC = importlib.util.spec_from_file_location("mouth_fragment_l37", BASE_PATH)
|
||||
BASE = importlib.util.module_from_spec(SPEC)
|
||||
@@ -184,8 +186,8 @@ def main():
|
||||
parser.add_argument("--capture-log-sw", type=Path)
|
||||
args = parser.parse_args()
|
||||
paths = {
|
||||
"L35": ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh",
|
||||
"L36": ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh",
|
||||
"L35": BASELINES / "mouth/l35/owner-fi-ground-mouth.vfmesh",
|
||||
"L36": BASELINES / "mouth/l36/owner-fi-ground-mouth.vfmesh",
|
||||
"L37": args.candidate if args.candidate.is_absolute()
|
||||
else ROOT / args.candidate,
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37"))
|
||||
BASELINES = OUT / "baselines"
|
||||
BIN = Path(os.environ.get("VOXELFORGE_VERIFY_HEADLESS",
|
||||
OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang"))
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
TERRACE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
RING_SET = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py"
|
||||
STAMP = re.compile(
|
||||
@@ -176,9 +178,9 @@ for label, recipe, surface, window_min, window_max, camera0, camera1, centers in
|
||||
|
||||
# Keep the established L30 terrace and L31 flat-surface negative controls live.
|
||||
controls = (
|
||||
("l30-terrace", ROOT / "Saved/Linux/l32/fi-floor-l30-camera-negative.vfmesh",
|
||||
("l30-terrace", BASELINES / "ring/l32-fi-floor-l30-camera-negative.vfmesh",
|
||||
-130.0, -110.0),
|
||||
("l31-flat", ROOT / "Saved/Linux/l31/fi-floor-v19.vfmesh", -122.0, -117.0),
|
||||
("l31-flat", BASELINES / "ring/l31-fi-floor-v19.vfmesh", -122.0, -117.0),
|
||||
)
|
||||
for name, mesh, height_min, height_max in (() if OPTIONS.skip_controls else controls):
|
||||
result, output, _ = run(f"control-{name}", ["python3", TERRACE, mesh,
|
||||
|
||||
@@ -14,6 +14,7 @@ import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l47")) / "baselines"
|
||||
VERIFY = ROOT / "Standalone/Tools/verify"
|
||||
LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37"
|
||||
sys.path.insert(0, str(VERIFY))
|
||||
@@ -177,8 +178,8 @@ if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
|
||||
entrance_audit = ROOT / "Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py"
|
||||
run_logged("entrance-floor-control-l46", [sys.executable, str(entrance_audit),
|
||||
"--mesh", str(ROOT / "Saved/Linux/l46/landmark-l46.vfmesh"),
|
||||
"--layout", str(ROOT / "Saved/Linux/l46/layouts/landmark-reachability-clang.json"),
|
||||
"--mesh", str(BASELINES / "landmarks/l46-entrance-control.vfmesh"),
|
||||
"--layout", str(BASELINES / "landmarks/l46-entrance-control-layout.json"),
|
||||
"--out", str(ARTIFACT_DIR / "entrance-profile-control-l46.json"), "--expect=fail"])
|
||||
run_logged("entrance-floor-audit", [sys.executable, str(entrance_audit),
|
||||
"--mesh", str(mesh), "--layout", str(LAYOUT_PATH),
|
||||
|
||||
@@ -6,11 +6,13 @@ import importlib.util
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l47")) / "baselines"
|
||||
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
spec = importlib.util.spec_from_file_location("ring_profile", PROFILE_PATH)
|
||||
if spec is None or spec.loader is None:
|
||||
@@ -21,9 +23,9 @@ spec.loader.exec_module(PROFILE)
|
||||
ARTIFACTS = ROOT / "Saved/Linux/l47"
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--layout", type=Path,
|
||||
default=ROOT / "Saved/Linux/l46/layouts/landmark-reachability-clang.json")
|
||||
default=BASELINES / "landmarks/l46-entrance-control-layout.json")
|
||||
PARSER.add_argument("--mesh", type=Path,
|
||||
default=ROOT / "Saved/Linux/l46/landmark-l46.vfmesh")
|
||||
default=BASELINES / "landmarks/l46-entrance-control.vfmesh")
|
||||
PARSER.add_argument("--out", type=Path, default=ARTIFACTS / "entrance-profile-before.json")
|
||||
PARSER.add_argument("--expect", choices=("pass", "fail", "measure"), default="measure")
|
||||
PARSER.add_argument("--lod", default="0", help="LOD level, or 'all' to include every exported level")
|
||||
|
||||
@@ -9,7 +9,8 @@ from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l56"))
|
||||
|
||||
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L54's triangle audit over L59's open-shaft lattice and controls."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
AUDIT54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/AuditWholeSurfaceL54.py"
|
||||
spec = importlib.util.spec_from_file_location("whole_surface_l54", AUDIT54)
|
||||
audit54 = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(audit54)
|
||||
original_make_route = audit54.make_route
|
||||
|
||||
|
||||
def build_segment_bvh(segments: list[tuple]) -> dict:
|
||||
def build(indices: list[int]) -> dict:
|
||||
minimum = tuple(min(min(segments[index][0][axis], segments[index][1][axis])
|
||||
for index in indices) for axis in range(3))
|
||||
maximum = tuple(max(max(segments[index][0][axis], segments[index][1][axis])
|
||||
for index in indices) for axis in range(3))
|
||||
if len(indices) <= 8:
|
||||
return {"minimum": minimum, "maximum": maximum,
|
||||
"segments": tuple(sorted(indices)), "left": None, "right": None}
|
||||
axis = max(range(3), key=lambda value: maximum[value] - minimum[value])
|
||||
indices.sort(key=lambda index: (segments[index][0][axis]
|
||||
+ segments[index][1][axis]) * 0.5)
|
||||
middle = len(indices) // 2
|
||||
return {"minimum": minimum, "maximum": maximum, "segments": None,
|
||||
"left": build(indices[:middle]), "right": build(indices[middle:])}
|
||||
|
||||
if not segments:
|
||||
raise ValueError("lattice audit route has no measurable segments")
|
||||
return build(list(range(len(segments))))
|
||||
|
||||
|
||||
def make_lattice_route(layout: dict, phase_m: float = 0.0) -> dict:
|
||||
edges = sorted((edge for edge in layout.get("cavern_edges", [])
|
||||
if edge["stable_id"] in ("lattice/spine/00-arrival-portal-v1",
|
||||
"lattice/spine/01-helix-ramp-v1", "lattice/spine/99-departure-portal-v1")
|
||||
or edge["stable_id"].startswith("lattice/spine/maze-edge-")),
|
||||
key=lambda edge: (0 if edge["stable_id"].startswith("lattice/spine/00-") else
|
||||
1 if edge["stable_id"].startswith("lattice/spine/01-") else
|
||||
2 if edge["stable_id"].startswith("lattice/spine/maze-edge-") else 3,
|
||||
edge["stable_id"]))
|
||||
if not edges:
|
||||
raise ValueError("lattice audit needs its resolved mandatory spine edges")
|
||||
segments = []
|
||||
routes = []
|
||||
for edge_index, edge in enumerate(edges):
|
||||
points = [[value / 1_000_000.0 for value in row]
|
||||
for row in edge.get("polyline_um", [])]
|
||||
if len(points) < 2:
|
||||
raise ValueError(f"{edge['stable_id']}: explicit path is missing")
|
||||
radius = edge["bore_radius_um"] / 1_000_000.0
|
||||
height = edge["bore_height_um"] / 1_000_000.0
|
||||
support_depth = edge.get("floor_support_depth_um", 0) / 1_000_000.0
|
||||
horizontal_s = [0.0]
|
||||
for a, b in zip(points, points[1:]):
|
||||
horizontal_s.append(horizontal_s[-1] + ((b[0] - a[0]) ** 2
|
||||
+ (b[1] - a[1]) ** 2) ** 0.5)
|
||||
first = points[1]
|
||||
first_dx, first_dy = first[0] - points[0][0], first[1] - points[0][1]
|
||||
first_length = max(1.0e-9, (first_dx * first_dx + first_dy * first_dy) ** 0.5)
|
||||
extension = (points[0][0] - first_dx / first_length * audit54.OPEN_GROUND_EXTENSION_M,
|
||||
points[0][1] - first_dy / first_length * audit54.OPEN_GROUND_EXTENSION_M,
|
||||
points[0][2] - (first[2] - points[0][2]) / first_length
|
||||
* audit54.OPEN_GROUND_EXTENSION_M)
|
||||
route_points = [extension, *points]
|
||||
route_s = [-audit54.OPEN_GROUND_EXTENSION_M, *horizontal_s]
|
||||
for index, (a, b) in enumerate(zip(route_points, route_points[1:])):
|
||||
segments.append((a, b, route_s[index], route_s[index + 1],
|
||||
radius, height, edge_index))
|
||||
routes.append({"edge": edge, "points": points, "radius": radius,
|
||||
"height": height, "support_depth": support_depth,
|
||||
"length": horizontal_s[-1], "index": edge_index})
|
||||
# The shaft core is an open cylindrical volume, not a walkable room. Its
|
||||
# axis-aligned bounds include unrelated rock outside the cylinder and
|
||||
# above the ramp headroom; the helix corridor already includes the inner
|
||||
# wall, lip, foot, and underside where those surfaces meet the route.
|
||||
rooms = [{"room": room,
|
||||
"center": tuple(value / 1_000_000.0 for value in room["center_um"]),
|
||||
"half": tuple(value / 1_000_000.0 for value in room["half_extents_um"]),
|
||||
"floor": room["floor_um"] / 1_000_000.0}
|
||||
for room in sorted(layout.get("cavern_rooms", []), key=lambda row: row["stable_id"])
|
||||
if room.get("stable_id") != "lattice/spine/shaft-core-v2"]
|
||||
return {"segments": segments, "edges": routes, "rooms": rooms, "phase_m": phase_m,
|
||||
"segment_bvh": build_segment_bvh(segments)}
|
||||
|
||||
|
||||
def make_route(layout: dict, phase_m: float = 0.0) -> dict:
|
||||
if any(edge.get("stable_id", "").startswith("lattice/")
|
||||
for edge in layout.get("cavern_edges", [])):
|
||||
return make_lattice_route(layout, phase_m)
|
||||
return original_make_route(layout, phase_m)
|
||||
|
||||
|
||||
def lattice_route_nearest(point: tuple[float, float, float], route: dict):
|
||||
"""Resolve stacked helix turns in 3D, then measure corridor radius in XY."""
|
||||
best = None
|
||||
def consider(segment_index: int) -> None:
|
||||
nonlocal best
|
||||
a, b, s0, s1, radius, height, edge_index = route["segments"][segment_index]
|
||||
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
||||
length_squared = dx * dx + dy * dy + dz * dz
|
||||
t = 0.0 if length_squared <= 1.0e-12 else max(0.0, min(1.0,
|
||||
((point[0] - a[0]) * dx + (point[1] - a[1]) * dy
|
||||
+ (point[2] - a[2]) * dz) / length_squared))
|
||||
x, y, z = (a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
||||
distance3d_squared = ((point[0] - x) ** 2 + (point[1] - y) ** 2
|
||||
+ (point[2] - z) ** 2)
|
||||
if (best is None or distance3d_squared < best[0]
|
||||
or (distance3d_squared == best[0] and segment_index < best[1])):
|
||||
horizontal_length = math.hypot(dx, dy)
|
||||
lateral = ((dx * (point[1] - y) - dy * (point[0] - x))
|
||||
/ horizontal_length if horizontal_length > 1.0e-9 else 0.0)
|
||||
best = (distance3d_squared, segment_index,
|
||||
math.hypot(point[0] - x, point[1] - y),
|
||||
s0 + (s1 - s0) * t, z, lateral, radius, height, edge_index,
|
||||
route["edges"][edge_index]["length"])
|
||||
|
||||
def lower_bound_squared(node: dict) -> float:
|
||||
distance_squared = 0.0
|
||||
for axis in range(3):
|
||||
gap = (node["minimum"][axis] - point[axis]
|
||||
if point[axis] < node["minimum"][axis]
|
||||
else point[axis] - node["maximum"][axis]
|
||||
if point[axis] > node["maximum"][axis] else 0.0)
|
||||
distance_squared += gap * gap
|
||||
return distance_squared
|
||||
|
||||
def visit(node: dict) -> None:
|
||||
if best is not None and lower_bound_squared(node) > best[0]:
|
||||
return
|
||||
if node["segments"] is not None:
|
||||
for segment_index in node["segments"]:
|
||||
consider(segment_index)
|
||||
return
|
||||
left, right = node["left"], node["right"]
|
||||
left_bound, right_bound = lower_bound_squared(left), lower_bound_squared(right)
|
||||
if left_bound <= right_bound:
|
||||
visit(left)
|
||||
visit(right)
|
||||
else:
|
||||
visit(right)
|
||||
visit(left)
|
||||
|
||||
visit(route["segment_bvh"])
|
||||
if best is None:
|
||||
raise ValueError("lattice audit route has no measurable segments")
|
||||
return best[2:]
|
||||
|
||||
|
||||
def mark_lattice_region(triangles: list, route: dict) -> None:
|
||||
margin = audit54.CORRIDOR_MARGIN_M
|
||||
extension = audit54.OPEN_GROUND_EXTENSION_M
|
||||
bounds = []
|
||||
for edge in route["edges"]:
|
||||
points = edge["points"]
|
||||
radius, height = edge["radius"], edge["height"]
|
||||
support_depth = edge["support_depth"]
|
||||
bounds.append((
|
||||
min(point[0] for point in points) - radius - margin,
|
||||
max(point[0] for point in points) + radius + margin,
|
||||
min(point[1] for point in points) - radius - margin,
|
||||
max(point[1] for point in points) + radius + margin,
|
||||
min(point[2] for point in points) - support_depth - margin,
|
||||
max(point[2] for point in points) + height + margin))
|
||||
for room in route["rooms"]:
|
||||
center, half, floor = room["center"], room["half"], room["floor"]
|
||||
bounds.append((center[0] - half[0] - margin, center[0] + half[0] + margin,
|
||||
center[1] - half[1] - margin, center[1] + half[1] + margin,
|
||||
floor - margin, floor + 2.0 * half[2] + margin))
|
||||
|
||||
for triangle in triangles:
|
||||
x, y, z = triangle.center
|
||||
if not any(low_x <= x <= high_x and low_y <= y <= high_y
|
||||
and low_z <= z <= high_z
|
||||
for low_x, high_x, low_y, high_y, low_z, high_z in bounds):
|
||||
triangle.in_region = False
|
||||
triangle.reference = False
|
||||
continue
|
||||
nearest = lattice_route_nearest(triangle.center, route)
|
||||
distance, distance_s, floor_z, lateral, radius, height, edge_index, edge_length = nearest
|
||||
support_depth = route["edges"][edge_index]["support_depth"]
|
||||
corridor = (-extension <= distance_s <= edge_length
|
||||
and distance <= radius + margin
|
||||
and floor_z - support_depth - margin <= z <= floor_z + height + margin)
|
||||
room_box = any(abs(x - room["center"][0]) <= room["half"][0] + margin
|
||||
and abs(y - room["center"][1]) <= room["half"][1] + margin
|
||||
and room["floor"] - margin <= z
|
||||
<= room["floor"] + 2.0 * room["half"][2] + margin
|
||||
for room in route["rooms"])
|
||||
triangle.in_region = corridor or room_box
|
||||
inside_room = any(abs(x - room["center"][0]) <= room["half"][0] + 0.45
|
||||
and abs(y - room["center"][1]) <= room["half"][1] + 0.45
|
||||
for room in route["rooms"])
|
||||
outside_bore = distance > radius + 0.45 and not inside_room
|
||||
triangle.reference = (triangle.in_region and outside_bore
|
||||
and triangle.smooth[2] >= 0.45)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--case", nargs=5, action="append", required=True,
|
||||
metavar=("LABEL", "MESH", "LAYOUT_OR_DASH", "EXPECT", "FOCUS"))
|
||||
parser.add_argument("--json", type=Path, required=True)
|
||||
parser.add_argument("--svg-dir", type=Path)
|
||||
parser.add_argument("--phase", type=float, choices=(-0.5, 0.0, 0.5), default=0.0)
|
||||
args = parser.parse_args()
|
||||
audit54.make_route = make_route
|
||||
results = []
|
||||
for label, mesh, layout, expect, focus in args.case:
|
||||
if expect not in ("pass", "fail", "measure") or focus not in ("whole", "floor"):
|
||||
parser.error("EXPECT must be pass/fail/measure and FOCUS must be whole/floor")
|
||||
native_lattice = layout != "-" and any(
|
||||
edge.get("stable_id", "").startswith("lattice/")
|
||||
for edge in json.loads(Path(layout).read_text(encoding="utf-8")).get(
|
||||
"cavern_edges", []))
|
||||
original_margin = audit54.CORRIDOR_MARGIN_M
|
||||
original_extension = audit54.OPEN_GROUND_EXTENSION_M
|
||||
original_nearest = audit54.route_nearest
|
||||
original_mark_region = audit54.mark_region
|
||||
if native_lattice:
|
||||
# Keep the whole-surface test on native records: one core voxel of
|
||||
# tolerance captures their meshed walls while excluding the separate
|
||||
# legacy transition above the resolved mouth.
|
||||
audit54.CORRIDOR_MARGIN_M = 0.25
|
||||
audit54.OPEN_GROUND_EXTENSION_M = 0.0
|
||||
audit54.route_nearest = lattice_route_nearest
|
||||
audit54.mark_region = mark_lattice_region
|
||||
try:
|
||||
results.append(audit54.audit_case(label, Path(mesh), Path(layout), expect,
|
||||
focus, args.svg_dir, args.phase))
|
||||
finally:
|
||||
audit54.CORRIDOR_MARGIN_M = original_margin
|
||||
audit54.OPEN_GROUND_EXTENSION_M = original_extension
|
||||
audit54.route_nearest = original_nearest
|
||||
audit54.mark_region = original_mark_region
|
||||
if native_lattice:
|
||||
print("[L59LatticeAuditScope] margin_m=0.250 ramp_underside_depth_m=1.000 "
|
||||
"outside_mouth_extension_m=0.000 nearest_centerline=3D "
|
||||
"radius_measure=horizontal status=PASS")
|
||||
args.json.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.json.write_text(json.dumps({"audit": "whole-surface-open-shaft-l59-v1",
|
||||
"cases": results}, indent=2) + "\n", encoding="utf-8")
|
||||
failures = [result for result in results if result["status"] != "PASS"]
|
||||
print(f"[L59WholeSurfaceAuditSummary] cases={len(results)} failed={len(failures)} "
|
||||
f"json={args.json} status={'FAIL' if failures else 'PASS'}")
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Split the owner surface strate around the L59 native lattice fixture."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--owner", type=Path, required=True)
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = json.loads(args.owner.read_text(encoding="utf-8"))
|
||||
strates = root.get("strates")
|
||||
if not isinstance(strates, list):
|
||||
raise ValueError("owner input has no strates array")
|
||||
by_index = {int(row.get("index", -1)): row for row in strates if isinstance(row, dict)}
|
||||
if set(by_index) != {0, 1} or len(strates) != 2:
|
||||
raise ValueError("fixture requires the reviewed two-strate owner input")
|
||||
upper, surface = by_index[0], by_index[1]
|
||||
if (int(upper.get("asset", {}).get("GeneratorType", -1)) != 0
|
||||
or int(surface.get("asset", {}).get("GeneratorType", -1)) != 4):
|
||||
raise ValueError("fixture requires TunnelNetwork over SurfaceWorld")
|
||||
if int(upper.get("passage_config", {}).get("Connections", -1)) != 1:
|
||||
raise ValueError("owner arrival passage count differs from the reviewed input")
|
||||
|
||||
# Keep both resolved mouths in the small synthetic Maze layer. The authored
|
||||
# owner reach ranges are intentionally broad and can place a synthetic mouth
|
||||
# hundreds of metres beyond a four-chunk prototype layer.
|
||||
upper["passage_config"].update({"ReachMin": 32, "ReachMax": 32})
|
||||
upper["asset"]["PassageConfig"].update({"ReachMin": 32, "ReachMax": 32})
|
||||
|
||||
maze = json.loads(json.dumps(surface))
|
||||
maze.update({"index": 1, "top_chunk_z": -16, "bottom_chunk_z": -19,
|
||||
"height_chunks": 4})
|
||||
maze["asset"].update({"StrateName": "L59 Native Lattice Maze",
|
||||
"GeneratorType": 3, "StrateHeightInChunks": 4})
|
||||
maze["asset"].setdefault("MazeParams", {})["BoundarySealThickness"] = 8
|
||||
maze["passage_config"]["Connections"] = 1
|
||||
maze["passage_config"].update({"ReachMin": 32, "ReachMax": 32})
|
||||
maze["asset"]["PassageConfig"].update({"Connections": 1, "ReachMin": 32, "ReachMax": 32})
|
||||
|
||||
lower = json.loads(json.dumps(surface))
|
||||
lower.update({"index": 2, "top_chunk_z": -20, "bottom_chunk_z": -23,
|
||||
"height_chunks": 4})
|
||||
lower["asset"].update({"StrateName": "L59 Lower Surface", "GeneratorType": 4,
|
||||
"StrateHeightInChunks": 4})
|
||||
lower["passage_config"]["Connections"] = 1
|
||||
lower["asset"]["PassageConfig"]["Connections"] = 1
|
||||
|
||||
root["total_strates"] = 3
|
||||
root["strates"] = [upper, maze, lower]
|
||||
# Keep the legacy passage portals proportional to this compact native
|
||||
# prototype; the owner world remains at its authored scale.
|
||||
root.update({"owner_voxel_size_cm": 6.25,
|
||||
"owner_voxel_size_microns": 62500,
|
||||
"core_voxel_size_cm": 25,
|
||||
"core_voxel_size_microns": 250000,
|
||||
"owner_to_core_length_scale": 0.25})
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(json.dumps(root, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
print(f"[LatticeFixtureBuild] source={args.owner} source_sha256={sha256(args.owner)} "
|
||||
f"fixture={args.out} fixture_sha256={sha256(args.out)} "
|
||||
"strates=0:TunnelNetwork,1:Maze[4 chunks],2:SurfaceWorld[60 chunks] "
|
||||
"owner_input_untouched=true status=PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,179 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export the L59 open-shaft lattice and render four reviewed viewpoints."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
CAPTURE54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/CaptureNativeCavesL54.py"
|
||||
spec = importlib.util.spec_from_file_location("capture_native_caves_l54", CAPTURE54)
|
||||
capture54 = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(capture54)
|
||||
run_step = capture54.run_step
|
||||
camera_angles = capture54.camera_angles
|
||||
filter_mesh_y_leq = capture54.filter_mesh_y_leq
|
||||
fmt = capture54.fmt
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def midpoint(points: list[list[float]]) -> tuple[float, float, float]:
|
||||
return tuple(sum(point[axis] for point in points) / len(points) for axis in range(3))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
parser.add_argument("--fixture", type=Path, required=True)
|
||||
parser.add_argument("--layout", type=Path, required=True)
|
||||
parser.add_argument("--headless", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
parser.add_argument("--artifact-dir", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
out = args.out.resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
capture_dir = out / "lattice-capture-png"
|
||||
mesh_dir = out / "lattice-meshes"
|
||||
for directory in (capture_dir, mesh_dir, artifact_dir):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
layout = json.loads(args.layout.read_text(encoding="utf-8"))
|
||||
edges = sorted(layout.get("cavern_edges", []), key=lambda row: row["stable_id"])
|
||||
helix = next((edge for edge in edges
|
||||
if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1"), None)
|
||||
shaft_core = next((room for room in layout.get("cavern_rooms", [])
|
||||
if room["stable_id"] == "lattice/spine/shaft-core-v2"), None)
|
||||
maze_edges = [edge for edge in edges
|
||||
if edge["stable_id"].startswith("lattice/spine/maze-edge-")]
|
||||
if helix is None or shaft_core is None or len(maze_edges) < 4:
|
||||
raise ValueError("layout is missing the resolved open shaft, helix, or maze spine")
|
||||
helix_points = [[value / 1_000_000.0 for value in point]
|
||||
for point in helix["polyline_um"]]
|
||||
core_center = [value / 1_000_000.0 for value in shaft_core["center_um"]]
|
||||
shaft_center = (core_center[0], core_center[1], core_center[2])
|
||||
top = helix_points[0]
|
||||
bottom = helix_points[-1]
|
||||
whole_mesh = mesh_dir / "lattice-whole-view.vfmesh"
|
||||
export_layout = out / "layouts" / "lattice-capture-layout.json"
|
||||
env = os.environ.copy()
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out / "xdg-cache")
|
||||
export_camera = (top[0], top[1], top[2] + 1.5, 0.0, -8.0)
|
||||
run_step("lattice-whole-view-export", [args.headless, args.fixture,
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=4",
|
||||
"--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0",
|
||||
"--recipe=lattice", "--lattice-strate=1",
|
||||
f"--camera={fmt(export_camera)}", f"--resolved-layout-output={export_layout}",
|
||||
f"--mesh-export={whole_mesh}"], out / "capture-lattice-export.log",
|
||||
env=env, stamped=True)
|
||||
if not whole_mesh.is_file() or whole_mesh.stat().st_size == 0:
|
||||
raise RuntimeError("whole-lattice export produced no mesh")
|
||||
|
||||
cutaway_mesh = mesh_dir / "lattice-cutaway-y-center.vfmesh"
|
||||
cutaway_triangles = filter_mesh_y_leq(whole_mesh, cutaway_mesh, shaft_center[1])
|
||||
print(f"[L59LatticeCutaway] plane_y_m={shaft_center[1]:.3f} "
|
||||
f"retained_triangles={cutaway_triangles} mesh={cutaway_mesh}", flush=True)
|
||||
|
||||
# Start inside the open shaft, offset toward the first tread so the view
|
||||
# follows the well down while keeping the helical ledges in frame.
|
||||
start_dx = top[0] - shaft_center[0]
|
||||
start_dy = top[1] - shaft_center[1]
|
||||
start_radius = max(1.0e-9, math.hypot(start_dx, start_dy))
|
||||
top_camera = (shaft_center[0] + start_dx / start_radius * 3.5,
|
||||
shaft_center[1] + start_dy / start_radius * 3.5,
|
||||
top[2] + 1.6)
|
||||
top_target = (shaft_center[0], shaft_center[1], bottom[2] + 1.3)
|
||||
eye_index = len(helix_points) // 2
|
||||
eye_floor = helix_points[eye_index]
|
||||
eye_dx = eye_floor[0] - shaft_center[0]
|
||||
eye_dy = eye_floor[1] - shaft_center[1]
|
||||
eye_radius = max(1.0e-9, math.hypot(eye_dx, eye_dy))
|
||||
# Put the eye on the walk line, looking across the open core at the far
|
||||
# wall and the ledges above and below.
|
||||
eye_camera = (eye_floor[0], eye_floor[1], eye_floor[2] + 1.35)
|
||||
eye_target = (shaft_center[0], shaft_center[1], eye_camera[2] + 0.35)
|
||||
maze_edge = next(edge for edge in maze_edges
|
||||
if edge["stable_id"] == "lattice/spine/maze-edge-02-v1")
|
||||
maze_points = [[value / 1_000_000.0 for value in point]
|
||||
for point in maze_edge["polyline_um"]]
|
||||
maze_a, maze_b = maze_points[0], maze_points[-1]
|
||||
maze_t = 0.5
|
||||
maze_floor = tuple(maze_a[axis] + (maze_b[axis] - maze_a[axis]) * maze_t
|
||||
for axis in range(3))
|
||||
maze_horizontal = max(1.0e-9, math.hypot(maze_b[0] - maze_a[0],
|
||||
maze_b[1] - maze_a[1]))
|
||||
maze_camera = (maze_floor[0] - (maze_b[1] - maze_a[1]) / maze_horizontal * 0.2,
|
||||
maze_floor[1] + (maze_b[0] - maze_a[0]) / maze_horizontal * 0.2,
|
||||
maze_floor[2] + 1.4)
|
||||
maze_target = (maze_camera[0] + (maze_b[0] - maze_a[0]) / maze_horizontal * 8.0,
|
||||
maze_camera[1] + (maze_b[1] - maze_a[1]) / maze_horizontal * 8.0,
|
||||
maze_camera[2] + (maze_b[2] - maze_a[2]) / maze_horizontal * 8.0)
|
||||
overview_camera = (shaft_center[0], shaft_center[1] + 12.0, top[2] + 4.0)
|
||||
overview_target = (shaft_center[0], shaft_center[1],
|
||||
(top[2] + bottom[2]) * 0.5 - 2.0)
|
||||
views = (
|
||||
("shaft-top-down", whole_mesh, top_camera, top_target, 22.0, 78.0, None),
|
||||
("ramp-eye-across", whole_mesh, eye_camera, eye_target, 15.0, 100.0, None),
|
||||
("maze-corridor", whole_mesh, maze_camera, maze_target, 8.0, 45.0, None),
|
||||
("shaft-overview", cutaway_mesh, overview_camera, overview_target, 40.0, 80.0, None),
|
||||
)
|
||||
quality = ROOT / "Standalone/Tools/verify/capture_quality.py"
|
||||
for name, render_mesh, camera, target, focus_radius, fov, clip_plane in views:
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
if name == "ramp-eye-across":
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
png = capture_dir / f"lattice-{name}.png"
|
||||
image = artifact_dir / f"lattice-{name}.jpg"
|
||||
height_tint = "off" if name == "maze-corridor" else "on"
|
||||
render_args = [args.renderer, render_mesh, png,
|
||||
"--size=1280x720", f"--fov-deg={fov:.1f}",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable",
|
||||
"--headlamp=on", f"--height-tint={height_tint}",
|
||||
f"--height-center-m={(top[2] + bottom[2]) * 0.5:.6f}",
|
||||
"--height-range-m=24", "--tile-edges=off",
|
||||
f"--focus-region={fmt((*target, focus_radius))}"]
|
||||
if clip_plane is not None:
|
||||
render_args.append(f"--clip-plane={fmt(clip_plane)}")
|
||||
run_step(f"{name}-render", render_args,
|
||||
out / f"capture-{name}-render.log", env=env, stamped=True)
|
||||
run_step(f"{name}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel",
|
||||
"error", "-y", "-i", png, "-frames:v", "1", "-q:v", "5", image],
|
||||
out / f"capture-{name}-jpeg.log", env=env)
|
||||
quality_output = run_step(f"{name}-quality", [sys.executable, quality, image],
|
||||
out / f"capture-{name}-quality.log", env=env)
|
||||
quality_lines = [line for line in quality_output.splitlines()
|
||||
if line.startswith("[CaptureQuality]")]
|
||||
if not quality_lines or not quality_lines[-1].endswith("status=PASS"):
|
||||
raise RuntimeError(f"capture quality gate failed for {name}")
|
||||
size = image.stat().st_size
|
||||
if size > 300_000:
|
||||
raise RuntimeError(f"capture exceeds 300 KB: {image} ({size} bytes)")
|
||||
print(f"[L59LatticeCapture] view={name} file={image} bytes={size} "
|
||||
f"sha256={sha256(image)} status=PASS", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L59LatticeCapture] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -1,49 +0,0 @@
|
||||
# Linux59 report
|
||||
|
||||
## Geometry and route
|
||||
|
||||
Resolved shaft: 21.0 m outer envelope; 4.5 m core radius; four-turn ramp at 6.0 m centerline radius, 3.0 m clear width, and 5.0 m pitch. The ramp spans radii 4.5–7.5 m and has 1.0 m floor support. Its inner edge is open to the core. The route keeps a 1.5 m margin from that edge (1.16 m capsule drop clearance). No raised lip was added.
|
||||
|
||||
Maximum route grade is 10.028° at the inner edge; minimum measured headroom is 2.20 m. Arrival-to-departure reachability passes all 883 stations, 44,150 capsule samples, and 4,415 support/headroom samples. The orphan-shaft control is disconnected as expected.
|
||||
|
||||
## Audits and determinism
|
||||
|
||||
The final-field interval oracle checked 2,058 samples in six boxes with zero misses. It proves shaft-core air, returns a mixed result at the core wall, rejects false all-air and all-solid proofs, and blocks the occupied maze tile.
|
||||
|
||||
The whole-surface audit passes with zero flagged triangles and zero clusters. Its scope includes a 0.25 m ramp-edge margin and the 1.0 m underside. The L45 ramp, L46 entrance, and L47 wall-foot controls fail as expected; the plain-native control passes. GCC and Clang camera peers at the shaft center `(11.790, 4.090, -130.630)` match across W1/W8/W12.
|
||||
|
||||
## Speed
|
||||
|
||||
| Measurement | L58 instrumented | L59 final |
|
||||
|---|---:|---:|
|
||||
| First tile | 0.222 s | 0.210 s |
|
||||
| Near ring | 0.364 s | 0.328 s |
|
||||
| Whole view | 1.625 s (1.69 s cited) | 1.605 s |
|
||||
| Moving p50 / p95 | 90.567 / 165.304 ms | 479.544 / 963.764 ms |
|
||||
|
||||
The roughly 1 s whole-view target was not reached; the measured whole view improved only slightly, and moving latency regressed. L59 tile-drain instrumentation recorded 972 stage rows and 28,507 ms summed worker time: 23,332 ms in marching cubes and 717 ms in column work. Marching cubes dominates the measured worker time.
|
||||
|
||||
## Hashes, tiers, and captures
|
||||
|
||||
Source stamp: `389ac77f5d35328a68e4639cac4f7d03ad5ab13b72d4832e89459437cf960ca4`
|
||||
Resolved layout: `resolved-layout-v3-fnv64:ab2f330ada023393`
|
||||
Headless SHA-256: Clang `9fc1ac6c4c5a18b6bbdc0d907d26fcaaa63a49c163a68365f3cc79726db1c474`; GCC `47b37a28a7f132af2a3f91cdd61693fb8ac519e2f23f4064b48bec0b7f8ad964`.
|
||||
|
||||
Fast passed in 174.7 s; medium passed in 1,140.1 s; full passed in 1,206.4 s with its own 7,200 s timeout. Trial count: 15/15. Earlier failed trials exposed ramp-surface spikes and an orphan-control interval false proof; the final surface and interval controls pass.
|
||||
|
||||
All captures are JPEG and under 300 KB:
|
||||
|
||||
- [Shaft top-down](captures/lattice-shaft-top-down.jpg) — 31,500 B — SHA-256 `83e38fce238ddbde84fe3ee13867f5722eac30fe177c44e746eb06b7ae341783`
|
||||
- [Ramp eye across shaft](captures/lattice-ramp-eye-across.jpg) — 24,409 B — SHA-256 `985a16fbd45c986c5bcdd0974e1cc32aac82a06a84b2df98a7b6a358f930cf14`
|
||||
- [Maze corridor](captures/lattice-maze-corridor.jpg) — 15,746 B — SHA-256 `fe72cdd5a3907f8b9071784b08889719593d8839f69a863aa45dc9ac547e771d`
|
||||
- [Shaft overview](captures/lattice-shaft-overview.jpg) — 39,081 B — SHA-256 `6394f0656200f91e77a23aed9c135e2e913dd0053c301921dff24336713c52d2`
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
**Parked on `wip/l59-open-shaft`, not merged.** The top-down capture finally reads as an open shaft with a ledge
|
||||
spiralling down the wall, so the concept is right. But the ramp's **inner (open) edge is severely jagged:** shard-like
|
||||
torn scallops along the whole edge (`lattice-ramp-eye-across.jpg`, and the top-down rim). The whole-surface audit
|
||||
passed only because its ROI stops 0.25 m in from the ramp edge: the blind-spot pattern again. Speed moved only
|
||||
1.69 → 1.61 s, at 15 of 15 trials. `assembler/world` keeps L58. Likely cause, to verify: a 1 m-thick ledge with a free
|
||||
edge is a thin feature meshed at 25 cm. The edge needs a bullnose/fillet radius of at least 2 voxels and a thickness
|
||||
the sampler resolves, and the audit ROI must include the edge itself.
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render cave, motif, and lattice features from meshes streamed at LOD4-8."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
CAPTURE54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/CaptureNativeCavesL54.py"
|
||||
spec = importlib.util.spec_from_file_location("capture_native_caves_l54", CAPTURE54)
|
||||
capture54 = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(capture54)
|
||||
run_step = capture54.run_step
|
||||
camera_angles = capture54.camera_angles
|
||||
fmt = capture54.fmt
|
||||
QUALITY = ROOT / "Standalone/Tools/verify/capture_quality.py"
|
||||
Q = 1_000_000.0
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def midpoint(points: list[tuple[float, float, float]]) -> tuple[float, float, float]:
|
||||
return tuple(sum(point[axis] for point in points) / len(points) for axis in range(3))
|
||||
|
||||
|
||||
def read_feature(name: str, layout_path: Path):
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
if name == "caves":
|
||||
edges = sorted(layout.get("cavern_edges", []), key=lambda row: row["stable_id"])
|
||||
if len(edges) != 2:
|
||||
raise ValueError("cave layout must have two resolved spine edges")
|
||||
points = [[value / Q for value in point]
|
||||
for point in edges[0]["control_points_um"]]
|
||||
target = tuple(points[-1][axis] for axis in range(3))
|
||||
return target, target[2] + 2.0, 28.0, "cave mouth"
|
||||
if name == "motifs":
|
||||
monolith = next(row for row in layout.get("stamps", []) if row.get("kind") == 5)
|
||||
target = tuple(value / Q for value in monolith["position_um"])
|
||||
floor = monolith["floor_um"] / Q
|
||||
return (target[0], target[1], floor + 12.0), floor + 12.0, 32.0, "monolith"
|
||||
edges = layout.get("cavern_edges", [])
|
||||
helix = next((edge for edge in edges
|
||||
if edge.get("stable_id") == "lattice/spine/01-helix-ramp-v1"), None)
|
||||
if helix is None:
|
||||
raise ValueError("lattice layout has no resolved helix")
|
||||
points = [tuple(value / Q for value in point) for point in helix["polyline_um"]]
|
||||
target = midpoint(points)
|
||||
return target, target[2] + 4.0, 36.0, "helix"
|
||||
|
||||
|
||||
def audit_roi_lods(mesh: Path, center: tuple[float, float, float], radius: float):
|
||||
roi_counts: dict[int, int] = {}
|
||||
roi_skirts: dict[int, int] = {}
|
||||
with mesh.open("rb") as stream:
|
||||
prefix = stream.read(60)
|
||||
if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
|
||||
raise ValueError("far-LOD audit requires a VFMSH001 Core mesh")
|
||||
version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
|
||||
if version != 1 or voxel_um <= 0:
|
||||
raise ValueError("mesh version or voxel size is invalid")
|
||||
for _ in range(tile_count):
|
||||
header = stream.read(36)
|
||||
if len(header) != 36:
|
||||
raise ValueError("truncated far-LOD tile header")
|
||||
key = struct.unpack_from("<4i", header, 0)
|
||||
surface_count, skirt_count, vertex_count, index_count, mask_count = \
|
||||
struct.unpack_from("<5I", header, 16)
|
||||
positions = stream.read(vertex_count * 12)
|
||||
normals = stream.read(vertex_count * 12)
|
||||
index_bytes = stream.read(index_count * 4)
|
||||
masks = stream.read(mask_count)
|
||||
if (len(positions) != vertex_count * 12
|
||||
or len(normals) != vertex_count * 12
|
||||
or len(index_bytes) != index_count * 4
|
||||
or len(masks) != mask_count):
|
||||
raise ValueError("truncated far-LOD tile payload")
|
||||
lod = key[3]
|
||||
coords = list(struct.iter_unpack("<3f", positions))
|
||||
indices = struct.unpack(f"<{index_count}I", index_bytes) if index_count else ()
|
||||
for triangle in range(index_count // 3):
|
||||
ids = indices[triangle * 3:triangle * 3 + 3]
|
||||
if max(ids) >= vertex_count:
|
||||
raise ValueError("far-LOD triangle references an invalid vertex")
|
||||
is_skirt = bool(masks and triangle < len(masks) and masks[triangle])
|
||||
if is_skirt:
|
||||
continue
|
||||
centroid = tuple(sum(coords[index][axis] for index in ids) / 3.0
|
||||
for axis in range(3))
|
||||
distance_squared = sum((centroid[axis] - center[axis]) ** 2
|
||||
for axis in range(3))
|
||||
if distance_squared <= radius * radius:
|
||||
roi_counts[lod] = roi_counts.get(lod, 0) + 1
|
||||
if skirt_count:
|
||||
roi_skirts[lod] = roi_skirts.get(lod, 0) + skirt_count
|
||||
if stream.read(1):
|
||||
raise ValueError("far-LOD mesh has trailing bytes")
|
||||
return roi_counts, roi_skirts
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
parser.add_argument("--fixture-dir", type=Path, required=True)
|
||||
parser.add_argument("--layout-dir", type=Path, required=True)
|
||||
parser.add_argument("--headless", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
parser.add_argument("--artifact-dir", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
out = args.out.resolve()
|
||||
fixture_dir, layout_dir = args.fixture_dir.resolve(), args.layout_dir.resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
mesh_dir, png_dir = out / "far-lod-meshes", out / "far-lod-png"
|
||||
for directory in (mesh_dir, png_dir, artifact_dir):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out / "xdg-cache")
|
||||
|
||||
cases = (
|
||||
("caves", fixture_dir / "caves.json",
|
||||
layout_dir / "native-caves-field-clang.json", "caves", []),
|
||||
("motifs", fixture_dir / "style-override-motifs-slice1.json",
|
||||
layout_dir / "motif-slice1-capsule-reachability-clang.json", "surface-world", []),
|
||||
("lattice", fixture_dir / "lattice-l58.json",
|
||||
layout_dir / "native-lattice-field-clang.json", "lattice", ["--lattice-strate=1"]),
|
||||
)
|
||||
quality_rows = []
|
||||
for name, fixture, layout, recipe, extra_flags in cases:
|
||||
if not fixture.is_file() or not layout.is_file():
|
||||
raise RuntimeError(f"missing {name} fixture or layout")
|
||||
target, target_height, radius, label = read_feature(name, layout)
|
||||
stream_camera = (target[0] + 1024.0, target[1], target[2], 0.0, 0.0)
|
||||
stream_yaw, stream_pitch = camera_angles(stream_camera[:3], target)
|
||||
stream_camera = (*stream_camera[:3], stream_yaw, stream_pitch)
|
||||
mesh = mesh_dir / f"{name}-lod4-8.vfmesh"
|
||||
mesh_layout = layout_dir / f"{name}-far-lod-layout.json"
|
||||
command = [args.headless, fixture, "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||
f"--camera={fmt(stream_camera)}", *extra_flags,
|
||||
f"--resolved-layout-output={mesh_layout}", f"--mesh-export={mesh}"]
|
||||
output = run_step(f"{name}-far-lod-export", command,
|
||||
out / f"capture-{name}-far-lod-export.log", env=env, stamped=True)
|
||||
worker_rows = {int(match.group(1)): (int(match.group(2)), int(match.group(3)))
|
||||
for match in re.finditer(
|
||||
r"^\[WorldAssemblerLODWorker\] lod=(\d+) tiles=(\d+) .*?skirt_triangles=(\d+)",
|
||||
output, re.M)}
|
||||
if (set(worker_rows) != set(range(9))
|
||||
or any(worker_rows[level][0] <= 0 for level in range(9))
|
||||
or sum(worker_rows[level][1] for level in range(4, 9)) <= 0):
|
||||
raise RuntimeError(f"{name} far view lacks LOD0-8 tile/skirt work: {worker_rows}")
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{name} far LOD export produced no mesh")
|
||||
roi, roi_skirts = audit_roi_lods(mesh, target, radius)
|
||||
feature_lods = sorted(level for level, count in roi.items() if count > 0)
|
||||
far_feature_lods = [level for level in feature_lods if level >= 4]
|
||||
if not far_feature_lods:
|
||||
raise RuntimeError(f"{name} {label} has no non-skirt surface triangles in LOD4-8 ROI: {roi}")
|
||||
|
||||
camera = (target[0] + 18.0, target[1] - 28.0, target_height + 10.0)
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
png = png_dir / f"{name}-far-lod.png"
|
||||
image = artifact_dir / f"{name}-far-lod.jpg"
|
||||
render = [args.renderer, mesh, png, "--size=1280x720", "--fov-deg=76",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}", "--tile-skirts=on", "--shade=readable",
|
||||
"--headlamp=on", "--height-tint=on", f"--height-center-m={target_height:.6f}",
|
||||
"--height-range-m=42", "--tile-edges=off",
|
||||
f"--focus-region={fmt((*target, radius))}"]
|
||||
run_step(f"{name}-far-lod-render", render,
|
||||
out / f"capture-{name}-far-lod-render.log", env=env, stamped=True)
|
||||
run_step(f"{name}-far-lod-jpeg", ["ffmpeg", "-nostdin", "-hide_banner",
|
||||
"-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "5", image],
|
||||
out / f"capture-{name}-far-lod-jpeg.log", env=env)
|
||||
quality_output = run_step(f"{name}-far-lod-quality", [sys.executable, QUALITY, image],
|
||||
out / f"capture-{name}-far-lod-quality.log", env=env)
|
||||
quality = [line for line in quality_output.splitlines()
|
||||
if line.startswith("[CaptureQuality]")]
|
||||
if not quality or not quality[-1].endswith("status=PASS"):
|
||||
raise RuntimeError(f"{name} far LOD capture failed its image quality gate")
|
||||
size = image.stat().st_size
|
||||
if not 0 < size <= 300_000:
|
||||
raise RuntimeError(f"{name} far LOD capture exceeds 300 KB: {size}")
|
||||
print(f"[L60FarLODCapture] generator={name} feature={label} file={image} "
|
||||
f"bytes={size} sha256={sha256(image)} feature_lods={feature_lods} "
|
||||
f"feature_triangles={sum(roi.values())} far_skirt_triangles="
|
||||
f"{sum(value for lod, value in roi_skirts.items() if lod >= 4)} status=PASS",
|
||||
flush=True)
|
||||
quality_rows.append({"generator": name, "feature": label, "file": str(image),
|
||||
"bytes": size, "sha256": sha256(image), "feature_triangles_by_lod": roi,
|
||||
"feature_skirts_by_lod": roi_skirts})
|
||||
(out / "far-lod-captures.json").write_text(json.dumps(quality_rows, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L60FarLODCapture] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,77 @@
|
||||
# Linux 60 report
|
||||
|
||||
## Result and root cause
|
||||
|
||||
The continuation is green. The full tier and speed matrix passed after fixing signed-overflow undefined behavior in the shared voxel hash.
|
||||
|
||||
The first differing density was LOD1 tile `(0,64,-512)`, step 2, sample order 23,658 at `(64,82,-476)`. Before the fix, Clang returned `-4.0078125` (`0xc0804000`) and GCC returned `-4.125` (`0xc0840000`). The sample follows the mesher's LOD X8/boundary scalar path into `DensityAtLattice` and the compiled analytic `TunnelNetwork` field. Its upstream cave passage placement comes from `UVoxelStrateManager::GeneratePassages` → `PassageRandom01` → `VoxelHash::Cell(i,c,seed)`.
|
||||
|
||||
`VoxelHash::Cell` added `0x7fffffff` to a signed cell coordinate before casting to `uint32`; connection `c=1` overflowed `int32`, so the compiler-dependent hash changed the passage coordinates and cave geometry. The trace showed passage pair `(0,1)` diverging, followed by all 21 control points of passage 1. The fix performs the bias in unsigned arithmetic in `Cell`, `Pair`, legacy `Cellular3D`, and the shared Core cellular hash. It does not quantize density. After the fix both compilers produced the same pair `(0,1)` coordinates, the same 44 passage points, and `0xc0804000` at the first sample. The evaluator version is now 211.
|
||||
|
||||
Before/after traces are in `Saved/Linux/l60/diag/caves-{clang,gcc}-probe.log` and `Saved/Linux/l60/diag/cache/fullcache-{clang,gcc}.log`.
|
||||
|
||||
## Far LOD support and seam checks
|
||||
|
||||
All full-view fixtures use max level `-1`, clip radius 3, and max clip level 8. All three generators streamed LOD0–8 with skirts.
|
||||
|
||||
- **Caves:** X8 and Z-run gates detect caves in manager and resolved stratum mappings, then use scalar lattice sampling for cave samples. Clang/GCC cave tile hashes match at W1/W8/W12, including random views at LOD1–2. The cave-mouth transition crossed levels 0–8 (432 entering tiles).
|
||||
- **Motifs:** The fixed motif field streams through the existing rings, caps, and skirts. The monolith transition crossed levels 0–8 (295 entering tiles); whole-surface foot, cap, and platform-edge audits passed.
|
||||
- **Lattice:** The fixture selects native lattice stratum 1 and uses the existing ring and skirt path. The helix transition crossed levels 0–8 (380 entering tiles); field, interval, reachability, and shape-free audits passed.
|
||||
|
||||
Hard, gradient, and interleaved stratum transitions passed with zero hole columns and zero air samples. Far-LOD cave, motif, and lattice JPEG captures passed their audits and are under 300 KB.
|
||||
|
||||
## Comparable speed matrix
|
||||
|
||||
`verify.py full --speed-matrix` ran five interleaved repetitions per generator, rotating generator order. Every run used camera `(-12,-118,-20 m)`, rotation `(0°,0°)`, clip radius 3, max clip level 8, max level `-1`, camera step 24 m, 12 workers, and shared column cache. The stage asserted the shared HeadlessConfig fields, whole-view and per-LOD tile-count ranges, and stable counts across repetitions. It passed for all eight generators. Every listed tile count occurred in all five runs; timings are medians from the final full run.
|
||||
|
||||
| Generator | Tiles | Whole view (s) | First tile (s) | Near ring (s) | Moving p50 (ms) | Moving p95 (ms) |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| Natural SW | 2,130 | 1.669 | 0.502 | 0.519 | 63.971 | 119.891 |
|
||||
| periodicHills | 1,986 | 1.311 | 0.454 | 0.479 | 60.267 | 115.648 |
|
||||
| mirroredRelief | 1,986 | 1.317 | 0.471 | 0.490 | 60.568 | 115.474 |
|
||||
| flatField | 1,986 | 1.303 | 0.465 | 0.485 | 60.627 | 117.144 |
|
||||
| Floating Islands | 2,130 | 4.926 | 0.817 | 0.977 | 528.939 | 871.290 |
|
||||
| Caves | 2,130 | 1.408 | 0.489 | 0.507 | 61.099 | 118.508 |
|
||||
| Motifs | 1,986 | 1.326 | 0.476 | 0.493 | 60.107 | 116.104 |
|
||||
| Lattice | 2,130 | 1.701 | 0.539 | 0.561 | 69.572 | 137.585 |
|
||||
|
||||
Full run records and common config are in `Saved/Linux/l60/speed-matrix.json`.
|
||||
|
||||
## Captures
|
||||
|
||||
- `Saved/Linux/l60/captures/caves-far-lod.jpg` — 16,642 bytes; SHA-256 `55f8d3d2de76541e9f1c1d09b771af40bfbb37b1ce5afc0b2f70419b84ed71a3`
|
||||
- `Saved/Linux/l60/captures/motifs-far-lod.jpg` — 24,926 bytes; SHA-256 `bfc5d8ddebc3706fedd6108653b4777fac4bac44e71f60c8e58b3508c14c3aa6`
|
||||
- `Saved/Linux/l60/captures/lattice-far-lod.jpg` — 30,714 bytes; SHA-256 `c261728e48975f01c712e31594b4284eede289b73e793e6204b78cb8ff2a2947`
|
||||
|
||||
## Hashes and tiers
|
||||
|
||||
Clang 19 and GCC 14 builds used FP contraction off and 12 jobs. Final source stamp and source-manifest SHA-256: `902a0d8e45f895d1b53c0a86999adee5598ff77e1752aa7ea90397520cf7b82d`.
|
||||
|
||||
- Clang headless: `dcf6286bcecac382f9cce2458e7c327485e9b8b993c3d544e627a8fc964947fa`
|
||||
- GCC headless: `be86f90f993ba27bfde235e0850ea783298396dc03456360cf4bb3fd4815988b`
|
||||
- Offscreen capture: `b8f7a441c0a41be6ebd61721ff903832a51291df7a886e669b9084d84441d4c5`
|
||||
|
||||
| Tier | Status | Checks / runs | Elapsed |
|
||||
|---|---|---:|---:|
|
||||
| Fast | PASS before hash fix | 453 / 113 | 335.7 s |
|
||||
| Medium | PASS before hash fix | 968 / 289 | 1,559.1 s |
|
||||
| Full continuation trial 1 | FAIL: stale binary stamp; no checks ran | — | 0.0 s |
|
||||
| Full continuation trial 2 | PASS on source stamp `914af3...` | 2,304 / 660 | 3,256.9 s |
|
||||
| Full continuation trial 3 | PASS on final source stamp, including speed matrix | 2,304 / 660 | 3,260.6 s |
|
||||
|
||||
Fast and medium were not rerun as standalone tiers after the hash change; the passing full run executed their checks at the current source stamp.
|
||||
|
||||
## Trials and failed runs
|
||||
|
||||
The prior L60 work used 15 trials. This continuation used 3 of its 10-trial budget. Trial 1 stopped before checks because it found stale Clang/GCC executables. Trial 2 passed, then the final source review restored the mesher's existing `<bit>` include and changed the source stamp, so trial 3 rebuilt and reran full against the exact final source. The pre-fix trial 15 had failed peer determinism on the same six LOD1 and eight LOD2 tile keys across four seeded views. Trial 3 had no failed checks; negative controls reported expected failures.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted. The stamp matches the final Clang and GCC builds (`902a0d8e…`); full PASS, including the speed matrix.
|
||||
**The root cause is a real cross-platform determinism bug:** `VoxelHash::Cell`/`Pair`/`Cellular3D` added
|
||||
`0x7FFFFFFF` to a signed `int32` (signed overflow is undefined behaviour), so passage placement became
|
||||
compiler-dependent. **The same code lives in the Unreal plugin** (`Source/VoxelForge/Public/VoxelCaveMorphology.h`,
|
||||
identical to the standalone copy before the fix), and only the standalone copy had been fixed. I applied the identical
|
||||
fix to the plugin header and confirmed the two copies match. MSVC typically wraps there, so the owner's existing
|
||||
worlds are most likely unchanged, but this needs his Windows build. No other signed-bias pattern remains in `Source/`
|
||||
or Core. The comparable speed matrix is now a standing `verify.py full --speed-matrix` stage.
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,65 @@
|
||||
# L61 sanitizer sweep
|
||||
|
||||
## Builds and coverage
|
||||
|
||||
- Clang 19: `address,undefined,float-cast-overflow`; GCC: `address,undefined`. Both used `-fno-sanitize-recover=undefined`, `-ffp-contract=off`, and two build jobs.
|
||||
- Clang's `integer` group was tried with unsigned overflow excluded. libstdc++ `std::rotl` emitted a non-actionable shift diagnostic, so the group is omitted from the standing stage.
|
||||
- ASan runs used `taskset -c 0-11 prlimit --rss=6442450944` to allow its shadow address space to map. Linux RSS limits are advisory; the measured descendant-tree peak from the complete comparable sweep was 1.798 GiB. The corrected final rerun used the same limit mode.
|
||||
- Each compiler ran 19 cases at LOD0–8: natural SW, FI, all three profiles, caves, motifs, lattice, both passage styles, and the live surface crossing. General generators used two camera views; the suite also covers seeded random cameras. Total: 38 runs, 46 passing checks, no ASan/UBSan reports. No edit fixture was present.
|
||||
|
||||
## Findings
|
||||
|
||||
The generator runs found no runtime sanitizer issue. The boundary findings below came from the requested static scan; they were fixed at the source.
|
||||
|
||||
| File:line | Kind and trigger | Fix |
|
||||
|---|---|---|
|
||||
| `VoxelClipmapDesiredTiles.h:35,49,57-68,129-171` (plugin and standalone) | Signed negation in floor/ceil division failed for `INT_MIN`; center/radius, ring, and tile-bound arithmetic could overflow at coordinate limits. | Quotient/remainder division, 64-bit intermediates and clamped bounds, bounded unsigned LOD shifts, and wide tile loops. |
|
||||
| `VoxelWorld.cpp:85-93,6254-6267,7679-7693,10100-10104,10388-10394,11473-11497,12141-12147,12886-12888` (plugin only) | Signed LOD shifts, biased division, origin/band calculations, and coordinate differences were unsafe for invalid levels or edge coordinates. | Central bounded unsigned scale/extent helpers; 64-bit arithmetic and clamping; reject initial clipmaps outside the supported voxel range. |
|
||||
| `VoxelContentManager.cpp:562-584,1109-1125,1476-1484,1533-1542` (plugin only) | `-INT_MIN`, player-cell ± radius, region-size products, and authored count spans could overflow. | Remainder-adjusted floor division, widened coordinates/count spans, capped reserve, and clamped count conversion. |
|
||||
| `VoxelDensityVolume.cpp:156-162,185-186,638-639` (plugin only) | Floor division negated `INT_MIN`; signed LOD shifts depended on valid levels. | Remainder-adjusted division and bounded unsigned shifts. |
|
||||
| `VoxelTypes.h:335-363` (plugin and standalone) | Signed shifts and coordinate-times-extent could overflow; `CenterCm` repeated the signed LOD shift. | Clamp LOD to 0–8, use unsigned shifts and 64-bit coordinate multiplication with clamping; derive `CenterCm` from the safe extent helper. |
|
||||
|
||||
## Unreal-only static scan
|
||||
|
||||
Scanned all 30 `Source/VoxelForge/Private/*.cpp` files. Standalone compiles 15; the other 15 were reviewed statically and are not compiled here: `VoxelAtmosphereManager.cpp`, `VoxelContentManager.cpp`, `VoxelDensityVolume.cpp`, `VoxelForgeCoreAdapter.cpp`, `VoxelForgeModule.cpp`, `VoxelRedefinedDensityField.cpp`, `VoxelSeasonAsset.cpp`, `VoxelSeasonManifest.cpp`, `VoxelStackSampler.cpp`, `VoxelStartupTrace.cpp`, `VoxelStats.cpp`, `VoxelStrateDefinition.cpp`, `VoxelStratePreview.cpp`, `VoxelStrateRecipe.cpp`, and `VoxelWorld.cpp`.
|
||||
|
||||
No signed-bias formulas or unbounded LOD shifts remain in that scan. Remaining private-source shifts are bounded (for example, `VoxelGenerator.cpp:900` clamps LOD to 0–5; marching-cubes shifts are fixed bit masks). The Unreal files could not be compiled here.
|
||||
|
||||
## Control, version, and hashes
|
||||
|
||||
The test-only `Saved/Linux/l61/sanitizer/signed-overflow-control.cpp` evaluates `INT_MAX + 1`. Both compilers report signed integer overflow, and the stage requires that diagnostic; logs are `signed-overflow-control-clang.log` and `signed-overflow-control-gcc.log`.
|
||||
|
||||
Version remains **211**. Supported-input output is unchanged; only invalid or out-of-range arithmetic is bounded.
|
||||
|
||||
- Source stamp: `c22b877c586423fbe67cdffb8ab762e3fe6e20e91897685447113fcb23944035`
|
||||
- Clang headless: `400b4a66a4d3f76f584f57aefed882bcfc736db563815145518583ac007bd57c`
|
||||
- GCC headless: `8f782ca4c73b6fc0a942fa06db4149426e58beaacd2bb9185974be4167a614b6`
|
||||
- Clang sanitizer: `2a1618d25904b1c3620cfcd0e37d59133fccd349df347442396bd0be42f7b21c`
|
||||
- GCC sanitizer: `6c70cc1c5c843b24de7f82e19a44947cdcd1d8bd4ba01a448027a230ebe5d3e7`
|
||||
|
||||
## Standing tier and results
|
||||
|
||||
Added a dedicated `verify.py sanitize` tier with a 1,200-second budget and the option to include it in full via `verify.py full --sanitize`.
|
||||
|
||||
| Tier | Timeout | Result |
|
||||
|---|---:|---|
|
||||
| Sanitize | 1,200 s | PASS, 787.3 s |
|
||||
| Fast | 900 s | PASS, 251.4 s |
|
||||
| Medium | 2,400 s | PASS, 1,554.2 s |
|
||||
| Full + five-run speed matrix | 3,600 s | PASS, 1,709.3 s |
|
||||
|
||||
The full tier checked Clang/GCC and W1/W8/W12 determinism, random cameras, the 30-cell set, baseline/candidate cost pairs, and five interleaved timings for all eight generator views. Expected black-image negative-control exits were handled by passing control checks.
|
||||
|
||||
## Trials and failed attempts
|
||||
|
||||
Used all 15 verification attempts: seven discarded sanitizer attempts, the initial sanitizer pass, fast/medium/full, then the corrected sanitizer pass and final fast/medium/full. Discarded attempts were two missing-`/usr/bin/time` wrapper failures, the noisy integer-sanitizer diagnostic, a source-stamp change during a run, two stale-binary refusals, and a LeakSanitizer ptrace failure. A separate Clang release build briefly failed on a duplicate `ExtentVoxels()` declaration; it was corrected and rebuilt. Final tiers have no unexpected failed runs.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted. The stamp matches the final Clang and GCC builds (`c22b877c…`); the sanitize, fast, medium and full tiers
|
||||
all pass. The runtime sweep is clean (38 runs, 0 reports), and the planted signed-overflow control is caught by both
|
||||
compilers. **The Unreal-only edits** (`VoxelWorld.cpp`, `VoxelContentManager.cpp`, `VoxelDensityVolume.cpp`) can't
|
||||
be compiled here. I reviewed the `VoxelWorld.cpp` diff: the helpers are `static FORCEINLINE`, they use UE types
|
||||
(`int32`, `int64`, `FMath::Clamp`, `MIN_int32`/`MAX_int32`), and their LOD clamp of 0–8 matches
|
||||
`UVoxelSettings::MaxClipLevel` (`ClampMax = "8"`) and `CoreMaxClipLevel = 8`, so they don't change supported
|
||||
behaviour. These go into the owner's Windows build list.
|
||||
@@ -0,0 +1,130 @@
|
||||
# L64 Linux baseline
|
||||
|
||||
## Empty top-strate result
|
||||
|
||||
**SurfaceWorld is not empty.** U2’s gate report says its sampled top window had 0 triangles, but does not give the camera/window coordinates. With the current owner file (SHA-256 `f42f96adca58…`), the correct SurfaceWorld windows mesh on Clang: the file `viewer_start` `(0, 0, -496 m)` gives 9/27 nonempty LOD0 tiles and 36,864 triangles; the resolved route arrival `(38.75, 70.75, -374.121 m)` gives 9/27 and 25,672; the departure mouth `(-6.75, 25.25, -376.307 m)` gives 11/27 and 33,582. The binding is `SurfaceWorld-native`; the 57 boundary probes had 0 dispatch disagreements. This points to the gate’s sample window placement, not an empty field or wrong generator binding. The precise U2 sample point cannot be confirmed from its report.
|
||||
|
||||
There is a separate owner-plan defect: strict layout validation rejects `passage/0-1/0` because its lower mouth is at z = −822.06 m, 78.56 m below FloatingIslands’ content band (bottom −743.5 m; that point is in TunnelNetwork). The plan’s passage audit also fails; details are below.
|
||||
|
||||
## Bisect
|
||||
|
||||
All 57 commits in `a176d07^..3202193` were exported and built with Clang 19, `-ffp-contract=off`, `-j12`, taskset CPUs 0–11, a 6 GiB address-space limit, and `TMPDIR` under `Saved/`. 56 built. `3e443bf` (L37/R1) fails to compile on an extra `)`; its following L37 fix `7f2d84d` builds. The timing matrix has 402 successful runs: three shuffled/interleaved passes per recipe and data/commit configuration. Every run used the same view `(−12, −118, −20 m)`, 12 workers, `--force-generate`, clip radius 3, max clip level 8, LOD8 step 0, hash off. The fixed data was `3202193` (`7b095f9b…`) for every commit; commits before L24 also ran the pre-L24 file (`289cd755…`).
|
||||
|
||||
**Not Core.** On fixed data, SurfaceWorld LOD0 worker time per tile is 10.05 ms at L14 and 12.78 ms at L61 (1.27×); whole-view is 4.31 s and 5.02 s (1.16×). The summed worker time across LOD0–8 is 31.2 s and 38.7 s (1.24×). This does not reproduce the Windows roughly 2× per-tile rise, so the cause is on the Unreal side: U1 or the adapter’s Core driving path. No Core fix was made; `Source/` was untouched.
|
||||
|
||||
`W/F/N/Σ/L0` below means whole-view seconds / first-tile seconds / near-ring seconds / summed worker-seconds across LOD0–8 / LOD0 worker-ms per tile. Each parenthesized percent is the delta against L14 for the same recipe and data file. L0 steps mark upward changes of at least 10% versus the previous successful commit in that dataset. For LOD0 p50/p95, builds before L55 lack tile-drain counters; later values are from separate `--instrument-tile-drain` passes on the same view/data (SW then FI, ms). Those opt-in passes have force-generation off, as required for the counters, and do not contribute to the timing medians.
|
||||
|
||||
| Round | Commit | Data | SW W/F/N/Σ/L0 (Δ%) | FI W/F/N/Σ/L0 (Δ%) | L0 step ≥10% | LOD0 p50/p95 SW; FI (ms) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| L14 | `a176d07` | 3202193 | 4.31 (+0%) / 1.71 (+0%) / 1.76 (+0%) / 31.2 (+0%) / 10.05 (+0%) | 23.57 (+0%) / 3.21 (+0%) / 3.61 (+0%) / 236.6 (+0%) / 166.93 (+0%) | — | — |
|
||||
| L14 | `a176d07` | pre-L24 | 4.53 (+0%) / 1.61 (+0%) / 1.67 (+0%) / 34.6 (+0%) / 11.71 (+0%) | 26.34 (+0%) / 3.91 (+0%) / 4.70 (+0%) / 291.3 (+0%) / 236.10 (+0%) | — | — |
|
||||
| L15 | `a6cd7cf` | 3202193 | 4.26 (-1%) / 1.44 (-16%) / 1.49 (-15%) / 32.1 (+3%) / 10.50 (+5%) | 23.88 (+1%) / 3.67 (+15%) / 4.30 (+19%) / 249.3 (+5%) / 179.96 (+8%) | — | — |
|
||||
| L15 | `a6cd7cf` | pre-L24 | 2.96 (-35%) / 1.33 (-17%) / 1.36 (-18%) / 19.4 (-44%) / 5.30 (-55%) | 27.50 (+4%) / 3.62 (-7%) / 4.33 (-8%) / 297.1 (+2%) / 247.25 (+5%) | — | — |
|
||||
| L16 | `9a7c87e` | 3202193 | 3.59 (-17%) / 1.60 (-6%) / 1.65 (-6%) / 23.9 (-23%) / 8.47 (-16%) | 23.94 (+2%) / 3.92 (+22%) / 4.26 (+18%) / 256.3 (+8%) / 182.06 (+9%) | — | — |
|
||||
| L16 | `9a7c87e` | pre-L24 | 4.49 (-1%) / 1.75 (+9%) / 1.81 (+9%) / 32.7 (-5%) / 10.08 (-14%) | 26.57 (+1%) / 3.66 (-6%) / 4.65 (-1%) / 271.7 (-7%) / 231.16 (-2%) | SW ↑90.2% | — |
|
||||
| L17 | `8704129` | 3202193 | 3.95 (-8%) / 1.51 (-12%) / 1.56 (-11%) / 29.9 (-4%) / 9.06 (-10%) | 24.71 (+5%) / 3.95 (+23%) / 4.67 (+29%) / 264.2 (+12%) / 176.04 (+5%) | — | — |
|
||||
| L17 | `8704129` | pre-L24 | 2.93 (-35%) / 0.87 (-46%) / 0.94 (-44%) / 26.0 (-25%) / 8.88 (-24%) | 25.58 (-3%) / 2.40 (-38%) / 3.11 (-34%) / 278.4 (-4%) / 231.14 (-2%) | — | — |
|
||||
| L18 | `dcf0763` | 3202193 | 4.08 (-5%) / 1.55 (-9%) / 1.60 (-9%) / 31.4 (+1%) / 10.92 (+9%) | 29.75 (+26%) / 3.56 (+11%) / 4.42 (+22%) / 317.7 (+34%) / 257.73 (+54%) | SW ↑20.5%, FI ↑46.4% | — |
|
||||
| L18 | `dcf0763` | pre-L24 | 4.02 (-11%) / 1.65 (+3%) / 1.70 (+2%) / 28.4 (-18%) / 6.94 (-41%) | 25.67 (-3%) / 4.14 (+6%) / 4.96 (+6%) / 258.9 (-11%) / 177.66 (-25%) | — | — |
|
||||
| L19 | `811c4b7` | 3202193 | 4.79 (+11%) / 1.80 (+5%) / 1.85 (+5%) / 36.5 (+17%) / 12.45 (+24%) | 26.15 (+11%) / 2.54 (-21%) / 3.15 (-13%) / 283.8 (+20%) / 235.00 (+41%) | SW ↑14.1% | — |
|
||||
| L19 | `811c4b7` | pre-L24 | 3.51 (-23%) / 0.80 (-50%) / 0.86 (-49%) / 32.4 (-6%) / 9.34 (-20%) | 25.38 (-4%) / 3.94 (+1%) / 4.64 (-1%) / 259.8 (-11%) / 213.94 (-9%) | SW ↑34.6%, FI ↑20.4% | — |
|
||||
| L20 | `6f9c7bd` | 3202193 | 4.52 (+5%) / 1.74 (+2%) / 1.78 (+1%) / 33.3 (+7%) / 12.08 (+20%) | 23.30 (-1%) / 2.36 (-27%) / 3.22 (-11%) / 253.6 (+7%) / 173.02 (+4%) | — | — |
|
||||
| L20 | `6f9c7bd` | pre-L24 | 4.32 (-5%) / 1.65 (+2%) / 1.69 (+1%) / 31.9 (-8%) / 10.91 (-7%) | 22.85 (-13%) / 2.18 (-44%) / 3.41 (-27%) / 229.8 (-21%) / 170.36 (-28%) | SW ↑16.8% | — |
|
||||
| L20 review | `5d5447d` | 3202193 | 4.76 (+11%) / 1.63 (-4%) / 1.70 (-3%) / 35.1 (+13%) / 11.85 (+18%) | 25.93 (+10%) / 3.91 (+22%) / 4.78 (+33%) / 275.4 (+16%) / 227.96 (+37%) | FI ↑31.8% | — |
|
||||
| L20 review | `5d5447d` | pre-L24 | 4.50 (-1%) / 1.65 (+3%) / 1.72 (+3%) / 34.5 (-0%) / 11.83 (+1%) | 27.01 (+3%) / 3.09 (-21%) / 3.87 (-18%) / 284.0 (-3%) / 225.37 (-5%) | FI ↑32.3% | — |
|
||||
| L21 | `8753f6d` | 3202193 | 4.26 (-1%) / 1.50 (-12%) / 1.56 (-11%) / 33.0 (+6%) / 11.30 (+12%) | 26.21 (+11%) / 3.80 (+19%) / 5.09 (+41%) / 268.6 (+14%) / 192.22 (+15%) | — | — |
|
||||
| L21 | `8753f6d` | pre-L24 | 3.78 (-17%) / 1.02 (-37%) / 1.12 (-33%) / 33.1 (-4%) / 10.18 (-13%) | 23.71 (-10%) / 3.73 (-5%) / 4.60 (-2%) / 260.3 (-11%) / 196.70 (-17%) | — | — |
|
||||
| L22 | `55c26a0` | 3202193 | 4.01 (-7%) / 1.48 (-13%) / 1.55 (-12%) / 28.9 (-7%) / 10.23 (+2%) | 35.34 (+50%) / 4.74 (+48%) / 5.49 (+52%) / 369.9 (+56%) / 240.61 (+44%) | FI ↑25.2% | — |
|
||||
| L22 | `55c26a0` | pre-L24 | 4.13 (-9%) / 1.58 (-2%) / 1.64 (-2%) / 30.4 (-12%) / 11.32 (-3%) | 33.67 (+28%) / 3.50 (-11%) / 4.07 (-13%) / 360.8 (+24%) / 273.30 (+16%) | SW ↑11.2%, FI ↑38.9% | — |
|
||||
| L23 | `71d09a8` | 3202193 | 3.38 (-21%) / 1.54 (-10%) / 1.60 (-9%) / 22.0 (-29%) / 9.67 (-4%) | 33.92 (+44%) / 3.21 (+0%) / 3.77 (+4%) / 359.9 (+52%) / 234.50 (+40%) | — | — |
|
||||
| L23 | `71d09a8` | pre-L24 | 4.34 (-4%) / 1.70 (+6%) / 1.76 (+5%) / 31.5 (-9%) / 10.93 (-7%) | 30.60 (+16%) / 4.29 (+10%) / 5.16 (+10%) / 316.1 (+9%) / 222.51 (-6%) | — | — |
|
||||
| L24 | `cb9236c` | 3202193 | 2.83 (-34%) / 0.95 (-44%) / 1.03 (-42%) / 17.5 (-44%) / 6.50 (-35%) | 36.87 (+56%) / 4.38 (+37%) / 5.18 (+44%) / 404.1 (+71%) / 298.88 (+79%) | FI ↑27.5% | — |
|
||||
| L25 | `ef789aa` | 3202193 | 4.29 (-0%) / 1.72 (+1%) / 1.78 (+1%) / 30.8 (-1%) / 10.86 (+8%) | 36.36 (+54%) / 2.18 (-32%) / 2.88 (-20%) / 387.8 (+64%) / 286.83 (+72%) | SW ↑67.0% | — |
|
||||
| L26 | `37a2230` | 3202193 | 4.06 (-6%) / 1.69 (-1%) / 1.76 (-0%) / 32.4 (+4%) / 7.90 (-21%) | 43.11 (+83%) / 4.54 (+42%) / 5.28 (+46%) / 456.7 (+93%) / 325.35 (+95%) | FI ↑13.4% | — |
|
||||
| L27 | `07b3fe2` | 3202193 | 4.10 (-5%) / 1.44 (-16%) / 1.49 (-16%) / 32.2 (+3%) / 9.12 (-9%) | 40.19 (+71%) / 3.68 (+15%) / 4.79 (+33%) / 441.3 (+87%) / 255.16 (+53%) | SW ↑15.4% | — |
|
||||
| L28 | `f23ab7b` | 3202193 | 4.44 (+3%) / 1.72 (+1%) / 1.78 (+1%) / 32.1 (+3%) / 10.38 (+3%) | 32.25 (+37%) / 3.55 (+11%) / 4.26 (+18%) / 339.3 (+43%) / 248.36 (+49%) | SW ↑13.9% | — |
|
||||
| L29 | `63c7e56` | 3202193 | 2.55 (-41%) / 0.72 (-58%) / 0.75 (-58%) / 21.5 (-31%) / 6.19 (-38%) | 34.47 (+46%) / 2.83 (-12%) / 3.64 (+1%) / 376.1 (+59%) / 265.76 (+59%) | — | — |
|
||||
| L30 | `103d8b4` | 3202193 | 4.41 (+2%) / 1.59 (-7%) / 1.65 (-6%) / 32.1 (+3%) / 10.65 (+6%) | 36.40 (+54%) / 4.25 (+33%) / 4.96 (+37%) / 387.8 (+64%) / 303.13 (+82%) | SW ↑72.0%, FI ↑14.1% | — |
|
||||
| L31 | `28069f0` | 3202193 | 3.12 (-27%) / 1.55 (-9%) / 1.61 (-8%) / 18.8 (-40%) / 7.23 (-28%) | 37.60 (+60%) / 4.14 (+29%) / 5.17 (+43%) / 407.7 (+72%) / 296.35 (+78%) | — | — |
|
||||
| L32 | `d0158c6` | 3202193 | 4.88 (+13%) / 1.57 (-8%) / 1.71 (-3%) / 37.2 (+19%) / 12.07 (+20%) | 34.29 (+46%) / 2.85 (-11%) / 3.31 (-8%) / 362.5 (+53%) / 229.88 (+38%) | SW ↑66.9% | — |
|
||||
| L33 | `b7a47be` | 3202193 | 2.65 (-38%) / 0.86 (-49%) / 0.89 (-50%) / 21.3 (-32%) / 9.38 (-7%) | 31.98 (+36%) / 2.91 (-9%) / 3.84 (+7%) / 350.3 (+48%) / 257.32 (+54%) | FI ↑11.9% | — |
|
||||
| L34 | `49e29fc` | 3202193 | 4.40 (+2%) / 1.53 (-11%) / 1.58 (-10%) / 34.5 (+11%) / 10.81 (+8%) | 36.15 (+53%) / 4.22 (+31%) / 5.02 (+39%) / 378.7 (+60%) / 252.48 (+51%) | SW ↑15.3% | — |
|
||||
| L35 | `ba01707` | 3202193 | 4.80 (+11%) / 1.77 (+4%) / 1.84 (+4%) / 36.0 (+15%) / 7.08 (-30%) | 35.01 (+49%) / 4.17 (+30%) / 4.96 (+37%) / 390.0 (+65%) / 289.50 (+73%) | FI ↑14.7% | — |
|
||||
| L36 | `8be1c4d` | 3202193 | 3.67 (-15%) / 1.03 (-40%) / 1.08 (-39%) / 34.2 (+10%) / 10.92 (+9%) | 34.93 (+48%) / 2.72 (-15%) / 3.21 (-11%) / 384.8 (+63%) / 266.69 (+60%) | SW ↑54.2% | — |
|
||||
| L37/R1 | `3e443bf` | 3202193 | — | — | compile fail | — |
|
||||
| L37 fix/R2 | `7f2d84d` | 3202193 | 4.42 (+3%) / 1.73 (+1%) / 1.78 (+1%) / 32.2 (+3%) / 10.07 (+0%) | 38.96 (+65%) / 4.48 (+40%) / 5.25 (+46%) / 417.3 (+76%) / 259.39 (+55%) | — | — |
|
||||
| R3 | `945225d` | 3202193 | 4.86 (+13%) / 1.85 (+8%) / 1.91 (+9%) / 35.3 (+13%) / 10.69 (+6%) | 33.96 (+44%) / 2.68 (-17%) / 3.43 (-5%) / 374.9 (+58%) / 266.08 (+59%) | — | — |
|
||||
| L38/R4 | `cf57c87` | 3202193 | 3.14 (-27%) / 1.34 (-22%) / 1.36 (-23%) / 21.5 (-31%) / 5.96 (-41%) | 37.38 (+59%) / 4.72 (+47%) / 5.58 (+55%) / 396.3 (+67%) / 252.07 (+51%) | — | — |
|
||||
| R5 | `88c54ea` | 3202193 | 3.37 (-22%) / 1.46 (-15%) / 1.56 (-11%) / 30.5 (-2%) / 11.09 (+10%) | 39.45 (+67%) / 3.52 (+10%) / 4.89 (+36%) / 416.6 (+76%) / 265.16 (+59%) | SW ↑85.8% | — |
|
||||
| L39/R6 | `753cc9c` | 3202193 | 3.32 (-23%) / 0.86 (-50%) / 0.88 (-50%) / 21.7 (-31%) / 9.49 (-5%) | 37.03 (+57%) / 4.41 (+37%) / 5.56 (+54%) / 389.5 (+65%) / 232.40 (+39%) | — | — |
|
||||
| L40/R7 | `efa9e89` | 3202193 | 4.57 (+6%) / 1.62 (-5%) / 1.68 (-4%) / 35.3 (+13%) / 10.78 (+7%) | 38.63 (+64%) / 3.39 (+6%) / 3.89 (+8%) / 412.4 (+74%) / 306.46 (+84%) | SW ↑13.6%, FI ↑31.9% | — |
|
||||
| L41/R8 | `1abee91` | 3202193 | 4.30 (-0%) / 1.71 (+0%) / 1.81 (+3%) / 26.8 (-14%) / 8.08 (-20%) | 38.25 (+62%) / 2.88 (-10%) / 3.42 (-5%) / 413.8 (+75%) / 324.95 (+95%) | — | — |
|
||||
| L42/R9 | `2566f53` | 3202193 | 4.37 (+1%) / 1.51 (-11%) / 1.56 (-11%) / 34.1 (+9%) / 10.05 (+0%) | 37.43 (+59%) / 2.15 (-33%) / 2.48 (-31%) / 427.7 (+81%) / 306.73 (+84%) | SW ↑24.4% | — |
|
||||
| R9 | `497e974` | 3202193 | 4.55 (+6%) / 1.75 (+2%) / 1.79 (+2%) / 33.5 (+7%) / 10.31 (+3%) | 35.69 (+51%) / 4.87 (+52%) / 5.68 (+57%) / 372.0 (+57%) / 257.01 (+54%) | — | — |
|
||||
| L43/R10 | `422dd77` | 3202193 | 3.54 (-18%) / 1.60 (-6%) / 1.69 (-4%) / 23.7 (-24%) / 8.14 (-19%) | 37.50 (+59%) / 2.92 (-9%) / 3.37 (-7%) / 401.3 (+70%) / 320.95 (+92%) | FI ↑24.9% | — |
|
||||
| L44 | `036bd53` | 3202193 | 3.48 (-19%) / 0.95 (-44%) / 1.03 (-41%) / 30.2 (-3%) / 10.30 (+3%) | 36.78 (+56%) / 4.32 (+35%) / 5.20 (+44%) / 390.7 (+65%) / 239.12 (+43%) | SW ↑26.6% | — |
|
||||
| L45/R12 | `f13b145` | 3202193 | 4.50 (+5%) / 1.67 (-2%) / 1.75 (-1%) / 33.9 (+9%) / 10.38 (+3%) | 38.59 (+64%) / 4.50 (+40%) / 5.26 (+46%) / 410.7 (+74%) / 300.28 (+80%) | FI ↑25.6% | — |
|
||||
| L46/R13 | `e016f6a` | 3202193 | 4.15 (-4%) / 1.93 (+13%) / 2.01 (+14%) / 31.3 (+1%) / 9.64 (-4%) | 38.45 (+63%) / 4.10 (+28%) / 4.99 (+38%) / 406.5 (+72%) / 296.82 (+78%) | — | — |
|
||||
| L47/R14 | `c091e44` | 3202193 | 4.70 (+9%) / 1.84 (+8%) / 1.90 (+8%) / 35.6 (+14%) / 10.76 (+7%) | 39.16 (+66%) / 4.03 (+26%) / 4.72 (+31%) / 420.2 (+78%) / 274.84 (+65%) | SW ↑11.6% | — |
|
||||
| L48/R15 | `3073d1b` | 3202193 | 4.66 (+8%) / 1.54 (-10%) / 1.65 (-7%) / 37.4 (+20%) / 11.89 (+18%) | 37.86 (+61%) / 3.98 (+24%) / 4.82 (+33%) / 406.9 (+72%) / 289.92 (+74%) | SW ↑10.5% | — |
|
||||
| R16 | `6110f11` | 3202193 | 4.72 (+10%) / 1.47 (-14%) / 1.59 (-10%) / 33.1 (+6%) / 10.39 (+3%) | 35.86 (+52%) / 2.42 (-25%) / 3.66 (+1%) / 400.1 (+69%) / 284.96 (+71%) | — | — |
|
||||
| R17 | `b9ccff4` | 3202193 | 3.78 (-12%) / 1.33 (-22%) / 1.36 (-22%) / 34.4 (+10%) / 10.78 (+7%) | 38.70 (+64%) / 4.79 (+49%) / 5.58 (+55%) / 409.9 (+73%) / 282.19 (+69%) | — | — |
|
||||
| L50/R18-R19 | `ca85e87` | 3202193 | 4.95 (+15%) / 1.76 (+3%) / 1.84 (+4%) / 38.2 (+22%) / 11.85 (+18%) | 36.53 (+55%) / 4.80 (+50%) / 5.20 (+44%) / 381.1 (+61%) / 251.21 (+50%) | — | — |
|
||||
| L51/R20 | `7555025` | 3202193 | 4.44 (+3%) / 1.61 (-6%) / 1.67 (-5%) / 33.8 (+8%) / 10.63 (+6%) | 18.33 (-22%) / 2.32 (-28%) / 2.80 (-22%) / 174.2 (-26%) / 180.36 (+8%) | — | — |
|
||||
| L51 | `c42c785` | 3202193 | 4.47 (+4%) / 1.61 (-6%) / 1.67 (-5%) / 32.2 (+3%) / 9.50 (-5%) | 14.36 (-39%) / 2.78 (-13%) / 3.17 (-12%) / 140.5 (-41%) / 131.17 (-21%) | — | — |
|
||||
| L52/R21 | `408c14b` | 3202193 | 4.49 (+4%) / 1.58 (-7%) / 1.65 (-6%) / 35.5 (+14%) / 11.05 (+10%) | 12.50 (-47%) / 4.75 (+48%) / 5.20 (+44%) / 102.9 (-57%) / 125.17 (-25%) | SW ↑16.3% | — |
|
||||
| L53/R22 | `20f6c98` | 3202193 | 4.42 (+3%) / 1.66 (-3%) / 1.83 (+4%) / 29.4 (-6%) / 9.54 (-5%) | 11.57 (-51%) / 3.77 (+17%) / 4.18 (+16%) / 112.4 (-52%) / 134.18 (-20%) | — | — |
|
||||
| L54/R23 | `9458217` | 3202193 | 4.62 (+7%) / 1.50 (-12%) / 1.56 (-12%) / 35.6 (+14%) / 11.17 (+11%) | 14.71 (-38%) / 4.59 (+43%) / 5.11 (+42%) / 123.4 (-48%) / 137.96 (-17%) | SW ↑17.1% | — |
|
||||
| L54 | `e35a77f` | 3202193 | 2.66 (-38%) / 0.77 (-55%) / 0.81 (-54%) / 22.6 (-27%) / 8.05 (-20%) | 12.72 (-46%) / 2.50 (-22%) / 3.18 (-12%) / 94.6 (-60%) / 92.67 (-44%) | — | — |
|
||||
| L55/R24 | `aee1487` | 3202193 | 4.16 (-3%) / 1.73 (+1%) / 1.79 (+2%) / 27.7 (-11%) / 8.99 (-11%) | 10.74 (-54%) / 1.45 (-55%) / 1.65 (-54%) / 111.8 (-53%) / 146.17 (-12%) | SW ↑11.7%, FI ↑57.7% | 4.65/11.20; 94.70/122.83 |
|
||||
| L56/R25 | `4e64b75` | 3202193 | 3.11 (-28%) / 0.76 (-55%) / 0.79 (-55%) / 28.0 (-10%) / 10.12 (+1%) | 9.68 (-59%) / 2.65 (-17%) / 3.23 (-10%) / 87.3 (-63%) / 95.93 (-43%) | SW ↑12.5% | 4.61/11.62; 97.07/124.52 |
|
||||
| L57/R26 | `1e3e04b` | 3202193 | 5.11 (+19%) / 1.59 (-7%) / 1.67 (-5%) / 43.7 (+40%) / 14.12 (+41%) | 12.83 (-46%) / 2.48 (-23%) / 2.83 (-21%) / 125.4 (-47%) / 130.60 (-22%) | SW ↑39.6%, FI ↑36.1% | 4.77/11.47; 86.41/114.69 |
|
||||
| L58/R27 | `6c6cc37` | 3202193 | 3.72 (-14%) / 0.93 (-46%) / 0.98 (-44%) / 33.5 (+7%) / 10.09 (+0%) | 9.00 (-62%) / 1.93 (-40%) / 2.13 (-41%) / 85.1 (-64%) / 95.95 (-43%) | — | 4.62/11.10; 86.08/114.90 |
|
||||
| R28 | `fbae82e` | 3202193 | 4.85 (+13%) / 1.85 (+8%) / 1.90 (+8%) / 35.9 (+15%) / 11.24 (+12%) | 10.47 (-56%) / 2.18 (-32%) / 2.42 (-33%) / 99.6 (-58%) / 109.15 (-35%) | SW ↑11.5%, FI ↑13.8% | 4.71/11.40; 85.49/114.11 |
|
||||
| L60 | `c0c204b` | 3202193 | 3.22 (-25%) / 1.46 (-14%) / 1.49 (-16%) / 21.0 (-33%) / 8.59 (-15%) | 11.36 (-52%) / 2.61 (-19%) / 2.99 (-17%) / 101.0 (-57%) / 96.22 (-42%) | — | 5.21/11.99; 85.26/114.07 |
|
||||
| L61/R31 | `1359578` | 3202193 | 5.02 (+16%) / 1.79 (+5%) / 1.86 (+6%) / 38.7 (+24%) / 12.78 (+27%) | 13.38 (-43%) / 2.92 (-9%) / 3.26 (-10%) / 126.2 (-47%) / 131.47 (-21%) | SW ↑48.8%, FI ↑36.6% | 5.01/11.65; 85.11/113.95 |
|
||||
| R32 | `3202193` | 3202193 | 3.69 (-14%) / 1.61 (-6%) / 1.71 (-3%) / 27.2 (-13%) / 11.06 (+10%) | 12.73 (-46%) / 2.60 (-19%) / 2.94 (-18%) / 124.2 (-48%) / 131.70 (-21%) | — | 4.89/11.24; 86.41/114.52 |
|
||||
|
||||
## Four-strate U2 baseline
|
||||
|
||||
The local mesh windows are all nonempty and deterministic at W1/W8/W12 on Clang and GCC. Each hash vector matched across all six compiler/worker configurations. The cost numbers are medians of three W12 full-view runs using the comparable clip-radius-3 speed-matrix configuration, with each camera in the named stratum. Because the owner plan fails strict ownership and passage validation, these full-view cost runs use a temporary diagnostic binary that continues after those validators; no in-tree Core or Unreal `Source/` file was changed; the bypass is confined to `Saved/Linux/l64/u2cost/export`. The cost cameras were SW `viewer_start` `(0, 0, -496 m)`, FI passage-1 upper mouth `(19.166, -14.200, -732.201 m)`, TN passage-1 lower mouth `(19.166, -14.200, -753.374 m)`, and VS passage-2 lower mouth `(−20.063, −5.068, −900.354 m)`. The SW local mesh window above is the resolved route arrival, not its cost camera.
|
||||
|
||||
| Strate (chunks; content band) | Core generator path | Local window (nonempty/27; triangles) | Determinism | Whole-view W/F/N/Σ/L0 | Arrival→departure audit |
|
||||
|---|---|---|---|---|---|
|
||||
| SW (0…−63; −503.5…7.5 m) | SurfaceWorld-native | route spawn: 9/27; 25,672 | W1/W8/W12 Clang/GCC: PASS | 6.34/0.63/0.80/68.8/60.39 | SW route fails: 24/33 points unsupported; SW→FI interface fails |
|
||||
| FI (−64…−93; −743.5…−504.5 m) | compiled-analytic/FloatingIslands (legacy) | passage 1 upper mouth: 21/27; 93,149 | W1/W8/W12 Clang/GCC: PASS | 8.41/0.53/0.97/95.1/74.21 | SW→FI lower mouth out of band; FI→TN interface fails |
|
||||
| TN (−94…−109; −871.5…−744.5 m) | compiled-analytic/TunnelNetwork (legacy) | passage 1 lower mouth: 13/27; 65,248 | W1/W8/W12 Clang/GCC: PASS | 6.50/0.53/0.79/71.6/56.33 | FI→TN and TN→VS interfaces fail |
|
||||
| VS (−110…−125; −999.5…−872.5 m) | compiled-analytic/VerticalShafts (legacy) | passage 2 lower mouth: 5/27; 4,761 | W1/W8/W12 Clang/GCC: PASS | 3.39/0.52/0.54/34.6/14.51 | TN→VS: 3 unsupported; floor ripple 3.57 cm > 2.5 cm |
|
||||
|
||||
Across the three interface audits, 0/3 passages passed. In addition to the out-of-band 0–1 mouth, passage 1–2 has floor ripple 4.78 cm (2.5 cm limit), and 2–3 has 3.57 cm ripple; unsupported/blocked samples also remain. Native caves and lattice are not enabled in this plan; TN and VS use their legacy analytic evaluators.
|
||||
|
||||
## Failed runs
|
||||
|
||||
- Historical build `3e443bf` (L37/R1) failed at `ShapeWorldInternal.h:1554` with an extra `)`; the next L37 fix built and was measured. The corrected bisect has 402/402 successful timing runs. An earlier discarded invocation used unsupported `--data=` syntax and was replaced by the positional data path.
|
||||
- `verify.py --tier fast --out Saved/Linux/l64` in the copied legacy-plan shadow reached the motif fixture and exited 1: `motif-slice1-capsule-reachability-clang` rejected its motif override because it did not resolve to a SurfaceWorld strate. Its hard-coded two-strata style map does not validate U2; U2 boundary, determinism, and passage audits are reported above. A first stale-source-stamp refusal was corrected by copying the L37 audit script into the shadow.
|
||||
- The first U2 whole-view SW pass exited at the passage walkability gate (`65/7,729` samples solid). The cost table reruns use the same diagnostic binary for all four strata with route validation bypassed, and retain the audit failure. A first build attempt also used a nonexistent `TMPDIR`; it was corrected and the diagnostic build passed.
|
||||
|
||||
Evidence is under `Saved/Linux/l64`: `runs-spawn-correct.json` and `runs-spawn-correct/`, `profiles-spawn.json`, `u2probe/determinism.json`, `u2probe/passage-audit-clang.log`, `u2probe/boundary-dispatch.log`, `u2cost/speed-cost.json`, and `verify-fast.log`.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
**The bisect timings are inflated by machine contention.** I was building Unreal Engine on cores 12–19 during this
|
||||
round (memory bandwidth and disk I/O), and adjacent commits swing ±40%. A clean re-measure on a quiet machine
|
||||
(6 interleaved runs each, same binaries, data `owner_world-3202193.json`, same command and camera):
|
||||
|
||||
| Build | Whole view (median of 6) | Worker-s, LOD0–8 | LOD0 ms/tile |
|
||||
|---|---:|---:|---:|
|
||||
| L14 `a176d07` | ≈1.55 s | 12.62 | 3.99 |
|
||||
| L61 `1359578` | ≈1.72 s | 14.65 | 4.99 |
|
||||
|
||||
**Core regressed about +16% in worker time and +25% per LOD0 tile, not ×2.** The owner's Windows trace shows about
|
||||
33 ms per LOD0 tile p95, roughly 6× Core's per-tile cost here, so most of the Windows regression is on the Unreal side
|
||||
or isn't a like-for-like comparison (R33: U1's trace was not a controlled A/B, at 2,578 tiles). Core's own +16% is
|
||||
plausibly R33 suspect #1: per-sample `ApplyResolvedStrateComposition` (L23) plus the per-sample passage loop (L37).
|
||||
**The new four-strate plan has real defects:**
|
||||
- passage 0–1's lower mouth is out of FI's band;
|
||||
- all three interfaces fail their audits;
|
||||
- FI, TN and VS run on legacy evaluators;
|
||||
- the full view costs 3.4–8.4 s per strate.
|
||||
|
||||
Next: get the Linux UE build to compile, then trace the Unreal path itself for L14 vs HEAD here.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Linux65 report
|
||||
|
||||
Date: 2026-10-26. No commit, push, or stash was made. The pre-existing untracked `Linux18/exports/` was left alone.
|
||||
|
||||
## Linux Unreal build
|
||||
|
||||
UE 5.7.4 / bundled Clang 20 builds pass with zero errors. The clean HEAD build completed 78 actions in 93.54 s; L14 (`a176d07`) completed 157 actions in 170.69 s. Final incremental builds after the Linux-only trace timing guards also pass. Logs are `Saved/Linux/l65/head-build5.log`, `l14-build1.log`, `head-build-final.log`, and `l14-build-final.log`.
|
||||
|
||||
Builds used CPUs 0–11, a 6 GiB RSS limit, at most 8 actions, AVX2, and `-NoUBA`. UBA stalled before starting compiler actions. Three existing RMC deprecation warnings remain: two `RemoveSectionGroup` calls and one `CreateSectionGroup` call in `VoxelWorld.cpp`.
|
||||
|
||||
Fixes:
|
||||
|
||||
- The Compat wrappers now forward to UE headers with `include_next` for generated Linux module TUs. `CoreNamespacePreamble.h` opts canonical Core TUs into the Compat definitions. `CoreMinimal.h` undefines `WITH_EDITOR` before defining the standalone value.
|
||||
- `VoxelForgeCore.Build.cs` enables that forwarding and AVX2 only on Linux. `VoxelForge.Build.cs` disables Unity only on Linux to avoid duplicate anonymous test helpers.
|
||||
- `VoxelContentManager.cpp` uses `std::int64_t` consistently; `VoxelRedefinedDensityField.cpp` groups the mixed `&&`/`||` expression.
|
||||
- Linux-only trace fields measure Core `generateTile`, mesh copy, and RMC stream-set construction. The `PLATFORM_LINUX` guards keep Windows worker behavior and trace JSON unchanged.
|
||||
|
||||
### Files changed for the owner's Windows build review
|
||||
|
||||
All `Source/` changes:
|
||||
|
||||
- `Source/VoxelForge/Private/VoxelContentManager.cpp`
|
||||
- `Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp`
|
||||
- `Source/VoxelForge/Private/VoxelStartupTrace.cpp`
|
||||
- `Source/VoxelForge/Private/VoxelWorld.cpp`
|
||||
- `Source/VoxelForge/Public/VoxelStartupTrace.h`
|
||||
- `Source/VoxelForge/Public/VoxelWorld.h`
|
||||
- `Source/VoxelForge/VoxelForge.Build.cs`
|
||||
- `Source/VoxelForgeCore/VoxelForgeCore.Build.cs`
|
||||
|
||||
All changed `Standalone/Compat` headers:
|
||||
|
||||
- `AssetRegistry/AssetData.h`, `AssetRegistry/AssetRegistryModule.h`
|
||||
- `Async/ParallelFor.h`, `CoreMinimal.h`
|
||||
- `Dom/JsonObject.h`, `Dom/JsonValue.h`, `Engine/DataAsset.h`, `GameplayTagContainer.h`
|
||||
- `HAL/CriticalSection.h`, `HAL/Event.h`, `HAL/FileManager.h`, `HAL/IConsoleManager.h`, `HAL/PlatformProcess.h`, `HAL/PlatformTime.h`
|
||||
- `Json.h`, `Math/RandomStream.h`
|
||||
- `Misc/CommandLine.h`, `Misc/Crc.h`, `Misc/FileHelper.h`, `Misc/Parse.h`, `Misc/Paths.h`, `Misc/ScopeLock.h`, `Misc/ScopeRWLock.h`, `Misc/SecureHash.h`
|
||||
- `Modules/ModuleManager.h`, `ProfilingDebugging/CpuProfilerTrace.h`
|
||||
- `Serialization/CustomVersion.h`, `Serialization/JsonSerializer.h`, `Serialization/JsonWriter.h`, `Stats/Stats.h`
|
||||
- `Templates/SubclassOf.h`, `Templates/UniquePtr.h`
|
||||
- `UObject/EnumProperty.h`, `UObject/FieldIterator.h`, `UObject/Object.h`, `UObject/UObjectGlobals.h`, `UObject/UnrealType.h`
|
||||
|
||||
Also changed: `Standalone/Core/src/CoreNamespacePreamble.h`.
|
||||
|
||||
## Unreal spawn trace
|
||||
|
||||
The U1 trace is `Tools/TestVoxelForgeCoreWorldHost.ps1 -TimeoutSeconds 900 -CoreVoxelSizeCm 25 -StreamingTraceOnly`. It runs `UnrealEditor-Cmd -run=VoxelForgeExplore` with `-coreworldtest=1`, `-coreworldstreamtrace=1`, `/Game/VoxelForge/DA_Settings.DA_Settings`, seed 0, 25 cm Core voxels, 600 cm/s movement, and the Legacy/default streaming flags. It creates a transient `UWorld`; no map is loaded.
|
||||
|
||||
The exact pre-U2 owner packages are unavailable in this Linux workspace. Both hosts used identical copies of the five current `/home/jahni/VoxelM/DA_*.uasset` files (post-U2), with matching SHA-256s in both projects:
|
||||
|
||||
| Asset | SHA-256 |
|
||||
|---|---|
|
||||
| `DA_Settings` | `8abe8243104980260feca0a6f4cf4927768e8e568221a6f6a2c2694f6d15d454` |
|
||||
| `DA_Strate1` | `a9ca6cddd102250a893e379350b1d0f4aaf04c4f7fca70e0db46052a067d2cfb` |
|
||||
| `DA_Strate2` | `8f9a12a3181635149e6f79f192e0ddaf6b6577442ee324071a7749dcc78b9a0c` |
|
||||
| `DA_Strate3` | `5470d538c3c5e88237463e71bca9e2eb170dfe3a0543c0f33298d9fb010fd2f9` |
|
||||
| `DA_Strate4` | `a98e33e1cd42f374d6cb70f5071e8ebba74d91ed59d2fd3201782dab84d28f5c` |
|
||||
|
||||
The minimal host lacked the two First Person Blueprints used by the commandlet's speed fixture. Stock UE template copies also lacked a valid sprint graph, so the isolated trace build trees used the test's established 600/6000 speed defaults only for `-StreamingTraceOnly`; no tracked source was changed for this workaround. The trace loop itself takes its 600 cm/s movement value from the command line.
|
||||
|
||||
| Measure | L14 | HEAD (`e157847`) |
|
||||
|---|---:|---:|
|
||||
| Tiles per LOD | Not reached | Not reached |
|
||||
| Cells per tile | Not reached | Not reached |
|
||||
| Initial worker-seconds | Not reached | Not reached |
|
||||
| LOD0 tile worker p50 / p95 | Not reached | Not reached |
|
||||
| Full initial set ready | Not reached | Not reached |
|
||||
| Walkable ring count / ready time | Not reached | Not reached |
|
||||
| `generateTile` / mesh copy / RMC stream build | Not reached | Not reached |
|
||||
|
||||
Both commandlets stop before spawning `AVoxelWorld` with the same error: `SurfaceWorld route candidates failed the capsule and support sweep.` No tile trace file was produced. Thus this run cannot identify the Windows worker-time regression or separate Core generation from Unreal packaging. The failure is consistent across L14 and HEAD, but whether the post-U2 asset set or the route audit itself is responsible is unresolved; no fix is justified from this run.
|
||||
|
||||
L14 was exported from `a176d07` to the requested `Saved/Linux/l65/l14tree/Plugins/VoxelForge`. The sandbox did not permit creating the sibling `/home/jahni/VFHostL14`, so its second host project is nested at `/home/jahni/VFHost/VFHostL14/VFHostL14.uproject`. The supplied `/home/jahni/VFHost/VFHost.uproject` was restored byte-for-byte after testing. RealtimeMeshComponent's original source tree was not modified.
|
||||
|
||||
## Failed runs
|
||||
|
||||
- Initial builds hit a read-only UBT config path, then a read-only RMC intermediate timestamp, then a UBA stall. The next compile found Linux Unity duplicate test helpers; disabling Unity on Linux fixed it. A final incremental attempt made after restoring the original host descriptor again hit the RMC read-only timestamp; rebuilding with the local RMC copy passed.
|
||||
- Two early trace launches rejected an output directory outside the host project. The next launch stopped on the absent staged character Blueprint. Stock UE template Blueprints then failed to provide a sprint value; the trace-only defaults got past that gate. Both final revisions then stopped at the identical SurfaceWorld route audit above.
|
||||
|
||||
There were 12 build invocations, within the 15-attempt Part A limit.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted. **The Linux UE build compiles VoxelForge with 0 errors** (HEAD and L14). I checked that the Compat and
|
||||
`Source/` changes don't alter Core output: standalone Clang and GCC rebuilds produce **identical whole-view hashes**
|
||||
to `3202193` on the pre-U2 data, for both SW and FI. The RealtimeMeshComponent tree is unmodified: its 203 git "M"
|
||||
entries are CRLF-only (empty diff with whitespace and CR ignored, no file newer than this round outside
|
||||
Intermediate/Binaries). The Unreal trace is blocked for both L14 and HEAD by the **owner's new assets**:
|
||||
`SurfaceWorld route candidates failed the capsule and support sweep` before `AVoxelWorld` spawns (consistent with
|
||||
L64: SW route 24/33 unsupported). The `verify.py` tiers still assume the old two-strate owner data and fail on the U2
|
||||
export; they need a pinned regression dataset plus new-plan baselines. `Source/` files to include in the owner's next
|
||||
Windows build are listed above.
|
||||