Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5389abed2e | ||
|
|
35f0a67fe3 | ||
|
|
1e111cc042 | ||
|
|
70c37aaab3 | ||
|
|
8805291d3e | ||
|
|
a7a03d42b4 | ||
|
|
04cdd197fb | ||
|
|
fa78192c86 | ||
|
|
0fe7f371f7 | ||
|
|
4f5f6667ae | ||
|
|
06f558dc91 | ||
|
|
0a98da6fd0 | ||
|
|
a81b5b7d06 | ||
|
|
f572108dc4 | ||
|
|
9cb5e53b17 | ||
|
|
b06397df9c | ||
|
|
2c8a26c0d0 | ||
|
|
ec20994a8e | ||
|
|
5e091d254c | ||
|
|
68cba5a51d | ||
|
|
2dfe599148 | ||
|
|
90378a585f | ||
|
|
9d5455ec68 | ||
|
|
c67b2006aa | ||
|
|
bd1b2a8f1d | ||
|
|
d970d75be8 | ||
|
|
971206c2ea | ||
|
|
018c0e1ec8 | ||
|
|
bd68aee1ca | ||
|
|
81e2452a3d | ||
|
|
bd7bba994c | ||
|
|
087333bf8a | ||
|
|
5e2fc53d39 | ||
|
|
47148e0a0d | ||
|
|
e157847509 | ||
|
|
9136f5f0e7 | ||
|
|
fc9227f9e0 | ||
|
|
32021930d2 | ||
|
|
135957831b | ||
|
|
c0c204ba50 | ||
|
|
fbae82e024 | ||
|
|
6c6cc37e1d | ||
|
|
1e3e04bbac | ||
|
|
4e64b75044 | ||
|
|
aee1487410 | ||
|
|
e35a77f676 | ||
|
|
9458217d34 | ||
|
|
20f6c98ec5 | ||
|
|
408c14baff | ||
|
|
c42c785e2c | ||
|
|
75550256ef | ||
|
|
ca85e876ad | ||
|
|
b9ccff4ad6 | ||
|
|
6110f118e3 | ||
|
|
3073d1b2fd | ||
|
|
c091e4434f | ||
|
|
e016f6a20f | ||
|
|
f13b145f23 | ||
|
|
036bd530fc | ||
|
|
422dd7761a | ||
|
|
497e974178 | ||
|
|
2566f53734 | ||
|
|
1abee91721 | ||
|
|
efa9e89468 | ||
|
|
753cc9c259 | ||
|
|
88c54eab44 | ||
|
|
cf57c87daf | ||
|
|
945225d942 | ||
|
|
7f2d84d2c7 |
No files matched your search
+154
@@ -2162,6 +2162,160 @@ recipe/style audits, route support, open-air, strict layout, legacy validators,
|
|||||||
paired-cost results are in
|
paired-cost results are in
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
||||||
|
|
||||||
|
### 8.50 Immutable field binding — World Assembler M3 L40
|
||||||
|
|
||||||
|
`FStandaloneShapeWorld` now keeps a sorted `FBoundStratumField` table built from the resolved layout
|
||||||
|
at world construction. Each entry owns a concrete SurfaceWorld, FloatingIslands, or legacy adapter
|
||||||
|
in `FGeneratorFieldAdapterV1`, plus the resolved bounds and native plan pointer. The adapters share
|
||||||
|
the immutable `densityAt(x,y,z,out)` shape, while `std::variant` keeps dispatch concrete instead of
|
||||||
|
adding a virtual call to every density sample. FloatingIslands keeps its original fixed-point entry
|
||||||
|
point for sample evaluation and its boundary parameter table is captured when the adapter is bound.
|
||||||
|
|
||||||
|
`DensityAt`, native SurfaceWorld/FloatingIslands evaluation, legacy SurfaceWorld bridging and
|
||||||
|
`ClassifyBoxDetailed` select from the bound table. The native adapters no longer ask the manager
|
||||||
|
which generator or plan to use per sample; box proof reuses the bound field objects. The existing
|
||||||
|
float-to-fixed conversion order, density arithmetic and shared boundary composition remain intact.
|
||||||
|
The manager's layout-width value is captured once for the legacy-compatible chunk-index projection.
|
||||||
|
|
||||||
|
The legacy adapter is still a compatibility bridge: `UVoxelGenerator::GetDensityAt` retains its
|
||||||
|
own manager routing when invoked for legacy SurfaceWorld fields or out-of-plan fallback queries.
|
||||||
|
Other shared-composition policy branches and passage/boundary planning remain outside this slice.
|
||||||
|
No Unreal adapter or `VoxelWorld.cpp` change was needed, and the evaluator version is unchanged.
|
||||||
|
L37/L39 owner hashes are the parity baseline; the L40 report records fast, medium, full and paired
|
||||||
|
cost evidence.
|
||||||
|
|
||||||
|
### 8.51 Resolved metre layout authority — World Assembler M3 L41
|
||||||
|
|
||||||
|
`UVoxelStrateManager::Initialize` now resolves the layout in integer micrometres before passages or
|
||||||
|
Core work are scheduled. The configured layout width is rounded once to micrometres; legacy height
|
||||||
|
and inter-strate gap counts are multiplied by that width. Each slot stores its half-open physical
|
||||||
|
interval `[bottom, top)`, height, transition mode/width, copied passage settings and their physical
|
||||||
|
dimensions. The effective world seed is captured in the manager before passage generation.
|
||||||
|
|
||||||
|
The historical 8 m layout keeps its old placement: the first upper edge is +8 m, and each authored
|
||||||
|
chunk of height or gap is exactly 8 m. `TopChunkZ` and `BottomChunkZ` are floor-based projections of
|
||||||
|
the resolved interval, including mathematical floor for negative Z. Season schema v2 still validates
|
||||||
|
its aligned top/bottom chunk values and stacked order as before, then maps those accepted projections
|
||||||
|
to the same physical intervals. L29's facing ceiling/bedrock bands are derived from these physical
|
||||||
|
intervals and the copied transition width, so their metre bounds retain the prior chunk-based values.
|
||||||
|
|
||||||
|
The Core adapter builds SurfaceWorld and FloatingIslands bounds, shared-layout bands, and resolved
|
||||||
|
passage dimensions from the micrometre fields. FI and SW both receive the same
|
||||||
|
`LayoutChunkWidthSubvoxels = round(layoutWidthMicrometres * 256 / coreVoxelMicrometres)`; layout
|
||||||
|
conversion does not use the owner/Core scale. Owner/Core ratios remain in the legacy FloatingIslands
|
||||||
|
parameter adapter for generator dimensions that are still authored in owner voxels. The manager's
|
||||||
|
legacy passage builder converts resolved passage micrometres back to owner voxels only at its legacy
|
||||||
|
geometry API boundary.
|
||||||
|
|
||||||
|
Chunk coordinates remain in `GetStrateForChunk`, `GetGeneratorTypeForChunk`, chunk-parameter queries,
|
||||||
|
season-v2 validation, and legacy generator dispatch because those interfaces accept layout-chunk
|
||||||
|
indices. Their values are cached projections from the physical slot bounds. World-Z queries and the
|
||||||
|
Core snapshot use the half-open metre intervals. L40's bound Core field table remains the native
|
||||||
|
per-sample dispatch path; this migration adds no manager lookup to it. The Unreal manager and adapter
|
||||||
|
cannot be compiled in this Linux workspace, so their small source changes still need the Windows
|
||||||
|
Unreal build before integration.
|
||||||
|
|
||||||
|
### 8.52 Resolved boundary and landing capabilities — World Assembler M3 L42
|
||||||
|
|
||||||
|
`ResolvedLayoutSnapshotV3::finalize` materializes runtime capability records from the immutable
|
||||||
|
strate/interface fields before sampling begins. The upper interface records whether its Gradient FI
|
||||||
|
bedrock air gap remains open and whether a SurfaceWorld-to-TunnelNetwork passage gets the special
|
||||||
|
landing support profile. Per-strate passage records carry the existing SurfaceWorld landing profile,
|
||||||
|
FloatingIslands route-support behavior, and passage-anchor column center policy. Shared boundary and
|
||||||
|
passage composition reads these records; it no longer compares generator-kind strings.
|
||||||
|
|
||||||
|
The records are deterministic derivatives of `generator_kind`, `transition_mode`, and the resolved
|
||||||
|
adjacent strate, and are omitted from canonical JSON. Those source fields remain in the hashed layout,
|
||||||
|
so owner layout IDs and density hashes retain their L41 identity. The upper strate still controls
|
||||||
|
Hard/Gradient/Interleaved evaluation; each strate still composes ceiling, content, then breakable
|
||||||
|
bedrock. L29 transition samples and the passage-mouth fade arithmetic are unchanged.
|
||||||
|
|
||||||
|
`BindResolvedStratumFields` still tests the resolved generator kind once to bind the concrete
|
||||||
|
SurfaceWorld, FloatingIslands, or legacy evaluator. Passage planning in `ResolvedLayoutInternal.h`
|
||||||
|
also retains generator-specific anchor and default-mouth placement rules; those are outside the
|
||||||
|
shared composition boundary policy. No Unreal source changed. See the L42 report for hashes, tier
|
||||||
|
results and paired cost measurements.
|
||||||
|
|
||||||
|
### 8.53 Shared Core landmark SDF stamps — World Assembler M3 L43
|
||||||
|
|
||||||
|
`ResolvedLayoutStampV1` adds a small, version-1 SDF vocabulary to the shared resolved layout:
|
||||||
|
carve ellipsoid, carve arch, fill pillar and fill dais. Requests and resolved records use integer
|
||||||
|
micrometres and carry a stable ID, strate, position, conservative bounds, half extents, smooth-CSG
|
||||||
|
width, bounded irregularity amplitude/wavelength and quarter-turn orientation. A stamp's seeded
|
||||||
|
noise and bounded placement offsets derive from `(world seed, strate, stable ID)`; records are
|
||||||
|
sorted by stable ID before the layout identity is finalized. If no request is made, the stamp fields
|
||||||
|
are omitted from canonical JSON, preserving the no-stamp layout identity and density hashes.
|
||||||
|
|
||||||
|
Resolution retries a finite, deterministic set of candidates and rejects a record if its complete
|
||||||
|
bound intersects a passage mouth or walk corridor, reaches a strate content face, or overlaps the
|
||||||
|
ceiling or bedrock band. The director reports failure if no safe candidate exists. The strict-layout
|
||||||
|
negative control forces a candidate onto a route and into a ceiling band and expects both rejections.
|
||||||
|
|
||||||
|
`FStandaloneShapeWorld` applies stamps after the shared resolved-strate boundary and passage
|
||||||
|
composition. Carves use smooth maximum with the negated stamp SDF; fills use smooth minimum. Seeded
|
||||||
|
trilinear value noise adds low-frequency relief without changing the conservative bound. The tile
|
||||||
|
query scope builds a BVH candidate list from each tile's sample bounds. Samples in a tile outside
|
||||||
|
every stamp box do no stamp SDF evaluation. Box classification marks stamp-intersecting boxes mixed,
|
||||||
|
and native-only readiness refuses to skip them.
|
||||||
|
|
||||||
|
The `--landmark-chamber-demo` recipe flag adds an irregular ellipsoid chamber, a separate arched
|
||||||
|
entrance and a low fill dais in the owner SurfaceWorld strate, about 31 m from the passage arrival
|
||||||
|
mouth and off the walk route. It stays disabled by default. Active records carry vocabulary version
|
||||||
|
1 in the layout identity, making the demo an explicit world change; no Unreal adapter was added.
|
||||||
|
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 slice 1 (Linux L58)
|
||||||
|
|
||||||
|
`Core.Lattice.PrimitiveField` is a version-1 recipe selected only for one Maze or VerticalShafts
|
||||||
|
strate by `--recipe=lattice --lattice-strate=N`. The legacy generator assets and their default
|
||||||
|
dispatch stay unchanged. Planning first binds the actual incoming lower mouth and outgoing upper
|
||||||
|
mouth from the resolved passage records, then creates arrival/exit overlap corridors, a mandatory
|
||||||
|
four-turn helix and the maze spine; two deterministic dead ends are added after that route. The
|
||||||
|
resolved room and edge seeds use `(world seed, strate, recipe version, stable ID)`. An audit control
|
||||||
|
translates the helix 20 m away from both route junctions to make the shaft network orphaned.
|
||||||
|
|
||||||
|
The prototype helix uses a 6 m centreline radius, 3 m clear width, 5 m pitch, four turns and 2.6 m
|
||||||
|
finished floor-to-soffit height. Its centreline descends 20 m over roughly 151 m; the inner-edge
|
||||||
|
grade is about 10.0 degrees. The 2.4 m floor-wall smooth-max gives an isolated ideal fillet radius
|
||||||
|
of 0.8485 m. A separate 0.4 m feature-union blend prevents adjoining ramps from erasing the tread.
|
||||||
|
The swept-clear diameter is 15 m; the conservative padded feature envelope is about 20.6 m (the
|
||||||
|
approved approximately 21 m site envelope).
|
||||||
|
|
||||||
|
The layout reuses the resolved cavern graph vocabulary. A per-strate opt-in selects the native
|
||||||
|
negative-solid/positive-air field; canonical feature order is stable-ID order. Room/corridor bounds
|
||||||
|
are indexed by the graph BVH and polyline segments have a nested deterministic BVH for density and
|
||||||
|
interval queries. Box classification stays conservative, and `isTileShapeFree` refuses any tile
|
||||||
|
that intersects the graph. The L58 pipeline audits the composed field from actual mouth to mouth
|
||||||
|
with a 0.34 m by 1.76 m capsule, per-station support, 2.2 m clearance and a 15 degree grade limit;
|
||||||
|
it also checks the orphan control, brute-force interval samples and the composed wall-foot fillet.
|
||||||
|
The Linux58 report contains the measured reachability, surface, peer-determinism, timing and capture
|
||||||
|
results; the code path is not promoted to legacy Maze/VerticalShafts generation.
|
||||||
|
|
||||||
|
### 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)
|
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||||
|
|
||||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||||
|
|||||||
+108
-24
@@ -30,7 +30,7 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
|||||||
| Term | Meaning |
|
| Term | Meaning |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| Legacy chunk | 32×32×32 voxels (`CHUNK_SIZE`). At the legacy 12.5 cm voxel size it spans 4 m; it is not the strate-layout unit or the Core tile. |
|
| Legacy chunk | 32×32×32 voxels (`CHUNK_SIZE`). At the legacy 12.5 cm voxel size it spans 4 m; it is not the strate-layout unit or the Core tile. |
|
||||||
| Strate-layout chunk | World-level `StrateLayoutChunkSizeMeters`, default 8 m, shared by all generators and independent of voxel size. The director resolves it once and Core reads the resolved snapshot. At 25 cm it is 32 Core voxels; at 20 cm it is 40 Core voxels. |
|
| Strate-layout chunk | World-level `StrateLayoutChunkSizeMeters`, default 8 m, shared by all generators and independent of voxel size. `UVoxelStrateManager::Initialize` resolves it to integer micrometres; slot bounds, heights, gap and transition widths use those physical values. Core derives the common `LayoutChunkWidthSubvoxels` from that same width. At 25 cm it is 32 Core voxels; at 20 cm it is 40 Core voxels. |
|
||||||
| Core tile | Internal 32×32×32 Core voxel tile. Its metre size follows the Core voxel size and stays separate from the configurable layout chunk. |
|
| Core tile | Internal 32×32×32 Core voxel tile. Its metre size follows the Core voxel size and stays separate from the configurable layout chunk. |
|
||||||
| Density | Scalar field. `< 0` solid, `> 0` air, `0` = surface (`IsoLevel`). |
|
| Density | Scalar field. `< 0` solid, `> 0` air, `0` = surface (`IsoLevel`). |
|
||||||
| Strate | Vertical layer of the world, stacked downward. Has its own generator + params. |
|
| Strate | Vertical layer of the world, stacked downward. Has its own generator + params. |
|
||||||
@@ -70,8 +70,10 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
|||||||
(RealtimeMeshComponent)
|
(RealtimeMeshComponent)
|
||||||
```
|
```
|
||||||
|
|
||||||
`UVoxelStrateManager` is the side oracle: "what strate is at this Z, what params,
|
`UVoxelStrateManager` owns resolved half-open metre intervals (`[bottom, top)`) and is the side
|
||||||
what generator type, and is there a passage/elevator SDF near here?"
|
oracle: "what strate is at this Z, what params, what generator type, and is there a passage/elevator
|
||||||
|
SDF near here?" Integer chunk bounds on each slot are compatibility projections for existing chunk
|
||||||
|
dispatch APIs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,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
|
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
|
**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
|
in metres and converted at runtime. The live
|
||||||
strate generator continues to read voxel-valued parameters from its assets, so those authored
|
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.
|
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
|
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**
|
**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
|
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
|
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.
|
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
|
The owner's voxel-valued live strate shape settings are not migrated by U1: unchanged saved values
|
||||||
produce half-size rooms, tunnels, features and noise periods in metres. See the step-3 migration list
|
therefore produce half-size rooms, tunnels, features and noise periods in metres. See the U1 report
|
||||||
in the handoff report for the exact `DA_Settings` fields and values.
|
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`,
|
`ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`,
|
||||||
`StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`,
|
`StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`,
|
||||||
`EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region
|
`EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region
|
||||||
@@ -108,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
|
`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.
|
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
|
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
|
former 512 m span. U1 automatically converts only strate height and transition widths and the
|
||||||
re-authored; the editor-only research field is the step-3 target.
|
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`):
|
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³
|
the 519³ field took **7.133834–7.825292 s** across three runs (median **7.347496 s**); the 259³
|
||||||
@@ -168,13 +174,16 @@ remains `0`, and lateral regions remain gated off.
|
|||||||
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
|
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
|
||||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
|
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
|
||||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. |
|
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. |
|
||||||
|
| `ResolvedLayoutStratePassageCapabilitiesV1::{hasNativeCaveField,hasNativeLatticeField}`, `IsNativeLatticeFieldStratum`, `HasNativeGraphField` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit per-strate native graph capability; `Core.Lattice.PrimitiveField` is restricted to Maze/VerticalShafts recipe version 1. Lattice seeds include world seed, strate, recipe version and stable feature ID; explicit polyline records keep their endpoints and receive conservative bounds. §8.54. |
|
||||||
|
| `FCavernRuntimeFeature::FSegmentNode`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | A deterministic nested segment BVH accelerates native graph distance and conservative interval evaluation; the outer graph BVH still supplies tile candidates and protects `isTileShapeFree`. The 0.4 m feature-union width is separate from the floor fillet width. §8.54. |
|
||||||
| `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
|
| `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
|
||||||
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
|
| `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`. |
|
| 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). |
|
| `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. |
|
| `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. |
|
||||||
| `Source/VoxelForge/Public/VoxelSettings.h` / `Private/VoxelSettings.cpp` | `UVoxelSettings` owns optional canonical world-assembly JSON and its Core digest. The editor file picker plus `ImportWorldAssemblyManifestJson` imports the reviewed manifest into the cooked settings asset and validates the recipe seed against the effective settings seed. |
|
| `Source/VoxelForge/Public/VoxelSettings.h` / `Private/VoxelSettings.cpp` | `UVoxelSettings` owns optional canonical world-assembly JSON and its Core digest. The editor file picker plus `ImportWorldAssemblyManifestJson` imports the reviewed manifest into the cooked settings asset and validates the recipe seed against the effective settings seed. |
|
||||||
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Validates the optional manifest against each resolved `SurfaceWorld` slot (unique strate index and exact top/bottom chunks), then exports canonical `resolved_layout` separately from `world_assembly` and its hash. The layout digest does not alter recipe hashes. Invalid plans fail Core initialization. |
|
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Resolves Core's common `LayoutChunkWidthSubvoxels` from the manager's physical layout width; builds SurfaceWorld/FloatingIslands bounds, L29 bands and passage settings from resolved micrometre slot fields; keeps owner/Core ratios in the legacy FloatingIslands parameter adapter. Chunk fields validate imported plans as compatibility projections. Exports canonical `resolved_layout` separately from `world_assembly` and its hash. Invalid plans fail Core initialization. |
|
||||||
| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. |
|
| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. |
|
||||||
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. |
|
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. |
|
||||||
| `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. |
|
| `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. |
|
||||||
@@ -598,13 +607,16 @@ content lists, audio, `GameplayTags`(393). EditConditions show/hide param groups
|
|||||||
type.
|
type.
|
||||||
|
|
||||||
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:152).
|
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:152).
|
||||||
Maps depth→strate at runtime; owns passages.
|
Resolves each slot's physical metre interval, layout gap, transition width, world seed and passage
|
||||||
|
settings during `Initialize`, then owns passages. World-Z APIs query the physical intervals; legacy
|
||||||
|
chunk APIs use the cached integer projection.
|
||||||
- `FVoxelPassage` (h:46): standing endpoints, radius, type, wandering control points, and
|
- `FVoxelPassage` (h:46): standing endpoints, radius, type, wandering control points, and
|
||||||
upper/lower local landing descriptors (room, floor, and door; no root connector).
|
upper/lower local landing descriptors (room, floor, and door; no root connector).
|
||||||
- `FStrateSlot` (h:128): definition + chunk-Z range + index.
|
- `FStrateSlot` (h:128): content definition, resolved micrometre bounds/height, resolved transition
|
||||||
|
and passage inputs, and compatibility chunk-Z projection + strate index.
|
||||||
| Method | .cpp line | Role |
|
| Method | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| `Initialize` | 1164 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
|
| `Initialize` | 1186 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
|
||||||
| `GeneratePassages` | 1659 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized local room/floor. The origin landing is reserved for the future shaft; no landing-to-root road is generated. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
|
| `GeneratePassages` | 1659 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized local room/floor. The origin landing is reserved for the future shaft; no landing-to-root road is generated. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
|
||||||
| `EvaluateModifierSDF` | 2643 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. `FPassageEvaluationCache::FloorProjections` reuses native/generic floor projections and walkable-air results once per `(sample, passage)` using exact double XYZ equality and unchanged float outputs; `voxel.FloorRound1PassageProjectionCache=0` disables only this A/B cache. §8.10. |
|
| `EvaluateModifierSDF` | 2643 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. `FPassageEvaluationCache::FloorProjections` reuses native/generic floor projections and walkable-air results once per `(sample, passage)` using exact double XYZ equality and unchanged float outputs; `voxel.FloorRound1PassageProjectionCache=0` disables only this A/B cache. §8.10. |
|
||||||
| `ApplyPassageModifier` | 2772 | Shared legacy/op-stack passage post: carves tube and local landing air through the idempotent landing primitive, then restores the local landing support slab in internal positive-solid density. |
|
| `ApplyPassageModifier` | 2772 | Shared legacy/op-stack passage post: carves tube and local landing air through the idempotent landing primitive, then restores the local landing support slab in internal positive-solid density. |
|
||||||
@@ -612,18 +624,18 @@ Maps depth→strate at runtime; owns passages.
|
|||||||
| `ApplyPassageLandingFloorMC` | 2983 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
|
| `ApplyPassageLandingFloorMC` | 2983 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
|
||||||
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
||||||
| `AnyPassageLandingFloorNearBox` | — | Conservative local-landing-floor AABB guard. It kills the `AllAir` hypothesis wherever a proved landing floor can occur; the op-stack returns `Both` for the same box. |
|
| `AnyPassageLandingFloorNearBox` | — | Conservative local-landing-floor AABB guard. It kills the `AllAir` hypothesis wherever a proved landing floor can occur; the op-stack returns `Both` for the same box. |
|
||||||
| `FindSlotIndexForChunkZ` | 6111 | Z → layout index. |
|
| `FindSlotIndexForChunkZ` / `FindSlotIndexForWorldZCentimetres` | 6239 / 6251 | Chunk callers use the cached compatibility projection; world-Z callers search physical half-open micrometre intervals. |
|
||||||
| `GetStrateAt` / `GetStrateIndex` | 6127 / 6139 | World-Z queries. |
|
| `GetStrateAt` / `GetStrateIndex` | 6276 / 6287 | World-Z queries against half-open micrometre intervals. |
|
||||||
| `GetLayoutVersion` | h:250 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
|
| `GetLayoutVersion` | h:250 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
|
||||||
| `GetStrateForChunk` | 6150 | Chunk → definition. |
|
| `GetStrateForChunk` | 6298 | Chunk → definition via the compatibility projection. |
|
||||||
| `GetGeneratorTypeForChunk` | 6176 | Chunk → generator type. |
|
| `GetGeneratorTypeForChunk` | 6324 | Chunk → generator type via the compatibility projection. |
|
||||||
| `UsesOperatorStackForChunk` | 6203 | Chunk → does its slot have an operator stack? True for any in-layout slot with a definition whose archetype is one of the 8 (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater); false outside the layout. **That list is written down here and nowhere else.** `ClassifyTile` guards on it; `GetDensityAt` uses it to share TunnelNetwork stacks by chunk Z. |
|
| `UsesOperatorStackForChunk` | 6351 | Chunk → does its slot have an operator stack? True for any in-layout slot with a definition whose archetype is one of the 8 (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater); false outside the layout. **That list is written down here and nowhere else.** `ClassifyTile` guards on it; `GetDensityAt` uses it to share TunnelNetwork stacks by chunk Z. |
|
||||||
| `GetRecipeForChunk` | — | Runtime immutable recipe/vector/strate-seed copy for cooked seasons; editor slot overrides use the same worker hand-off. |
|
| `GetRecipeForChunk` | — | Runtime immutable recipe/vector/strate-seed copy for cooked seasons; editor slot overrides use the same worker hand-off. |
|
||||||
| `GetSlabParamsForChunk` | 6263 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
| `GetSlabParamsForChunk` | 6415 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
||||||
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
||||||
| `GetGenerationParams` | 6509 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
| `GetGenerationParams` | 6661 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
||||||
| `SetComposerOverrideForStrate` / `GetComposerOverrideForChunk` | editor-only | Temporary density-only candidate overlay for one existing slot. Copies immutable params/recipe into the worker refetch path, leaves layout/content/passages intact, and bumps `PassagesVersion` to invalidate generator memos. |
|
| `SetComposerOverrideForStrate` / `GetComposerOverrideForChunk` | editor-only | Temporary density-only candidate overlay for one existing slot. Copies immutable params/recipe into the worker refetch path, leaves layout/content/passages intact, and bumps `PassagesVersion` to invalidate generator memos. |
|
||||||
| `BuildParamsFromDefinition` (static) | 6815 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
|
| `BuildParamsFromDefinition` (static) | 6985 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
|
||||||
|
|
||||||
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
||||||
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
|
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
|
||||||
@@ -1268,6 +1280,78 @@ ARCHITECTURE §8.47 and
|
|||||||
`WorldAssemblyEvaluatorVersion=23`. See ARCHITECTURE §8.49 and
|
`WorldAssemblyEvaluatorVersion=23`. See ARCHITECTURE §8.49 and
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
||||||
|
|
||||||
|
### 3.33 Immutable field binding — World Assembler M3 L40
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `FSurfaceWorldFieldAdapter`, `FFloatingIslandFieldAdapter`, `FLegacyGeneratorFieldAdapter`, `FGeneratorFieldAdapterV1` | `Standalone/Core/src/ShapeWorldInternal.h` | Concrete, immutable density adapters with one shared `densityAt(x,y,z,out)` call shape; the variant avoids per-sample virtual dispatch. FI retains its fixed-point entry point and prebound legacy boundary parameters. |
|
||||||
|
| `FBoundStratumField`, `BindResolvedStratumFields`, `FindBoundStratumForChunk` | `Standalone/Core/src/ShapeWorldInternal.h` | Bind a resolved strate's plan, evaluator and chunk interval once during world initialization; native field and classification queries select this table. |
|
||||||
|
| `NativeSurfaceDensityAt`, `NativeFloatingIslandDensityAt`, `ClassifyBoxDetailed` | `Standalone/Core/src/ShapeWorldInternal.h` | Consume the prebound adapters and plan pointers instead of looking up strate type/parameters through the manager per query. |
|
||||||
|
|
||||||
|
Hash parity is unchanged from L37/L39; see ARCHITECTURE §8.50 and
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux40/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux40/REPORT.md).
|
||||||
|
|
||||||
|
### 3.34 Resolved boundary and landing capabilities — World Assembler M3 L42
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `ResolvedLayoutStratePassageCapabilitiesV1`, `ResolvedLayoutInterfaceCapabilitiesV1` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Runtime-only resolved records for surface/island landing, passage-column anchoring, the FI Gradient air gap, and SW-to-TunnelNetwork support. They are derived during layout finalization from fields already included in the canonical layout identity. |
|
||||||
|
| `ResolvedLayoutSnapshotV3::finalize` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Resolves those capabilities once before sampling; canonical JSON and stable layout IDs stay unchanged. |
|
||||||
|
| `FStandaloneShapeWorld::ApplyResolvedStrateBoundaryBand`, `ApplyResolvedStratePassageCarves`, `BuildPassageVoidCrossingPlan`, `PassageBridgeSignedDistance` | `Standalone/Core/src/ShapeWorldInternal.h` | Consume resolved capability records. Generator-kind selection remains only in `BindResolvedStratumFields`, where it binds the concrete evaluator once. |
|
||||||
|
|
||||||
|
Owner hashes match L41; see ARCHITECTURE §8.52 and
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux42/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux42/REPORT.md).
|
||||||
|
|
||||||
|
### 3.35 Core landmark SDF stamps — World Assembler M3 L43
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `ResolvedLayoutStampRequestV1`, `ResolvedLayoutStampV1`, `ResolvedSdfStampKindV1` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Version-1 bounded stamp vocabulary and integer-micrometre request/resolved records shared by all Core generators. |
|
||||||
|
| `ResolveResolvedLayoutStampsV1`, `AuditResolvedLayoutStampPlacementV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Seed/strate/stable-ID placement with bounded retries; rejects passage-mouth, walk-corridor, content-face, ceiling and bedrock conflicts. Empty requests preserve the old canonical layout payload. |
|
||||||
|
| `BuildStampSpatialIndex`, `ApplyResolvedSdfStamps`, `StampSdf` | `Standalone/Core/src/ShapeWorldInternal.h` | BVH-built conservative bounds select stamps once per tile; shared density composition applies smooth carve/fill SDFs with seeded value-noise relief. Box classification and native-only readiness decline boxes touched by a stamp. |
|
||||||
|
| `--landmark-chamber-demo` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Opt-in owner-world recipe: one SW ellipsoid chamber, arched entrance and low fill dais, placed beside the passage arrival. Off by default. |
|
||||||
|
|
||||||
|
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 slice 1 (Linux L58)
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed`, `ResolvedCavernRoomV1::unionWidthMicrometres`, `ResolvedCavernEdgeV1::unionWidthMicrometres` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Versioned Maze/VerticalShafts opt-in and deterministic seed from `(seed, strate, recipe version, stable ID)`; optional union width preserves prior cave JSON when unset. |
|
||||||
|
| `MakeNativeLatticeSpine`, `--recipe=lattice`, `--lattice-strate=N` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Resolves actual adjacent mouths, builds a mandatory four-turn helix and maze spine, then adds two seeded dead ends; `--lattice-orphan-control` moves the helix away from both junctions. |
|
||||||
|
| `AuditNativeLatticeField` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the final field along the mouth-to-mouth capsule route, checking support, headroom, grade, orphan rejection, composed fillet, interval signs and feature-tile exclusion. |
|
||||||
|
| `BuildLatticeFixtureL58.py`, `CaptureNativeLatticeL58.py`, `AuditLatticeSurfaceL58.py` | `Standalone/Research/WorldAssemblerM3/Linux58/` | Isolated Maze strate fixture, four reviewed captures and the L54 whole-surface triangle audit adapted to explicit native polylines. |
|
||||||
|
|
||||||
|
See ARCHITECTURE §8.54 and
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md)
|
||||||
|
for resolved records, final-field reachability, controls, timing, hashes and captures.
|
||||||
|
|
||||||
|
### 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)
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||||
|
|||||||
@@ -399,6 +399,7 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
|
|||||||
LegacySettings->WorldRadiusVoxels = 987.0f;
|
LegacySettings->WorldRadiusVoxels = 987.0f;
|
||||||
LegacySettings->TotalStrates = 1;
|
LegacySettings->TotalStrates = 1;
|
||||||
LegacyDefinition->StrateHeightInChunks = 7;
|
LegacyDefinition->StrateHeightInChunks = 7;
|
||||||
|
LegacyDefinition->StrateHeightMeters = 7.0 * 8.0;
|
||||||
LegacySettings->StratePool.Add(
|
LegacySettings->StratePool.Add(
|
||||||
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
|
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
|
||||||
TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
|
TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
|
||||||
|
|||||||
@@ -525,6 +525,7 @@ bool FVoxelForgeZeroTransitionBlendTest::RunTest(const FString& Parameters)
|
|||||||
{
|
{
|
||||||
UVoxelStrateDefinition* Definition = World.Definitions[Index].Get();
|
UVoxelStrateDefinition* Definition = World.Definitions[Index].Get();
|
||||||
Definition->TransitionBlendChunks = 0;
|
Definition->TransitionBlendChunks = 0;
|
||||||
|
Definition->TransitionWidthMeters = 0.0;
|
||||||
Definition->TransitionType = (Index & 1) == 0
|
Definition->TransitionType = (Index & 1) == 0
|
||||||
? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved;
|
? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ namespace
|
|||||||
Settings->Seed = LayoutSeed;
|
Settings->Seed = LayoutSeed;
|
||||||
Settings->TotalStrates = NumPoolDefinitions;
|
Settings->TotalStrates = NumPoolDefinitions;
|
||||||
Settings->InterStrateGapChunks = 1;
|
Settings->InterStrateGapChunks = 1;
|
||||||
|
Settings->InterStrateGapMeters = Settings->StrateLayoutChunkSizeMeters;
|
||||||
Settings->bOpenSurfaceEntry = true;
|
Settings->bOpenSurfaceEntry = true;
|
||||||
|
|
||||||
static const ECaveGeneratorType GeneratorTypes[] = {
|
static const ECaveGeneratorType GeneratorTypes[] = {
|
||||||
@@ -69,6 +70,8 @@ namespace
|
|||||||
FString::Printf(TEXT("Layout order test strate %d"), Index));
|
FString::Printf(TEXT("Layout order test strate %d"), Index));
|
||||||
Def->GeneratorType = GeneratorTypes[Index];
|
Def->GeneratorType = GeneratorTypes[Index];
|
||||||
Def->StrateHeightInChunks = 3 + Index;
|
Def->StrateHeightInChunks = 3 + Index;
|
||||||
|
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
|
||||||
|
* Settings->StrateLayoutChunkSizeMeters;
|
||||||
Def->TransitionType = EVoxelStrateTransition::Hard;
|
Def->TransitionType = EVoxelStrateTransition::Hard;
|
||||||
|
|
||||||
// Non-trivial, distinct passage configs exercise the connection index and the
|
// Non-trivial, distinct passage configs exercise the connection index and the
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ namespace VoxelForgeTest
|
|||||||
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||||
Settings->Seed = InSeed;
|
Settings->Seed = InSeed;
|
||||||
Settings->InterStrateGapChunks = InGapChunks;
|
Settings->InterStrateGapChunks = InGapChunks;
|
||||||
|
Settings->InterStrateGapMeters = static_cast<double>(InGapChunks)
|
||||||
|
* Settings->StrateLayoutChunkSizeMeters;
|
||||||
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
|
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
|
||||||
// WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0.
|
// 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
|
// refinement and roll tests use the smaller synthetic volume as a bounded memory
|
||||||
// control; the owner-facing showcase opts into 8 chunks below.
|
// control; the owner-facing showcase opts into 8 chunks below.
|
||||||
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
|
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
|
// Hard transitions: param blending across a boundary would make "which archetype
|
||||||
// owns this chunk" ambiguous, and these tests want an unambiguous mapping.
|
// owns this chunk" ambiguous, and these tests want an unambiguous mapping.
|
||||||
Def->TransitionType = EVoxelStrateTransition::Hard;
|
Def->TransitionType = EVoxelStrateTransition::Hard;
|
||||||
|
|||||||
@@ -4,6 +4,27 @@
|
|||||||
|
|
||||||
namespace VoxelClipmapDesiredTiles
|
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
|
struct FParameters
|
||||||
{
|
{
|
||||||
int32 ClipRadius = 3;
|
int32 ClipRadius = 3;
|
||||||
@@ -13,7 +34,10 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
|
|
||||||
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
|
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)
|
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
|
||||||
@@ -24,7 +48,10 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
|
|
||||||
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
|
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,
|
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
|
||||||
@@ -33,10 +60,12 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
||||||
int32& OutAxisMin, int32& OutAxisMax)
|
int32& OutAxisMin, int32& OutAxisMax)
|
||||||
{
|
{
|
||||||
const int32 BaseMin = CenterAxis - Radius;
|
const int64 SafeRadius = FMath::Max<int64>(0, Radius);
|
||||||
const int32 BaseEnd = CenterAxis + Radius + 1;
|
const int64 SafeAlignment = FMath::Max<int64>(1, Alignment);
|
||||||
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
|
const int64 BaseMin = static_cast<int64>(CenterAxis) - SafeRadius;
|
||||||
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
|
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.X, OutMin.X, OutMax.X);
|
||||||
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
||||||
@@ -49,8 +78,13 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
{
|
{
|
||||||
return Radius;
|
return Radius;
|
||||||
}
|
}
|
||||||
return FMath::Max(Radius,
|
const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8);
|
||||||
(Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel);
|
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,
|
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
|
||||||
@@ -73,12 +107,12 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OutMinChunkZ = CenterChunkZ - ViewDistanceDown;
|
OutMinChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) - ViewDistanceDown);
|
||||||
OutMaxChunkZ = CenterChunkZ + ViewDistanceUp;
|
OutMaxChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) + ViewDistanceUp);
|
||||||
if (bHasStrate)
|
if (bHasStrate)
|
||||||
{
|
{
|
||||||
OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks;
|
OutMinChunkZ = ClampInt32(static_cast<int64>(StrateBottomChunkZ) - StrateViewMarginChunks);
|
||||||
OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks;
|
OutMaxChunkZ = ClampInt32(static_cast<int64>(StrateTopChunkZ) + StrateViewMarginChunks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +128,7 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
|
|
||||||
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
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 FIntVector CenterLevel = FloorDiv(Center, Pow);
|
||||||
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
||||||
|
|
||||||
@@ -105,24 +139,23 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
FIntVector RingMin, RingMax;
|
FIntVector RingMin, RingMax;
|
||||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||||
|
|
||||||
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
|
int64 TileMinZ = RingMin.Z;
|
||||||
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
|
int64 TileMaxZ = RingMax.Z;
|
||||||
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
||||||
{
|
{
|
||||||
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
|
TileMinZ = FMath::Max<int64>(TileMinZ, FloorDiv(MinChunkZ, Pow));
|
||||||
DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
|
TileMaxZ = FMath::Min<int64>(TileMaxZ, FloorDiv(MaxChunkZ, Pow));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
for (int64 Z = TileMinZ; Z <= TileMaxZ; ++Z)
|
||||||
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
|
for (int64 Y = RingMin.Y; Y <= RingMax.Y; ++Y)
|
||||||
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
|
for (int64 X = RingMin.X; X <= RingMax.X; ++X)
|
||||||
for (int32 DeltaX = RingMin.X - CenterLevel.X;
|
|
||||||
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
|
|
||||||
{
|
{
|
||||||
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
|
const FIntVector Tile(static_cast<int32>(X), static_cast<int32>(Y),
|
||||||
const int32 TileMinZ = Tile.Z << Level;
|
static_cast<int32>(Z));
|
||||||
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
|
const int64 TileBottomChunkZ = static_cast<int64>(Tile.Z) * Pow;
|
||||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
const int64 TileTopChunkZ = (static_cast<int64>(Tile.Z) + 1) * Pow - 1;
|
||||||
|
if (TileTopChunkZ < MinChunkZ || TileBottomChunkZ > MaxChunkZ)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -130,12 +163,12 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
if (Level > 0)
|
if (Level > 0)
|
||||||
{
|
{
|
||||||
const bool bCovered =
|
const bool bCovered =
|
||||||
(2 * Tile.X >= PreviousRingMin.X)
|
(2 * static_cast<int64>(Tile.X) >= PreviousRingMin.X)
|
||||||
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
|
&& (2 * static_cast<int64>(Tile.X) + 1 <= PreviousRingMax.X)
|
||||||
&& (2 * Tile.Y >= PreviousRingMin.Y)
|
&& (2 * static_cast<int64>(Tile.Y) >= PreviousRingMin.Y)
|
||||||
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
|
&& (2 * static_cast<int64>(Tile.Y) + 1 <= PreviousRingMax.Y)
|
||||||
&& (2 * Tile.Z >= PreviousRingMin.Z)
|
&& (2 * static_cast<int64>(Tile.Z) >= PreviousRingMin.Z)
|
||||||
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
|
&& (2 * static_cast<int64>(Tile.Z) + 1 <= PreviousRingMax.Z);
|
||||||
if (bCovered)
|
if (bCovered)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "Materials/MaterialInterface.h"
|
#include "Materials/MaterialInterface.h"
|
||||||
#include "Tasks/Task.h"
|
#include "Tasks/Task.h"
|
||||||
#include "HAL/PlatformProcess.h"
|
#include "HAL/PlatformProcess.h"
|
||||||
|
#include "Misc/Crc.h"
|
||||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
|
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
|
||||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -240,10 +241,10 @@ static std::vector<VoxelForgeCore::WorldAssemblyFeatureQueryV1> VF_ContentFeatur
|
|||||||
{
|
{
|
||||||
using namespace VoxelForgeCore;
|
using namespace VoxelForgeCore;
|
||||||
constexpr double Q = WorldAssemblySubvoxelsPerVoxel;
|
constexpr double Q = WorldAssemblySubvoxelsPerVoxel;
|
||||||
const std::array<int64,3> Min{static_cast<int64>(std::floor(MinX*Q)),
|
const std::array<std::int64_t,3> Min{static_cast<std::int64_t>(std::floor(MinX*Q)),
|
||||||
static_cast<int64>(std::floor(MinY*Q)),static_cast<int64>(std::floor(MinZ*Q))};
|
static_cast<std::int64_t>(std::floor(MinY*Q)),static_cast<std::int64_t>(std::floor(MinZ*Q))};
|
||||||
const std::array<int64,3> Max{static_cast<int64>(std::ceil(MaxX*Q)),
|
const std::array<std::int64_t,3> Max{static_cast<std::int64_t>(std::ceil(MaxX*Q)),
|
||||||
static_cast<int64>(std::ceil(MaxY*Q)),static_cast<int64>(std::ceil(MaxZ*Q))};
|
static_cast<std::int64_t>(std::ceil(MaxY*Q)),static_cast<std::int64_t>(std::ceil(MaxZ*Q))};
|
||||||
std::vector<WorldAssemblyFeatureQueryV1> Queries;
|
std::vector<WorldAssemblyFeatureQueryV1> Queries;
|
||||||
if (Surface)
|
if (Surface)
|
||||||
if (const auto* Plan = Surface->FindPlan(StrateIndex))
|
if (const auto* Plan = Surface->FindPlan(StrateIndex))
|
||||||
@@ -322,11 +323,54 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
|
|||||||
Seed = InSeed;
|
Seed = InSeed;
|
||||||
NativeSurfaceSnapshot.Reset();
|
NativeSurfaceSnapshot.Reset();
|
||||||
NativeFloatingIslandSnapshot.Reset();
|
NativeFloatingIslandSnapshot.Reset();
|
||||||
|
CoreGeometryAnchors.Reset();
|
||||||
|
|
||||||
if (Settings)
|
if (Settings)
|
||||||
{
|
{
|
||||||
FString ManifestJson, ManifestHash, ManifestError;
|
FString ManifestJson, ManifestHash, ManifestError;
|
||||||
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
|
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
|
const bool bHasRuntimeAssembly = VoxelWorld != nullptr
|
||||||
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
|
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
|
||||||
if (!bHasRuntimeAssembly)
|
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
|
// 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
|
// 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.
|
// 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)
|
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,
|
static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell,
|
||||||
int32 FarR, int32 R)
|
int32 FarR, int32 R)
|
||||||
{
|
{
|
||||||
const int32 MinX = FloorDivPos(PlayerCell.X - FarR, R);
|
const int32 MinX = FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R);
|
||||||
const int32 MaxX = FloorDivPos(PlayerCell.X + FarR, R);
|
const int32 MaxX = FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R);
|
||||||
const int32 MinY = FloorDivPos(PlayerCell.Y - FarR, R);
|
const int32 MinY = FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R);
|
||||||
const int32 MaxY = FloorDivPos(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;
|
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,
|
OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual,
|
||||||
FMath::Abs(static_cast<double>(Density)));
|
FMath::Abs(static_cast<double>(Density)));
|
||||||
++OutAudit.AnchorChecks;
|
++OutAudit.AnchorChecks;
|
||||||
if (FMath::Abs(Density)>0.25f)
|
// Core content crossings use the same finite-step column refinement as runtime
|
||||||
{ OutError=TEXT("Populated fixture is not anchored on the canonical field."); return false; }
|
// 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())
|
if (!Spawns.IsEmpty())
|
||||||
@@ -1102,17 +1161,22 @@ void UVoxelContentManager::RebuildDesiredCells(FDecoGrid& G, const FIntPoint& Pl
|
|||||||
const int32 R = RegionSize();
|
const int32 R = RegionSize();
|
||||||
|
|
||||||
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
|
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
|
||||||
const FIntPoint RMin(FloorDivPos(PlayerCell.X - FarR, R),
|
const FIntPoint RMin(FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R),
|
||||||
FloorDivPos(PlayerCell.Y - FarR, R));
|
FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R));
|
||||||
const FIntPoint RMax(FloorDivPos(PlayerCell.X + FarR, R),
|
const FIntPoint RMax(FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R),
|
||||||
FloorDivPos(PlayerCell.Y + FarR, R));
|
FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R));
|
||||||
|
|
||||||
TSet<FIntPoint> DesiredRegions;
|
TSet<FIntPoint> DesiredRegions;
|
||||||
DesiredRegions.Reserve((RMax.X - RMin.X + 1) * (RMax.Y - RMin.Y + 1));
|
constexpr int64 MaxRegionReserve = 4096;
|
||||||
for (int32 ry = RMin.Y; ry <= RMax.Y; ++ry)
|
const int64 WidthX = static_cast<int64>(RMax.X) - RMin.X + 1;
|
||||||
for (int32 rx = RMin.X; rx <= RMax.X; ++rx)
|
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).
|
// 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));
|
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
|
||||||
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
|
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
|
||||||
|
|
||||||
const int32 Span = FMath::Max(0, Comp.CountMax - Comp.CountMin);
|
const int64 Span = FMath::Max<int64>(0,
|
||||||
const int32 Count = Comp.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(CH ^ 0x1234u)) * (float)(Span + 1));
|
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)
|
for (int32 ii = 0; ii < Count; ++ii)
|
||||||
{
|
{
|
||||||
if (CompBudget <= 0) break;
|
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));
|
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
|
||||||
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
|
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
|
||||||
|
|
||||||
const int32 SubSpan = FMath::Max(0, Sub.CountMax - Sub.CountMin);
|
const int64 SubSpan = FMath::Max<int64>(0,
|
||||||
const int32 SubCount = Sub.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(SH ^ 0x1234u)) * (float)(SubSpan + 1));
|
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)
|
for (int32 sj = 0; sj < SubCount; ++sj)
|
||||||
{
|
{
|
||||||
if (CompBudget <= 0) break;
|
if (CompBudget <= 0) break;
|
||||||
@@ -2374,6 +2452,28 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
|
|||||||
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
|
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
|
||||||
if (ExclWorld > 0.0f) bAnyExclusion = true;
|
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)
|
if (L.AnchorMode == ELandmarkAnchor::HashLattice)
|
||||||
{
|
{
|
||||||
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
|
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)
|
FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B)
|
||||||
{
|
{
|
||||||
// True floor division (B > 0). FMath::DivideAndRoundDown truncates toward zero for negatives —
|
// True floor division (B > 0). Remainder adjustment handles INT_MIN without negating it.
|
||||||
// a footgun the content manager hit too (see FloorDivPos there). Cells span the origin, so floor.
|
if (B <= 0) { return 0; }
|
||||||
return (A >= 0) ? (A / B) : -(((-A) + B - 1) / B);
|
const int32 Quotient = A / B;
|
||||||
|
const int32 Remainder = A % B;
|
||||||
|
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||||
}
|
}
|
||||||
|
|
||||||
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
|
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
|
||||||
@@ -180,7 +182,8 @@ void UVoxelDensityVolume::EnsureAllocated()
|
|||||||
for (int32 L = 0; L < N; ++L)
|
for (int32 L = 0; L < N; ++L)
|
||||||
{
|
{
|
||||||
FClipLevel& Lv = Levels[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.OriginCells = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
Lv.bHasData = false;
|
Lv.bHasData = false;
|
||||||
Lv.Density.SetNumZeroed(Count); // start all-air (0)
|
Lv.Density.SetNumZeroed(Count); // start all-air (0)
|
||||||
@@ -632,7 +635,8 @@ void UVoxelDensityVolume::ProcessOneFill(const FPendingFill& F)
|
|||||||
if (!Gen) return;
|
if (!Gen) return;
|
||||||
if (bShuttingDown.load(std::memory_order_relaxed) || bFillThreadStop.load(std::memory_order_relaxed)) 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;
|
FFillResult R;
|
||||||
R.Level = F.Level;
|
R.Level = F.Level;
|
||||||
R.Epoch = F.Epoch;
|
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;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -15,9 +15,73 @@
|
|||||||
#include "HAL/IConsoleManager.h"
|
#include "HAL/IConsoleManager.h"
|
||||||
#include "Misc/CommandLine.h"
|
#include "Misc/CommandLine.h"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
struct FVoxelMesherThreadState
|
||||||
|
{
|
||||||
|
int32 OctaveBias=0,SampleStep=1,TileCells=0,TileReachBlocks=0,TileReachBlockIndex=INDEX_NONE;
|
||||||
|
FIntVector TileOrigin=FIntVector::ZeroValue;
|
||||||
|
uint8 TilePostReachFlags=VoxelGenLOD::AllTileReach;
|
||||||
|
bool bTilePostReachDebug=false,bTilePostReachBypass=false;
|
||||||
|
bool bTileReachDiagnosticTile=false,bTileCoreReachProofEnabled=false;
|
||||||
|
bool bTileCoreReachDecisionRecorded=false,bTileBlockReachValid=false;
|
||||||
|
float TileReachScale=1.0f;
|
||||||
|
std::array<uint8,VoxelGenLOD::TileReachMaxBlockCount> TileBlockReachFlags{};
|
||||||
|
|
||||||
|
static FVoxelMesherThreadState Capture()
|
||||||
|
{
|
||||||
|
FVoxelMesherThreadState State;
|
||||||
|
State.OctaveBias=VoxelGenLOD::OctaveBias;
|
||||||
|
State.SampleStep=VoxelGenLOD::SampleStep;
|
||||||
|
State.TileOrigin=VoxelGenLOD::TileOriginVoxels;
|
||||||
|
State.TileCells=VoxelGenLOD::TileCellsPerAxis;
|
||||||
|
State.TilePostReachFlags=VoxelGenLOD::TilePostReachFlags;
|
||||||
|
State.bTilePostReachDebug=VoxelGenLOD::bTilePostReachDebug;
|
||||||
|
State.bTilePostReachBypass=VoxelGenLOD::bTilePostReachBypass;
|
||||||
|
State.bTileReachDiagnosticTile=VoxelGenLOD::bTileReachDiagnosticTile;
|
||||||
|
State.bTileCoreReachProofEnabled=VoxelGenLOD::bTileCoreReachProofEnabled;
|
||||||
|
State.bTileCoreReachDecisionRecorded=VoxelGenLOD::bTileCoreReachDecisionRecorded;
|
||||||
|
State.bTileBlockReachValid=VoxelGenLOD::bTileBlockReachValid;
|
||||||
|
State.TileReachScale=VoxelGenLOD::TileReachScale;
|
||||||
|
State.TileReachBlocks=VoxelGenLOD::TileReachBlocksPerAxis;
|
||||||
|
State.TileReachBlockIndex=VoxelGenLOD::TileReachBlockIndex;
|
||||||
|
std::copy(std::begin(VoxelGenLOD::TileBlockReachFlags),
|
||||||
|
std::end(VoxelGenLOD::TileBlockReachFlags),State.TileBlockReachFlags.begin());
|
||||||
|
return State;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Apply() const
|
||||||
|
{
|
||||||
|
VoxelGenLOD::OctaveBias=OctaveBias;
|
||||||
|
VoxelGenLOD::SampleStep=SampleStep;
|
||||||
|
VoxelGenLOD::TileOriginVoxels=TileOrigin;
|
||||||
|
VoxelGenLOD::TileCellsPerAxis=TileCells;
|
||||||
|
VoxelGenLOD::TilePostReachFlags=TilePostReachFlags;
|
||||||
|
VoxelGenLOD::bTilePostReachDebug=bTilePostReachDebug;
|
||||||
|
VoxelGenLOD::bTilePostReachBypass=bTilePostReachBypass;
|
||||||
|
VoxelGenLOD::bTileReachDiagnosticTile=bTileReachDiagnosticTile;
|
||||||
|
VoxelGenLOD::bTileCoreReachProofEnabled=bTileCoreReachProofEnabled;
|
||||||
|
VoxelGenLOD::bTileCoreReachDecisionRecorded=bTileCoreReachDecisionRecorded;
|
||||||
|
VoxelGenLOD::bTileBlockReachValid=bTileBlockReachValid;
|
||||||
|
VoxelGenLOD::TileReachScale=TileReachScale;
|
||||||
|
VoxelGenLOD::TileReachBlocksPerAxis=TileReachBlocks;
|
||||||
|
VoxelGenLOD::TileReachBlockIndex=TileReachBlockIndex;
|
||||||
|
std::copy(TileBlockReachFlags.begin(),TileBlockReachFlags.end(),
|
||||||
|
std::begin(VoxelGenLOD::TileBlockReachFlags));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FVoxelMesherThreadStateScope
|
||||||
|
{
|
||||||
|
explicit FVoxelMesherThreadStateScope(const FVoxelMesherThreadState& InTarget)
|
||||||
|
:Previous(FVoxelMesherThreadState::Capture())
|
||||||
|
{ InTarget.Apply(); }
|
||||||
|
~FVoxelMesherThreadStateScope(){Previous.Apply();}
|
||||||
|
FVoxelMesherThreadState Previous;
|
||||||
|
};
|
||||||
|
|
||||||
int32 GVoxelForgeUseOperatorBlock = 1;
|
int32 GVoxelForgeUseOperatorBlock = 1;
|
||||||
FAutoConsoleVariableRef CVarVoxelForgeUseOperatorBlock(
|
FAutoConsoleVariableRef CVarVoxelForgeUseOperatorBlock(
|
||||||
TEXT("voxel.UseOperatorBlock"),
|
TEXT("voxel.UseOperatorBlock"),
|
||||||
@@ -692,6 +756,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
|
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
|
||||||
static thread_local TArray<float> DensityGrid;
|
static thread_local TArray<float> DensityGrid;
|
||||||
DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
|
DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
|
||||||
|
float* const DensityGridData=DensityGrid.GetData();
|
||||||
const FVoxelSharedDensityGrid* ReuseGrid = InSharedDensityGrid != nullptr
|
const FVoxelSharedDensityGrid* ReuseGrid = InSharedDensityGrid != nullptr
|
||||||
? InSharedDensityGrid : SharedDensityGrid;
|
? InSharedDensityGrid : SharedDensityGrid;
|
||||||
const int32 SharedDeltaX = ReuseGrid
|
const int32 SharedDeltaX = ReuseGrid
|
||||||
@@ -739,7 +804,15 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
const bool bUseOperatorBlock = !bUseSharedDensityGrid
|
const bool bUseOperatorBlock = !bUseSharedDensityGrid
|
||||||
&& !bUseParameterizedBrickEvaluator
|
&& !bUseParameterizedBrickEvaluator
|
||||||
&& GVoxelForgeUseOperatorBlock != 0;
|
&& GVoxelForgeUseOperatorBlock != 0;
|
||||||
if (bUseOperatorBlock)
|
const int32 GridZSampleCount=GzHi-GzLo+1;
|
||||||
|
const std::size_t GridFillLaneCount=std::min<std::size_t>({
|
||||||
|
GridFillExecution.WorkerLimit,16,
|
||||||
|
static_cast<std::size_t>(FMath::Max(0,GridZSampleCount))});
|
||||||
|
const bool bUseParallelDensityGrid=GridFillExecution.ParallelFor!=nullptr
|
||||||
|
&&GridFillLaneCount>1&&GridFillExecution.BeginWorkerContext!=nullptr
|
||||||
|
&&GridFillExecution.EndWorkerContext!=nullptr&&!bUseSharedDensityGrid
|
||||||
|
&&!bUseParameterizedBrickEvaluator&&OutCaptureGrid==nullptr;
|
||||||
|
if (bUseOperatorBlock&&!bUseParallelDensityGrid)
|
||||||
{
|
{
|
||||||
// The grid includes one sample of halo on every side. The generator groups these exact
|
// The grid includes one sample of halo on every side. The generator groups these exact
|
||||||
// lattice points by chunk key and lets the prepared op stack fill each group once.
|
// lattice points by chunk key and lets the prepared op stack fill each group once.
|
||||||
@@ -889,6 +962,80 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
else if(bUseParallelDensityGrid)
|
||||||
|
{
|
||||||
|
const FVoxelMesherThreadState TileThreadState=FVoxelMesherThreadState::Capture();
|
||||||
|
std::array<int64,16> LaneSampleCounts{};
|
||||||
|
std::atomic<bool> bAborted{false};
|
||||||
|
auto RunLane=[&](std::size_t Lane,std::size_t Worker)
|
||||||
|
{
|
||||||
|
void* WorkerState=GridFillExecution.BeginWorkerContext(
|
||||||
|
GridFillExecution.WorkerContext,Worker);
|
||||||
|
int64 LaneSamples=0;
|
||||||
|
{
|
||||||
|
FVoxelMesherThreadStateScope ThreadStateScope(TileThreadState);
|
||||||
|
const int32 FirstGz=GzLo+static_cast<int32>(
|
||||||
|
(static_cast<std::size_t>(GridZSampleCount)*Lane)/GridFillLaneCount);
|
||||||
|
const int32 EndGz=GzLo+static_cast<int32>(
|
||||||
|
(static_cast<std::size_t>(GridZSampleCount)*(Lane+1))/GridFillLaneCount);
|
||||||
|
if(bUseOperatorBlock)
|
||||||
|
{
|
||||||
|
Generator->BeginDensityBlock(
|
||||||
|
OriginVoxels+FIntVector(-Step,-Step,FirstGz*Step),Step,
|
||||||
|
MDim,MDim,EndGz-FirstGz);
|
||||||
|
}
|
||||||
|
for(int32 gz=FirstGz;gz<EndGz;++gz)
|
||||||
|
{
|
||||||
|
if(ShouldAbortWork())
|
||||||
|
{ bAborted.store(true,std::memory_order_relaxed);break; }
|
||||||
|
for(int32 gy=-1;gy<=GridDim;++gy)
|
||||||
|
{
|
||||||
|
if(ShouldAbortWork())
|
||||||
|
{ bAborted.store(true,std::memory_order_relaxed);break; }
|
||||||
|
for(int32 gx=-1;gx<=GridDim;++gx)
|
||||||
|
{
|
||||||
|
const int32 LocalIndex=((gz+1)*MDim+(gy+1))*MDim+(gx+1);
|
||||||
|
const int32 ReachBlockX=FMath::Clamp(gx,0,CellsPerAxis-1)
|
||||||
|
/VoxelGenLOD::TileReachBlockCells;
|
||||||
|
const int32 ReachBlockY=FMath::Clamp(gy,0,CellsPerAxis-1)
|
||||||
|
/VoxelGenLOD::TileReachBlockCells;
|
||||||
|
const int32 ReachBlockZ=FMath::Clamp(gz,0,CellsPerAxis-1)
|
||||||
|
/VoxelGenLOD::TileReachBlockCells;
|
||||||
|
VoxelGenLOD::TileReachBlockIndex=
|
||||||
|
((ReachBlockZ*ReachBlocksPerAxis)+ReachBlockY)
|
||||||
|
*ReachBlocksPerAxis+ReachBlockX;
|
||||||
|
float PreEditDensity=0.0f;
|
||||||
|
DensityGridData[LocalIndex]=Generator->GetDensityAt(
|
||||||
|
OriginVoxels.X+gx*Step,OriginVoxels.Y+gy*Step,
|
||||||
|
OriginVoxels.Z+gz*Step,
|
||||||
|
bCapturePreEditGrid?&PreEditDensity:nullptr);
|
||||||
|
if(bCapturePreEditGrid)
|
||||||
|
OutPreEditDensityGrid->Samples[LocalIndex]=PreEditDensity;
|
||||||
|
++LaneSamples;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(bAborted.load(std::memory_order_relaxed))break;
|
||||||
|
}
|
||||||
|
if(bUseOperatorBlock)Generator->EndDensityBlock();
|
||||||
|
}
|
||||||
|
LaneSampleCounts[Lane]=LaneSamples;
|
||||||
|
GridFillExecution.EndWorkerContext(
|
||||||
|
GridFillExecution.WorkerContext,WorkerState,Worker);
|
||||||
|
};
|
||||||
|
struct FDispatchContext{decltype(RunLane)* Run=nullptr;};
|
||||||
|
FDispatchContext DispatchContext{&RunLane};
|
||||||
|
const auto DispatchLane=[](void* RawContext,std::size_t Lane,std::size_t Worker)
|
||||||
|
{
|
||||||
|
auto& Context=*static_cast<FDispatchContext*>(RawContext);
|
||||||
|
(*Context.Run)(Lane,Worker);
|
||||||
|
};
|
||||||
|
GridFillExecution.ParallelFor(GridFillExecution.SchedulerContext,
|
||||||
|
GridFillLaneCount,GridFillLaneCount,&DispatchContext,DispatchLane);
|
||||||
|
if(OutDensitySampleCount)
|
||||||
|
for(std::size_t Lane=0;Lane<GridFillLaneCount;++Lane)
|
||||||
|
*OutDensitySampleCount+=LaneSampleCounts[Lane];
|
||||||
|
if(bAborted.load(std::memory_order_relaxed))return FVoxelMeshData();
|
||||||
|
}
|
||||||
else for (int32 gz = GzLo; gz <= GzHi; gz++)
|
else for (int32 gz = GzLo; gz <= GzHi; gz++)
|
||||||
{
|
{
|
||||||
if (ShouldAbortWork())
|
if (ShouldAbortWork())
|
||||||
@@ -959,7 +1106,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
GridSampleCount * sizeof(float));
|
GridSampleCount * sizeof(float));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bUseOperatorBlock)
|
if (bUseOperatorBlock&&!bUseParallelDensityGrid)
|
||||||
{
|
{
|
||||||
Generator->EndDensityBlock();
|
Generator->EndDensityBlock();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2590,8 +2590,8 @@ static float EvaluateInternal(const UVoxelGenerator& Generator,
|
|||||||
{
|
{
|
||||||
EnsurePlan(Generator);
|
EnsurePlan(Generator);
|
||||||
}
|
}
|
||||||
const bool bChunkResolved = Context != nullptr && Context->bChunkResolved
|
const bool bChunkResolved = (Context != nullptr && Context->bChunkResolved)
|
||||||
|| ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved;
|
|| (ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved);
|
||||||
const FIntVector Chunk = Context != nullptr && Context->bChunkResolved
|
const FIntVector Chunk = Context != nullptr && Context->bChunkResolved
|
||||||
? Context->Chunk
|
? Context->Chunk
|
||||||
: ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved
|
: ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved
|
||||||
|
|||||||
@@ -1958,10 +1958,11 @@ namespace VoxelSeasonCompositionPrivate
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
|
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
|
||||||
FVoxelSeasonFixedStrate& OutFixed)
|
double LayoutChunkMeters, FVoxelSeasonFixedStrate& OutFixed)
|
||||||
{
|
{
|
||||||
OutFixed = FVoxelSeasonFixedStrate();
|
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.Archetype = Definition.GeneratorType;
|
||||||
OutFixed.SourceDefinitionPath = Definition.GetPathName();
|
OutFixed.SourceDefinitionPath = Definition.GetPathName();
|
||||||
OutFixed.bUsesRecipe = false;
|
OutFixed.bUsesRecipe = false;
|
||||||
@@ -1999,7 +2000,9 @@ namespace VoxelSeasonCompositionPrivate
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FVoxelSeasonFixedStrate Fixed;
|
FVoxelSeasonFixedStrate Fixed;
|
||||||
CopyDefinitionParams(*Definition, Fixed);
|
const double LayoutChunkMeters = Settings.WorldSettings != nullptr
|
||||||
|
? Settings.WorldSettings->StrateLayoutChunkSizeMeters : 8.0;
|
||||||
|
CopyDefinitionParams(*Definition, LayoutChunkMeters, Fixed);
|
||||||
Fixed.Seed = SeasonSeed;
|
Fixed.Seed = SeasonSeed;
|
||||||
if (Fixed.SourceDefinitionPath.IsEmpty())
|
if (Fixed.SourceDefinitionPath.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -2974,7 +2977,9 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* Wo
|
|||||||
if (WorldSettings != nullptr)
|
if (WorldSettings != nullptr)
|
||||||
{
|
{
|
||||||
Settings.SelectedStrateCount = WorldSettings->TotalStrates;
|
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.OriginSpineRadius = WorldSettings->OriginSpineRadius;
|
||||||
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
|
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
|
||||||
Settings.SeasonNumber = WorldSettings->CurrentSeason;
|
Settings.SeasonNumber = WorldSettings->CurrentSeason;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "VoxelSettings.h"
|
#include "VoxelSettings.h"
|
||||||
|
|
||||||
|
#include "VoxelForgeAssetVersion.h"
|
||||||
|
#include "VoxelStrateDefinition.h"
|
||||||
#include "VoxelSeasonAsset.h"
|
#include "VoxelSeasonAsset.h"
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
@@ -9,6 +11,74 @@
|
|||||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||||
#include <string>
|
#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
|
int32 UVoxelSettings::GetEffectiveWorldSeed() const
|
||||||
{
|
{
|
||||||
if (!Season.IsNull())
|
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,"
|
"\"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,"
|
"\"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,"
|
"\"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.TileX, Sample.TileY, Sample.TileZ,
|
||||||
Sample.Level, Sample.WorkPriority,
|
Sample.Level, Sample.WorkPriority,
|
||||||
static_cast<unsigned long long>(Sample.RequestOrdinal),
|
static_cast<unsigned long long>(Sample.RequestOrdinal),
|
||||||
@@ -312,8 +316,12 @@ void RecordTile(const FTileSample& Sample)
|
|||||||
static_cast<unsigned long long>(Sample.GeometryBytes),
|
static_cast<unsigned long long>(Sample.GeometryBytes),
|
||||||
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
||||||
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
||||||
*Number(Sample.WorkerTaskSeconds),
|
*Number(Sample.WorkerTaskSeconds), *Number(Sample.GenerationSeconds),
|
||||||
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds),
|
#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)));
|
*Number(Sample.MeshSeconds), *Number(Sample.StreamSeconds), *Number(Sample.ApplySeconds)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
// Serialized strate-definition migrations.
|
// Serialized strate-definition migrations.
|
||||||
|
|
||||||
#include "VoxelStrateDefinition.h"
|
#include "VoxelStrateDefinition.h"
|
||||||
|
#include "VoxelForgeAssetVersion.h"
|
||||||
|
|
||||||
#include "Serialization/CustomVersion.h"
|
#include "Serialization/CustomVersion.h"
|
||||||
#include "VoxelPassageGeometry.h"
|
#include "VoxelPassageGeometry.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
// The field name is retained for package compatibility, but version 1 changes its meaning
|
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = VoxelForgeAssetVersion::TunnelFloorSlopeDegrees;
|
||||||
// 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;
|
|
||||||
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
|
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
|
||||||
GVoxelForgeStrateSerializationVersion,
|
VoxelForgeAssetVersion::GUID,
|
||||||
GVoxelForgeStrateGentleSlopeDegreesVersion,
|
VoxelForgeAssetVersion::MetreAuthoringV1,
|
||||||
TEXT("VoxelForgeStrateSerialization"));
|
TEXT("VoxelForgeStrateSerialization"));
|
||||||
|
|
||||||
float LegacyGradientToDegrees(float LegacyGradient)
|
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()
|
void UVoxelStrateDefinition::PostLoad()
|
||||||
{
|
{
|
||||||
Super::PostLoad();
|
Super::PostLoad();
|
||||||
@@ -67,7 +89,7 @@ void UVoxelStrateDefinition::PostLoad()
|
|||||||
// is normalized to the ignored compatibility default before any runtime copy is made.
|
// is normalized to the ignored compatibility default before any runtime copy is made.
|
||||||
GenerationParams.bTunnelFloorEnabled = true;
|
GenerationParams.bTunnelFloorEnabled = true;
|
||||||
|
|
||||||
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion)
|
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
||||||
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
|
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||||
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||||
#include "HAL/IConsoleManager.h"
|
#include "HAL/IConsoleManager.h"
|
||||||
|
#include <cmath> // std::llround
|
||||||
#include "Misc/CommandLine.h"
|
#include "Misc/CommandLine.h"
|
||||||
#include "Misc/Crc.h"
|
#include "Misc/Crc.h"
|
||||||
#include "Misc/Parse.h"
|
#include "Misc/Parse.h"
|
||||||
@@ -24,6 +25,27 @@ namespace
|
|||||||
{
|
{
|
||||||
std::atomic<uint64> GNextStrateManagerLifetimeId { 0 };
|
std::atomic<uint64> GNextStrateManagerLifetimeId { 0 };
|
||||||
|
|
||||||
|
int64 VF_OwnerVoxelsToMicrometres(double OwnerVoxels)
|
||||||
|
{
|
||||||
|
return static_cast<int64>(std::llround(
|
||||||
|
OwnerVoxels * static_cast<double>(VOXEL_SIZE) * 10000.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
float VF_ResolvedWorldMicrometresToOwnerVoxels(int64 WorldMicrometres)
|
||||||
|
{
|
||||||
|
return static_cast<float>(static_cast<double>(WorldMicrometres)
|
||||||
|
/ (static_cast<double>(VOXEL_SIZE) * 10000.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
int64 VF_FloorDivide64(int64 Numerator, int64 Denominator)
|
||||||
|
{
|
||||||
|
if (Denominator <= 0) return 0;
|
||||||
|
int64 Quotient = Numerator / Denominator;
|
||||||
|
const int64 Remainder = Numerator % Denominator;
|
||||||
|
if (Remainder < 0) --Quotient;
|
||||||
|
return Quotient;
|
||||||
|
}
|
||||||
|
|
||||||
// Exact A/B switch for the repeated passage-floor projection item. The other field-
|
// Exact A/B switch for the repeated passage-floor projection item. The other field-
|
||||||
// preserving rewrites in this round are deliberately unconditional; this switch isolates
|
// preserving rewrites in this round are deliberately unconditional; this switch isolates
|
||||||
// the projection reuse without changing any density arithmetic.
|
// the projection reuse without changing any density arithmetic.
|
||||||
@@ -1233,7 +1255,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
? SeasonManifest.Strates.Num() : Settings->TotalStrates;
|
? SeasonManifest.Strates.Num() : Settings->TotalStrates;
|
||||||
const int32 LayoutGapChunks = bUseSeason
|
const int32 LayoutGapChunks = bUseSeason
|
||||||
? SeasonManifest.InterStrateGapChunks
|
? 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)
|
// STEP 1: Build shuffled pool (seed-based randomization)
|
||||||
@@ -1300,8 +1323,11 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current Z position (in chunks). Starts at 0 and goes downward (negative).
|
// Resolve the stack in physical integer micrometres first. The historical chunk indices are
|
||||||
int32 CurrentTopZ = 0;
|
// projected from these half-open bounds only after each slot is complete.
|
||||||
|
int64 CurrentTopWorldMicrometres = StrateLayoutChunkSizeMicrometres;
|
||||||
|
const int64 ResolvedGapMicrometres = static_cast<int64>(LayoutGapChunks)
|
||||||
|
* StrateLayoutChunkSizeMicrometres;
|
||||||
|
|
||||||
for (int32 i = 0; i < TotalStrates; i++)
|
for (int32 i = 0; i < TotalStrates; i++)
|
||||||
{
|
{
|
||||||
@@ -1344,7 +1370,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
|
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->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
|
||||||
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
|
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
|
||||||
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams;
|
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams;
|
||||||
@@ -1356,16 +1383,28 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
const int32 LayoutChunkVoxels=FMath::RoundToInt(StrateLayoutChunkWidthVoxels);
|
const int32 LayoutChunkVoxels=FMath::RoundToInt(StrateLayoutChunkWidthVoxels);
|
||||||
Slot.TopChunkZ = SeasonStrate.TopWorldZ / LayoutChunkVoxels - 1;
|
Slot.TopChunkZ = SeasonStrate.TopWorldZ / LayoutChunkVoxels - 1;
|
||||||
Slot.BottomChunkZ = SeasonStrate.BottomWorldZ / LayoutChunkVoxels;
|
Slot.BottomChunkZ = SeasonStrate.BottomWorldZ / LayoutChunkVoxels;
|
||||||
|
Slot.TopWorldMicrometres = (static_cast<int64>(Slot.TopChunkZ) + 1)
|
||||||
|
* StrateLayoutChunkSizeMicrometres;
|
||||||
|
Slot.BottomWorldMicrometres = static_cast<int64>(Slot.BottomChunkZ)
|
||||||
|
* StrateLayoutChunkSizeMicrometres;
|
||||||
|
Slot.HeightMicrometres = Slot.TopWorldMicrometres - Slot.BottomWorldMicrometres;
|
||||||
|
const int64 ExpectedTopChunk = VF_FloorDivide64(
|
||||||
|
CurrentTopWorldMicrometres, StrateLayoutChunkSizeMicrometres) - 1;
|
||||||
|
const int64 ExpectedBottomChunk = ExpectedTopChunk - (Slot.HeightInChunks - 1);
|
||||||
if (SeasonStrate.TopWorldZ % LayoutChunkVoxels != 0
|
if (SeasonStrate.TopWorldZ % LayoutChunkVoxels != 0
|
||||||
|| SeasonStrate.BottomWorldZ % LayoutChunkVoxels != 0
|
|| SeasonStrate.BottomWorldZ % LayoutChunkVoxels != 0
|
||||||
|| Slot.TopChunkZ != CurrentTopZ
|
|| Slot.TopChunkZ != ExpectedTopChunk
|
||||||
|| Slot.BottomChunkZ != CurrentTopZ - (Slot.HeightInChunks - 1))
|
|| Slot.BottomChunkZ != ExpectedBottomChunk
|
||||||
|
|| Slot.TopWorldMicrometres != CurrentTopWorldMicrometres
|
||||||
|
|| Slot.HeightMicrometres != static_cast<int64>(Slot.HeightInChunks)
|
||||||
|
* StrateLayoutChunkSizeMicrometres)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Error,
|
UE_LOG(LogTemp, Error,
|
||||||
TEXT("[StrateManager] Season slot %d bounds do not describe the declared stacked layout."), i);
|
TEXT("[StrateManager] Season slot %d bounds do not describe the declared stacked layout."), i);
|
||||||
RestorePreviousLayout();
|
RestorePreviousLayout();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
CurrentTopWorldMicrometres = Slot.BottomWorldMicrometres - ResolvedGapMicrometres;
|
||||||
SeasonStrates.Add(SeasonStrate);
|
SeasonStrates.Add(SeasonStrate);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1388,15 +1427,54 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute Z range from definition's height
|
// Resolve the authored chunk height into an exact physical interval.
|
||||||
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
Slot.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
|
||||||
Slot.TopChunkZ = CurrentTopZ;
|
Slot.Definition->StrateHeightMeters * 1000000.0
|
||||||
Slot.BottomChunkZ = CurrentTopZ - (Slot.HeightInChunks - 1);
|
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
||||||
|
Slot.TopWorldMicrometres = CurrentTopWorldMicrometres;
|
||||||
|
Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks)
|
||||||
|
* StrateLayoutChunkSizeMicrometres;
|
||||||
|
Slot.BottomWorldMicrometres = Slot.TopWorldMicrometres - Slot.HeightMicrometres;
|
||||||
|
Slot.TopChunkZ = static_cast<int32>(VF_FloorDivide64(
|
||||||
|
Slot.TopWorldMicrometres, StrateLayoutChunkSizeMicrometres) - 1);
|
||||||
|
Slot.BottomChunkZ = static_cast<int32>(VF_FloorDivide64(
|
||||||
|
Slot.BottomWorldMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||||
|
CurrentTopWorldMicrometres = Slot.BottomWorldMicrometres - ResolvedGapMicrometres;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move the cursor down for the next strate, leaving a solid-bedrock gap of
|
Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
|
||||||
// InterStrateGapChunks chunks between this strate and the next.
|
Slot.ResolvedTransitionBlendChunks = FMath::Max(0, FMath::RoundToInt(
|
||||||
CurrentTopZ = Slot.BottomChunkZ - 1 - LayoutGapChunks;
|
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);
|
||||||
|
Slot.PassageMidRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||||
|
Slot.ResolvedPassageConfig.MidRadius);
|
||||||
|
Slot.PassageReachMinimumMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||||
|
Slot.ResolvedPassageConfig.ReachMin);
|
||||||
|
Slot.PassageReachMaximumMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||||
|
Slot.ResolvedPassageConfig.ReachMax);
|
||||||
|
Slot.PassageDistanceMinimumMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||||
|
Slot.ResolvedPassageConfig.DistanceMin);
|
||||||
|
Slot.PassageDistanceMaximumMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||||
|
Slot.ResolvedPassageConfig.DistanceMax);
|
||||||
|
Slot.PassageWanderMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||||
|
Slot.ResolvedPassageConfig.Wander);
|
||||||
|
Slot.PassageVerticalWobbleMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||||
|
Slot.ResolvedPassageConfig.VerticalWobble);
|
||||||
|
Slot.PassageSpiralRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||||
|
Slot.ResolvedPassageConfig.SpiralRadius);
|
||||||
|
Slot.PassageCascadeLedgeMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||||
|
Slot.ResolvedPassageConfig.CascadeLedge);
|
||||||
|
Slot.LegacyTopWorldZVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Slot.TopWorldMicrometres);
|
||||||
|
Slot.LegacyBottomWorldZVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Slot.BottomWorldMicrometres);
|
||||||
|
Slot.LegacyHeightVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Slot.HeightMicrometres);
|
||||||
|
|
||||||
StrateLayout.Add(Slot);
|
StrateLayout.Add(Slot);
|
||||||
|
|
||||||
@@ -1429,6 +1507,7 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
OriginSpineRadius = bUseSeason
|
OriginSpineRadius = bUseSeason
|
||||||
? SeasonManifest.OriginSpineRadius : Settings->OriginSpineRadius;
|
? SeasonManifest.OriginSpineRadius : Settings->OriginSpineRadius;
|
||||||
InterStrateGapChunks = LayoutGapChunks;
|
InterStrateGapChunks = LayoutGapChunks;
|
||||||
|
ResolvedInterStrateGapMicrometres = ResolvedGapMicrometres;
|
||||||
if (bUseSeason)
|
if (bUseSeason)
|
||||||
{
|
{
|
||||||
ActiveSeasonContentHash = SeasonManifest.ContentHash;
|
ActiveSeasonContentHash = SeasonManifest.ContentHash;
|
||||||
@@ -1537,8 +1616,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
|||||||
}
|
}
|
||||||
VF_SetComposerRuntimeBounds(
|
VF_SetComposerRuntimeBounds(
|
||||||
Override->Params,
|
Override->Params,
|
||||||
(float)(TargetSlot->TopChunkZ + 1) * StrateLayoutChunkWidthVoxels,
|
TargetSlot->LegacyTopWorldZVoxels,
|
||||||
(float)TargetSlot->BottomChunkZ * StrateLayoutChunkWidthVoxels);
|
TargetSlot->LegacyBottomWorldZVoxels);
|
||||||
Override->bUseRecipe = bUseRecipe;
|
Override->bUseRecipe = bUseRecipe;
|
||||||
if (Recipe != nullptr)
|
if (Recipe != nullptr)
|
||||||
{
|
{
|
||||||
@@ -1557,8 +1636,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
|||||||
Override->bUseRegions = true;
|
Override->bUseRegions = true;
|
||||||
Override->Regions = *InRegions;
|
Override->Regions = *InRegions;
|
||||||
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
|
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
|
||||||
Override->Regions.StrateTopWorldZ = (float)(TargetSlot->TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Override->Regions.StrateTopWorldZ = TargetSlot->LegacyTopWorldZVoxels;
|
||||||
Override->Regions.StrateBottomWorldZ = (float)TargetSlot->BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Override->Regions.StrateBottomWorldZ = TargetSlot->LegacyBottomWorldZVoxels;
|
||||||
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
|
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
|
||||||
Override->Regions.StrateTopWorldZ,
|
Override->Regions.StrateTopWorldZ,
|
||||||
Override->Regions.StrateBottomWorldZ);
|
Override->Regions.StrateBottomWorldZ);
|
||||||
@@ -1805,14 +1884,36 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
// that opens the next room rather than a second vertical spine.
|
// that opens the next room rather than a second vertical spine.
|
||||||
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
|
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
|
||||||
if (!UpperDef) continue;
|
if (!UpperDef) continue;
|
||||||
const FStratePassageConfig& Cfg = UpperDef->PassageConfig;
|
// GeneratePassages is the legacy owner-voxel route adapter. Its immutable source is the
|
||||||
|
// physical passage record resolved during Initialize, converted back to owner voxels here.
|
||||||
|
FStratePassageConfig Cfg = Upper.ResolvedPassageConfig;
|
||||||
|
Cfg.MouthRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Upper.PassageMouthRadiusMicrometres);
|
||||||
|
Cfg.MidRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Upper.PassageMidRadiusMicrometres);
|
||||||
|
Cfg.ReachMin = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Upper.PassageReachMinimumMicrometres);
|
||||||
|
Cfg.ReachMax = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Upper.PassageReachMaximumMicrometres);
|
||||||
|
Cfg.DistanceMin = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Upper.PassageDistanceMinimumMicrometres);
|
||||||
|
Cfg.DistanceMax = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Upper.PassageDistanceMaximumMicrometres);
|
||||||
|
Cfg.Wander = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Upper.PassageWanderMicrometres);
|
||||||
|
Cfg.VerticalWobble = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Upper.PassageVerticalWobbleMicrometres);
|
||||||
|
Cfg.SpiralRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Upper.PassageSpiralRadiusMicrometres);
|
||||||
|
Cfg.CascadeLedge = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||||
|
Upper.PassageCascadeLedgeMicrometres);
|
||||||
|
|
||||||
// Upper strate floor and lower strate ceiling (differ when there's a bedrock gap).
|
// Upper strate floor and lower strate ceiling (differ when there's a bedrock gap).
|
||||||
const float UpperTopZ = (float)(Upper.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
const float UpperTopZ = Upper.LegacyTopWorldZVoxels;
|
||||||
const float UpperBottomZ = (float)(Upper.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float UpperBottomZ = Upper.LegacyBottomWorldZVoxels;
|
||||||
const float LowerTopZ = (float)(Lower.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
const float LowerTopZ = Lower.LegacyTopWorldZVoxels;
|
||||||
const float UpperMax = (float)Upper.HeightInChunks * StrateLayoutChunkWidthVoxels * 0.9f;
|
const float UpperMax = Upper.LegacyHeightVoxels * 0.9f;
|
||||||
const float LowerMax = (float)Lower.HeightInChunks * StrateLayoutChunkWidthVoxels * 0.9f;
|
const float LowerMax = Lower.LegacyHeightVoxels * 0.9f;
|
||||||
|
|
||||||
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||||
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||||
@@ -1942,7 +2043,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
LowerDef->FloatingIslandParams,
|
LowerDef->FloatingIslandParams,
|
||||||
LowerQuerySeed,
|
LowerQuerySeed,
|
||||||
LowerTopZ,
|
LowerTopZ,
|
||||||
(float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels,
|
Lower.LegacyBottomWorldZVoxels,
|
||||||
PX,
|
PX,
|
||||||
PY,
|
PY,
|
||||||
MaxLateralSnap,
|
MaxLateralSnap,
|
||||||
@@ -2002,7 +2103,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
if (bAimedLowerAtPlayerFitPoint)
|
if (bAimedLowerAtPlayerFitPoint)
|
||||||
{
|
{
|
||||||
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
|
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
|
||||||
const float LowerBottomZ = (float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float LowerBottomZ = Lower.LegacyBottomWorldZVoxels;
|
||||||
const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
|
const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
|
||||||
const float InnerTopZ = LowerTopZ - LowerSealThickness;
|
const float InnerTopZ = LowerTopZ - LowerSealThickness;
|
||||||
if (VoxelMath::IsFinite(SuggestedLowerPoint.X)
|
if (VoxelMath::IsFinite(SuggestedLowerPoint.X)
|
||||||
@@ -2170,7 +2271,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
Cfg.MouthRadius,
|
Cfg.MouthRadius,
|
||||||
LowerDoorDirection,
|
LowerDoorDirection,
|
||||||
LowerTopZ,
|
LowerTopZ,
|
||||||
(float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels,
|
Lower.LegacyBottomWorldZVoxels,
|
||||||
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
|
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
|
||||||
bAimedLowerAtPlayerFitPoint);
|
bAimedLowerAtPlayerFitPoint);
|
||||||
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
||||||
@@ -2595,8 +2696,8 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
|
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
|
||||||
{
|
{
|
||||||
const FStrateSlot& Top = StrateLayout[0];
|
const FStrateSlot& Top = StrateLayout[0];
|
||||||
const float TopZ = (float)(Top.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Top.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = (float)Top.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Top.LegacyBottomWorldZVoxels;
|
||||||
const float TopSeal = Top.Definition
|
const float TopSeal = Top.Definition
|
||||||
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
|
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
|
||||||
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
|
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
|
||||||
@@ -5921,8 +6022,8 @@ bool UVoxelStrateManager::AnyLandingFloorAtLattice(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||||
@@ -5964,8 +6065,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearBox(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
|
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
|
||||||
VoxelBox, TopZ, BottomZ,
|
VoxelBox, TopZ, BottomZ,
|
||||||
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
||||||
@@ -5985,8 +6086,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearBox(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
|
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
|
||||||
VoxelBox, TopZ, BottomZ,
|
VoxelBox, TopZ, BottomZ,
|
||||||
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
||||||
@@ -6010,8 +6111,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearLattice(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
|
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
|
||||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius,
|
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius,
|
||||||
@@ -6048,8 +6149,8 @@ bool UVoxelStrateManager::AnyOriginLandingAirNearLattice(
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||||
@@ -6125,8 +6226,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
||||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
|
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
|
||||||
@@ -6143,15 +6244,36 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
|||||||
|
|
||||||
int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
||||||
{
|
{
|
||||||
// Linear search through strate layout.
|
// Legacy chunk callers use the projected interval cached during Initialize. The source bounds
|
||||||
// With ~10-20 strates this is fine. If we ever have hundreds,
|
// are the slot's physical micrometre interval; these indices do not define or stack strates.
|
||||||
// switch to binary search (layout is sorted by Z).
|
for (int32 i = 0; i < StrateLayout.Num(); ++i)
|
||||||
for (int32 i = 0; i < StrateLayout.Num(); i++)
|
|
||||||
{
|
{
|
||||||
const FStrateSlot& Slot = StrateLayout[i];
|
const FStrateSlot& Slot = StrateLayout[i];
|
||||||
if (ChunkZ <= Slot.TopChunkZ && ChunkZ >= Slot.BottomChunkZ)
|
if (ChunkZ <= Slot.TopChunkZ && ChunkZ >= Slot.BottomChunkZ) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UVoxelStrateManager::FindSlotIndexForWorldZCentimetres(double WorldZCentimetres) const
|
||||||
|
{
|
||||||
|
const double WorldZMicrometres = WorldZCentimetres * 10000.0;
|
||||||
|
int32 Low = 0;
|
||||||
|
int32 High = StrateLayout.Num() - 1;
|
||||||
|
while (Low <= High)
|
||||||
{
|
{
|
||||||
return i;
|
const int32 Mid = Low + (High - Low) / 2;
|
||||||
|
const FStrateSlot& Slot = StrateLayout[Mid];
|
||||||
|
if (WorldZMicrometres >= static_cast<double>(Slot.TopWorldMicrometres))
|
||||||
|
{
|
||||||
|
High = Mid - 1;
|
||||||
|
}
|
||||||
|
else if (WorldZMicrometres < static_cast<double>(Slot.BottomWorldMicrometres))
|
||||||
|
{
|
||||||
|
Low = Mid + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Mid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
@@ -6159,9 +6281,8 @@ int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
|||||||
|
|
||||||
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
||||||
{
|
{
|
||||||
// Convert world Z to chunk Z coordinate
|
if (!FMath::IsFinite(WorldZ)) return nullptr;
|
||||||
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
|
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||||
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
|
||||||
if (SlotIdx >= 0)
|
if (SlotIdx >= 0)
|
||||||
{
|
{
|
||||||
return StrateLayout[SlotIdx].Definition;
|
return StrateLayout[SlotIdx].Definition;
|
||||||
@@ -6171,8 +6292,8 @@ UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
|||||||
|
|
||||||
int32 UVoxelStrateManager::GetStrateIndex(float WorldZ) const
|
int32 UVoxelStrateManager::GetStrateIndex(float WorldZ) const
|
||||||
{
|
{
|
||||||
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
|
if (!FMath::IsFinite(WorldZ)) return -1;
|
||||||
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||||
if (SlotIdx >= 0)
|
if (SlotIdx >= 0)
|
||||||
{
|
{
|
||||||
return StrateLayout[SlotIdx].StrateIndex;
|
return StrateLayout[SlotIdx].StrateIndex;
|
||||||
@@ -6285,9 +6406,13 @@ bool UVoxelStrateManager::IsGapChunk(const FIntVector& ChunkCoord) const
|
|||||||
if (StrateLayout.Num() == 0) return false;
|
if (StrateLayout.Num() == 0) return false;
|
||||||
|
|
||||||
// Above the top strate or below the bottom strate = open air, NOT a gap.
|
// Above the top strate or below the bottom strate = open air, NOT a gap.
|
||||||
const int32 StackTop = StrateLayout[0].TopChunkZ;
|
const int64 ChunkCenterMicrometres = static_cast<int64>(ChunkCoord.Z)
|
||||||
const int32 StackBottom = StrateLayout.Last().BottomChunkZ;
|
* StrateLayoutChunkSizeMicrometres + StrateLayoutChunkSizeMicrometres / 2;
|
||||||
if (ChunkCoord.Z > StackTop || ChunkCoord.Z < StackBottom) return false;
|
if (ChunkCenterMicrometres >= StrateLayout[0].TopWorldMicrometres
|
||||||
|
|| ChunkCenterMicrometres < StrateLayout.Last().BottomWorldMicrometres)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Inside the stack's Z span but not in any strate slot → it's a bedrock gap.
|
// Inside the stack's Z span but not in any strate slot → it's a bedrock gap.
|
||||||
return FindSlotIndexForChunkZ(ChunkCoord.Z) < 0;
|
return FindSlotIndexForChunkZ(ChunkCoord.Z) < 0;
|
||||||
@@ -6331,8 +6456,8 @@ FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVecto
|
|||||||
// Fill in the runtime Z bounds (voxel coordinates, same convention as
|
// Fill in the runtime Z bounds (voxel coordinates, same convention as
|
||||||
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
||||||
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*StrateLayoutChunkWidthVoxels.
|
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*StrateLayoutChunkWidthVoxels.
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
@@ -6365,8 +6490,8 @@ FMazeGenerationParams UVoxelStrateManager::GetMazeParamsForChunk(const FIntVecto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6392,8 +6517,8 @@ FSurfaceGenerationParams UVoxelStrateManager::GetSurfaceParamsForChunk(const FIn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6419,8 +6544,8 @@ FVerticalShaftParams UVoxelStrateManager::GetVerticalShaftParamsForChunk(const F
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6446,8 +6571,8 @@ FFloatingIslandParams UVoxelStrateManager::GetFloatingIslandParamsForChunk(const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6481,14 +6606,14 @@ FBiomeContext UVoxelStrateManager::GetBiomeContextForChunk(const FIntVector& Chu
|
|||||||
|
|
||||||
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
||||||
{
|
{
|
||||||
const int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
|
if (!FMath::IsFinite(WorldZ)) return false;
|
||||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||||
if (SlotIdx < 0) return false;
|
if (SlotIdx < 0) return false;
|
||||||
|
|
||||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
// Voxel-space Z bounds → Unreal units. Ceiling = top chunk's upper edge.
|
// Return the resolved physical bounds in Unreal centimeters.
|
||||||
OutTopZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels * VOXEL_SIZE;
|
OutTopZ = static_cast<float>(static_cast<double>(Slot.TopWorldMicrometres) / 10000.0);
|
||||||
OutBottomZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels * VOXEL_SIZE;
|
OutBottomZ = static_cast<float>(static_cast<double>(Slot.BottomWorldMicrometres) / 10000.0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6501,8 +6626,8 @@ FStrateDisturbanceParams UVoxelStrateManager::GetDisturbanceParamsForChunk(const
|
|||||||
}
|
}
|
||||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
FStrateDisturbanceParams Result = Slot.Definition->Disturbances;
|
FStrateDisturbanceParams Result = Slot.Definition->Disturbances;
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6534,8 +6659,8 @@ float UVoxelStrateManager::GetWaterLevelWorldZForChunk(const FIntVector& ChunkCo
|
|||||||
}
|
}
|
||||||
if (Rel <= 0.0f) return -FLT_MAX;
|
if (Rel <= 0.0f) return -FLT_MAX;
|
||||||
|
|
||||||
const float BottomZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
const float TopZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
return FMath::Lerp(BottomZ, TopZ, FMath::Clamp(Rel, 0.0f, 1.0f));
|
return FMath::Lerp(BottomZ, TopZ, FMath::Clamp(Rel, 0.0f, 1.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6579,8 +6704,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
FindComposerOverride(Slot.StrateIndex))
|
FindComposerOverride(Slot.StrateIndex))
|
||||||
{
|
{
|
||||||
FStrateGenerationParams Result = Override->Params.TunnelNetworkParams;
|
FStrateGenerationParams Result = Override->Params.TunnelNetworkParams;
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
// A candidate is a hard replacement of this slot. Do not blend its rolled vector with an
|
// A candidate is a hard replacement of this slot. Do not blend its rolled vector with an
|
||||||
// authored neighbour at a boundary; this is also how the offline fixture measures it.
|
// authored neighbour at a boundary; this is also how the offline fixture measures it.
|
||||||
return ResolveFinalParams(Result);
|
return ResolveFinalParams(Result);
|
||||||
@@ -6590,8 +6715,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
if (SeasonStrates.IsValidIndex(SlotIdx))
|
||||||
{
|
{
|
||||||
FStrateGenerationParams Result = SeasonStrates[SlotIdx].Params.TunnelNetworkParams;
|
FStrateGenerationParams Result = SeasonStrates[SlotIdx].Params.TunnelNetworkParams;
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
// Season vectors are already final, measured slot records. Vertical structural posts own
|
// Season vectors are already final, measured slot records. Vertical structural posts own
|
||||||
// their boundary; blending them with another selected recipe would describe neither one.
|
// their boundary; blending them with another selected recipe would describe neither one.
|
||||||
return ResolveFinalParams(Result);
|
return ResolveFinalParams(Result);
|
||||||
@@ -6608,8 +6733,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
//
|
//
|
||||||
// TopChunkZ=0, CHUNK_SIZE=32: top of the strate = chunk 0's top edge = voxel Z=32
|
// TopChunkZ=0, CHUNK_SIZE=32: top of the strate = chunk 0's top edge = voxel Z=32
|
||||||
// BottomChunkZ=-3: bottom of the strate = chunk -3's bottom edge = voxel Z=-3*32 = -96
|
// BottomChunkZ=-3: bottom of the strate = chunk -3's bottom edge = voxel Z=-3*32 = -96
|
||||||
BaseParams.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
BaseParams.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
BaseParams.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
BaseParams.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
// Core currently dispatches one generator field per resolved strate. Gradient and
|
// Core currently dispatches one generator field per resolved strate. Gradient and
|
||||||
// interleaved metadata therefore use the explicitly declared Hard fallback for every
|
// interleaved metadata therefore use the explicitly declared Hard fallback for every
|
||||||
@@ -6656,18 +6781,23 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
|
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
|
||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
// DistFromBottom = how many chunks above the bottom edge of this strate.
|
// Legacy chunk callers project their sample to a layout interval; physical distance and
|
||||||
|
// transition width are both resolved in micrometres before projecting the blend fraction.
|
||||||
// When 0, we're right at the boundary. When == TransitionBlendChunks, we're at
|
// When 0, we're right at the boundary. When == TransitionBlendChunks, we're at
|
||||||
// the outer edge of the transition zone.
|
// the outer edge of the transition zone.
|
||||||
int32 DistFromBottom = ChunkCoord.Z - Slot.BottomChunkZ;
|
const int64 DistFromBottomMicrometres = static_cast<int64>(ChunkCoord.Z)
|
||||||
|
* StrateLayoutChunkSizeMicrometres - Slot.BottomWorldMicrometres;
|
||||||
|
const int32 DistFromBottom = static_cast<int32>(VF_FloorDivide64(
|
||||||
|
DistFromBottomMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||||
|
|
||||||
if (SlotIdx + 1 < StrateLayout.Num())
|
if (SlotIdx + 1 < StrateLayout.Num())
|
||||||
{
|
{
|
||||||
// The upper strate (this one) controls the transition type at its lower edge
|
// The upper strate (this one) controls the transition type at its lower edge
|
||||||
const EVoxelStrateTransition TransType = Slot.Definition->TransitionType;
|
const EVoxelStrateTransition TransType = Slot.ResolvedTransitionType;
|
||||||
|
|
||||||
// Per-definition blend distance.
|
// Per-definition blend distance.
|
||||||
const int32 EffectiveBlend = Slot.Definition->TransitionBlendChunks;
|
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
|
||||||
|
Slot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||||
|
|
||||||
// Prepare the neighbor's params (only used for Gradient and Interleaved)
|
// Prepare the neighbor's params (only used for Gradient and Interleaved)
|
||||||
const FStrateSlot& BelowSlot = StrateLayout[SlotIdx + 1];
|
const FStrateSlot& BelowSlot = StrateLayout[SlotIdx + 1];
|
||||||
@@ -6691,8 +6821,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (EffectiveBlend > 0 && DistFromBottom < EffectiveBlend)
|
if (EffectiveBlend > 0 && DistFromBottom < EffectiveBlend)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||||
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
|
||||||
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
// Linear alpha: 0 at EffectiveBlend chunks away, 1 at the boundary
|
// Linear alpha: 0 at EffectiveBlend chunks away, 1 at the boundary
|
||||||
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
|
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
|
||||||
@@ -6729,8 +6859,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (DistFromBottom < CheckRange)
|
if (DistFromBottom < CheckRange)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||||
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
|
||||||
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
// Sample 3D Perlin noise to warp the boundary position.
|
// Sample 3D Perlin noise to warp the boundary position.
|
||||||
// XY frequency 0.15 gives medium-scale variation (~6-7 chunks per cycle).
|
// XY frequency 0.15 gives medium-scale variation (~6-7 chunks per cycle).
|
||||||
@@ -6768,14 +6898,18 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
// Mirror logic: the ABOVE strate's TransitionType controls its lower edge,
|
// Mirror logic: the ABOVE strate's TransitionType controls its lower edge,
|
||||||
// which is this strate's upper edge. So we read from StrateLayout[SlotIdx-1].
|
// which is this strate's upper edge. So we read from StrateLayout[SlotIdx-1].
|
||||||
int32 DistFromTop = Slot.TopChunkZ - ChunkCoord.Z;
|
const int64 DistFromTopMicrometres = Slot.TopWorldMicrometres
|
||||||
|
- (static_cast<int64>(ChunkCoord.Z) + 1) * StrateLayoutChunkSizeMicrometres;
|
||||||
|
const int32 DistFromTop = static_cast<int32>(VF_FloorDivide64(
|
||||||
|
DistFromTopMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||||
|
|
||||||
if (SlotIdx > 0)
|
if (SlotIdx > 0)
|
||||||
{
|
{
|
||||||
// The strate ABOVE controls the transition at its lower edge (= our upper edge)
|
// The strate ABOVE controls the transition at its lower edge (= our upper edge)
|
||||||
const FStrateSlot& AboveSlot = StrateLayout[SlotIdx - 1];
|
const FStrateSlot& AboveSlot = StrateLayout[SlotIdx - 1];
|
||||||
const EVoxelStrateTransition TransType = AboveSlot.Definition->TransitionType;
|
const EVoxelStrateTransition TransType = AboveSlot.ResolvedTransitionType;
|
||||||
const int32 EffectiveBlend = AboveSlot.Definition->TransitionBlendChunks;
|
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
|
||||||
|
AboveSlot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||||
|
|
||||||
switch (TransType)
|
switch (TransType)
|
||||||
{
|
{
|
||||||
@@ -6790,8 +6924,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (EffectiveBlend > 0 && DistFromTop < EffectiveBlend)
|
if (EffectiveBlend > 0 && DistFromTop < EffectiveBlend)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||||
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
|
||||||
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
|
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
|
||||||
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
||||||
@@ -6812,8 +6946,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (DistFromTop < CheckRange)
|
if (DistFromTop < CheckRange)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||||
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
|
||||||
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
// Same noise function but with a different seed offset to avoid
|
// Same noise function but with a different seed offset to avoid
|
||||||
// symmetry between top and bottom boundaries of adjacent strates
|
// symmetry between top and bottom boundaries of adjacent strates
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -261,8 +261,12 @@ namespace VoxelHash
|
|||||||
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
|
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
|
||||||
{
|
{
|
||||||
uint32 H = Seed;
|
uint32 H = Seed;
|
||||||
H ^= Mix((uint32)(CellX + 0x7FFFFFFF));
|
// Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for
|
||||||
H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u);
|
// 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);
|
return Mix(H);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,10 +278,14 @@ namespace VoxelHash
|
|||||||
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
|
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
|
||||||
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
|
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
|
||||||
uint32 H = Seed ^ 0xDEADBEEF;
|
uint32 H = Seed ^ 0xDEADBEEF;
|
||||||
H ^= Mix((uint32)(MinX + 0x7FFFFFFF));
|
const uint32 BiasedMinX = static_cast<uint32>(MinX) + 0x7FFFFFFFu;
|
||||||
H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u);
|
const uint32 BiasedMinY = static_cast<uint32>(MinY) + 0x7FFFFFFFu;
|
||||||
H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u);
|
const uint32 BiasedMaxX = static_cast<uint32>(MaxX) + 0x7FFFFFFFu;
|
||||||
H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u);
|
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);
|
return Mix(H);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,10 +485,13 @@ namespace VoxelNoise
|
|||||||
|
|
||||||
// Hash the neighbor cell to get a feature point position [0,1)
|
// Hash the neighbor cell to get a feature point position [0,1)
|
||||||
// Using three different hash mixes for X, Y, Z offsets
|
// 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 H = VoxelHash::Mix(
|
||||||
(uint32)(NX + 0x7FFFFFFF)
|
BiasedX
|
||||||
^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u)
|
^ VoxelHash::Mix(BiasedY * 2654435761u)
|
||||||
^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u)
|
^ VoxelHash::Mix(BiasedZ * 374761393u)
|
||||||
);
|
);
|
||||||
|
|
||||||
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
|
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
|
||||||
|
|||||||
@@ -340,6 +340,14 @@ private:
|
|||||||
float SuppressRadiusWorld = 0.0f;
|
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
|
/** 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
|
* (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
|
* 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.
|
// (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.
|
// Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint.
|
||||||
TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
|
TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
|
||||||
|
TMap<FString, FResolvedCoreGeometryAnchor> CoreGeometryAnchors;
|
||||||
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
|
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
|
||||||
|
|
||||||
// Set in BeginDestroy; worker tasks check it before touching us.
|
// Set in BeginDestroy; worker tasks check it before touching us.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <cstddef>
|
||||||
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
|
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
|
||||||
#include "VoxelGenerator.h"
|
#include "VoxelGenerator.h"
|
||||||
#include "VoxelMarchingCubesMesher.generated.h"
|
#include "VoxelMarchingCubesMesher.generated.h"
|
||||||
@@ -38,6 +39,24 @@ struct FVoxelSharedDensityGrid
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Host-owned scheduling hooks for a predicted-heavy density-grid fill. Each task owns one
|
||||||
|
// deterministic Z slab and gets the source tile's worker-local sampling context.
|
||||||
|
struct FVoxelGridFillExecution
|
||||||
|
{
|
||||||
|
using FTask = void(*)(void* taskContext, std::size_t index, std::size_t workerIndex);
|
||||||
|
using FParallelFor = double(*)(void* schedulerContext, std::size_t count,
|
||||||
|
std::size_t workerLimit, void* taskContext, FTask task);
|
||||||
|
using FBeginWorkerContext = void*(*)(void* context, std::size_t workerIndex);
|
||||||
|
using FEndWorkerContext = void(*)(void* context, void* workerState,
|
||||||
|
std::size_t workerIndex);
|
||||||
|
void* SchedulerContext = nullptr;
|
||||||
|
FParallelFor ParallelFor = nullptr;
|
||||||
|
std::size_t WorkerLimit = 1;
|
||||||
|
void* WorkerContext = nullptr;
|
||||||
|
FBeginWorkerContext BeginWorkerContext = nullptr;
|
||||||
|
FEndWorkerContext EndWorkerContext = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
// Stable plan data copied onto one worker-owned mesher for hybrid LOD0 generation.
|
// Stable plan data copied onto one worker-owned mesher for hybrid LOD0 generation.
|
||||||
// Donnees stables du plan copiees dans un mailleur de worker pour le mode hybride LOD0.
|
// Donnees stables du plan copiees dans un mailleur de worker pour le mode hybride LOD0.
|
||||||
struct FVoxelHybridSharpFeatureBox
|
struct FVoxelHybridSharpFeatureBox
|
||||||
@@ -111,6 +130,8 @@ public:
|
|||||||
// GenerateMesh calls finish. A malformed/incomplete lattice is ignored and falls back to
|
// GenerateMesh calls finish. A malformed/incomplete lattice is ignored and falls back to
|
||||||
// the canonical generator path rather than changing geometry.
|
// the canonical generator path rather than changing geometry.
|
||||||
void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; }
|
void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; }
|
||||||
|
void SetGridFillExecution(const FVoxelGridFillExecution& InExecution)
|
||||||
|
{ GridFillExecution = InExecution; }
|
||||||
|
|
||||||
// The caller owns this mesher instance for one tile task; no shared mutable feature state.
|
// The caller owns this mesher instance for one tile task; no shared mutable feature state.
|
||||||
// L'appelant possede cette instance pour une tuile; aucune donnee partagee modifiable.
|
// L'appelant possede cette instance pour une tuile; aucune donnee partagee modifiable.
|
||||||
@@ -137,6 +158,7 @@ private:
|
|||||||
|
|
||||||
const std::atomic<bool>* ShutdownFlag = nullptr;
|
const std::atomic<bool>* ShutdownFlag = nullptr;
|
||||||
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
|
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
|
||||||
|
FVoxelGridFillExecution GridFillExecution;
|
||||||
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
|
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
|
||||||
bool bUseHybridSharpMesher = false;
|
bool bUseHybridSharpMesher = false;
|
||||||
bool bUseHybridSharpFeatureSnap = false;
|
bool bUseHybridSharpFeatureSnap = false;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class UVoxelSettings : public UPrimaryDataAsset
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
virtual void Serialize(FArchive& Ar) override;
|
||||||
|
virtual void PostLoad() override;
|
||||||
|
|
||||||
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
|
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
|
||||||
int32 GetEffectiveWorldSeed() const;
|
int32 GetEffectiveWorldSeed() const;
|
||||||
@@ -411,14 +413,16 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
|
||||||
int32 TotalStrates = 10;
|
int32 TotalStrates = 10;
|
||||||
|
|
||||||
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock.
|
// Legacy count retained so pre-metre assets still deserialize during the upgrade.
|
||||||
// 0 = strates touch (just the seals between them). Higher = a thick rock layer
|
// Ancien compte conserve pour lire les assets precedant les metres.
|
||||||
// between each biome that the player must dig through at (0,0), and that the
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (DeprecatedProperty, DeprecationMessage = "Use InterStrateGapMeters."))
|
||||||
// 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"))
|
|
||||||
int32 InterStrateGapChunks = 0;
|
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
|
// 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.
|
// 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.
|
// 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 ResultQueueSeconds = 0.0;
|
||||||
double WorkerTaskSeconds = 0.0;
|
double WorkerTaskSeconds = 0.0;
|
||||||
double GenerationSeconds = 0.0;
|
double GenerationSeconds = 0.0;
|
||||||
|
double CoreGenerateSeconds = 0.0;
|
||||||
|
double MeshCopySeconds = 0.0;
|
||||||
|
double StreamBuildSeconds = 0.0;
|
||||||
double ClassifySeconds = 0.0;
|
double ClassifySeconds = 0.0;
|
||||||
double MeshSeconds = 0.0;
|
double MeshSeconds = 0.0;
|
||||||
double StreamSeconds = 0.0;
|
double StreamSeconds = 0.0;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
virtual void Serialize(FArchive& Ar) override;
|
||||||
virtual void PostLoad() override;
|
virtual void PostLoad() override;
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -66,14 +67,14 @@ public:
|
|||||||
// DIMENSIONS
|
// DIMENSIONS
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|
||||||
// How many chunks tall this strate is.
|
// Legacy serialized chunk count kept for Blueprint/package compatibility during the unit migration.
|
||||||
// Each strate can have a different height!
|
// Ancien nombre de chunks conserve pour la compatibilite Blueprint/package pendant la migration.
|
||||||
// Human-scale defaults use 16 chunks = 64 m. This gives a 20 m+ vertical budget for
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (DeprecatedProperty, DeprecationMessage = "Use StrateHeightMeters."))
|
||||||
// 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"))
|
|
||||||
int32 StrateHeightInChunks = 16;
|
int32 StrateHeightInChunks = 16;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (Units = "m", ClampMin = "1", ClampMax = "2048"))
|
||||||
|
double StrateHeightMeters = 128.0;
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// BOUNDARY TRANSITION
|
// BOUNDARY TRANSITION
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -99,9 +100,13 @@ public:
|
|||||||
// 2 → moderate (good default)
|
// 2 → moderate (good default)
|
||||||
// 4 → wide, gentle blend across many chunks
|
// 4 → wide, gentle blend across many chunks
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
||||||
meta = (ClampMin = "1", ClampMax = "8", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
|
meta = (DeprecatedProperty, DeprecationMessage = "Use TransitionWidthMeters."))
|
||||||
int32 TransitionBlendChunks = 2;
|
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
|
// GENERATOR TYPE
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -115,6 +120,11 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
|
||||||
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
|
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
|
* 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
|
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ struct FVoxelPassage
|
|||||||
* FStrateSlot — Runtime info for one strate in the layout.
|
* FStrateSlot — Runtime info for one strate in the layout.
|
||||||
*
|
*
|
||||||
* Created during Initialize() and stored in the StrateLayout array.
|
* Created during Initialize() and stored in the StrateLayout array.
|
||||||
* Each slot knows its definition, its Z-range in chunk coordinates,
|
* Each slot owns its resolved half-open physical Z interval. Chunk coordinates and legacy
|
||||||
* and its index in the sequence.
|
* authoring values below are projections kept for existing callers and assets.
|
||||||
*/
|
*/
|
||||||
USTRUCT()
|
USTRUCT()
|
||||||
struct FStrateSlot
|
struct FStrateSlot
|
||||||
@@ -136,6 +136,33 @@ struct FStrateSlot
|
|||||||
// Strate index (0 = topmost, increases downward)
|
// Strate index (0 = topmost, increases downward)
|
||||||
int32 StrateIndex = 0;
|
int32 StrateIndex = 0;
|
||||||
|
|
||||||
|
// Resolved physical bounds, in micrometres, with interval [bottom, top). These are computed
|
||||||
|
// once during Initialize from the configured metre chunk width and legacy asset dimensions.
|
||||||
|
int64 TopWorldMicrometres = 0;
|
||||||
|
int64 BottomWorldMicrometres = 0;
|
||||||
|
int64 HeightMicrometres = 0;
|
||||||
|
float LegacyTopWorldZVoxels = 0.0f;
|
||||||
|
float LegacyBottomWorldZVoxels = 0.0f;
|
||||||
|
float LegacyHeightVoxels = 0.0f;
|
||||||
|
|
||||||
|
// Immutable transition and passage inputs copied when this slot is resolved. The width is
|
||||||
|
// physical; chunk counts and raw owner-voxel values remain compatibility projections for
|
||||||
|
// legacy parameter and route adapters.
|
||||||
|
EVoxelStrateTransition ResolvedTransitionType = EVoxelStrateTransition::Gradient;
|
||||||
|
int32 ResolvedTransitionBlendChunks = 2;
|
||||||
|
int64 TransitionWidthMicrometres = 0;
|
||||||
|
FStratePassageConfig ResolvedPassageConfig;
|
||||||
|
int64 PassageMouthRadiusMicrometres = 0;
|
||||||
|
int64 PassageMidRadiusMicrometres = 0;
|
||||||
|
int64 PassageReachMinimumMicrometres = 0;
|
||||||
|
int64 PassageReachMaximumMicrometres = 0;
|
||||||
|
int64 PassageDistanceMinimumMicrometres = 0;
|
||||||
|
int64 PassageDistanceMaximumMicrometres = 0;
|
||||||
|
int64 PassageWanderMicrometres = 0;
|
||||||
|
int64 PassageVerticalWobbleMicrometres = 0;
|
||||||
|
int64 PassageSpiralRadiusMicrometres = 0;
|
||||||
|
int64 PassageCascadeLedgeMicrometres = 0;
|
||||||
|
|
||||||
// Z chunk range (inclusive). TopZ > BottomZ since Z decreases downward.
|
// Z chunk range (inclusive). TopZ > BottomZ since Z decreases downward.
|
||||||
// Example: TopZ = 0, BottomZ = -3 means this strate spans chunks Z=0 to Z=-3
|
// Example: TopZ = 0, BottomZ = -3 means this strate spans chunks Z=0 to Z=-3
|
||||||
int32 TopChunkZ = 0;
|
int32 TopChunkZ = 0;
|
||||||
@@ -248,7 +275,11 @@ public:
|
|||||||
// This is independent of Core's 32^3 tile width.
|
// This is independent of Core's 32^3 tile width.
|
||||||
float GetStrateLayoutChunkWidthVoxels() const { return StrateLayoutChunkWidthVoxels; }
|
float GetStrateLayoutChunkWidthVoxels() const { return StrateLayoutChunkWidthVoxels; }
|
||||||
uint32 GetStrateLayoutChunkSizeMicrometres() const { return StrateLayoutChunkSizeMicrometres; }
|
uint32 GetStrateLayoutChunkSizeMicrometres() const { return StrateLayoutChunkSizeMicrometres; }
|
||||||
void SetHardTransitionFallbackForAssembly(bool bEnabled)
|
int64 GetResolvedInterStrateGapMicrometres() const
|
||||||
|
{ return ResolvedInterStrateGapMicrometres; }
|
||||||
|
// Called by the Core adapter through a const manager pointer; the flag is runtime routing
|
||||||
|
// state, not layout data, hence const + mutable.
|
||||||
|
void SetHardTransitionFallbackForAssembly(bool bEnabled) const
|
||||||
{ bHardTransitionFallbackForAssembly = bEnabled; }
|
{ bHardTransitionFallbackForAssembly = bEnabled; }
|
||||||
|
|
||||||
/** Layout/passage generation counter (bumped by every Initialize → GeneratePassages).
|
/** Layout/passage generation counter (bumped by every Initialize → GeneratePassages).
|
||||||
@@ -528,14 +559,15 @@ protected:
|
|||||||
// Copied from VoxelSettings::OriginSpineRadius.
|
// Copied from VoxelSettings::OriginSpineRadius.
|
||||||
float OriginSpineRadius = 28.0f;
|
float OriginSpineRadius = 28.0f;
|
||||||
|
|
||||||
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
|
// Compatibility gap projection, in layout chunks. Physical authority is the micrometre value
|
||||||
// VoxelSettings::InterStrateGapChunks during Initialize().
|
// resolved during Initialize below.
|
||||||
int32 InterStrateGapChunks = 0;
|
int32 InterStrateGapChunks = 0;
|
||||||
|
int64 ResolvedInterStrateGapMicrometres = 0;
|
||||||
|
|
||||||
// Resolved once from VoxelSettings::StrateLayoutChunkSizeMeters during Initialize().
|
// Resolved once from VoxelSettings::StrateLayoutChunkSizeMeters during Initialize().
|
||||||
float StrateLayoutChunkWidthVoxels = 32.0f;
|
float StrateLayoutChunkWidthVoxels = 32.0f;
|
||||||
uint32 StrateLayoutChunkSizeMicrometres = 8000000;
|
uint32 StrateLayoutChunkSizeMicrometres = 8000000;
|
||||||
bool bHardTransitionFallbackForAssembly = false;
|
mutable bool bHardTransitionFallbackForAssembly = false;
|
||||||
|
|
||||||
// Production keeps the legacy writer available only as a fail-closed fallback for a
|
// Production keeps the legacy writer available only as a fail-closed fallback for a
|
||||||
// non-native/malformed passage; native D-floors bypass it. This switch never changes the
|
// non-native/malformed passage; native D-floors bypass it. This switch never changes the
|
||||||
@@ -552,6 +584,7 @@ protected:
|
|||||||
// Find which slot a chunk Z coordinate falls into.
|
// Find which slot a chunk Z coordinate falls into.
|
||||||
// Returns INDEX into StrateLayout, or -1 if not found.
|
// Returns INDEX into StrateLayout, or -1 if not found.
|
||||||
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
||||||
|
int32 FindSlotIndexForWorldZCentimetres(double WorldZCentimetres) const;
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
|
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
|
||||||
|
|||||||
@@ -200,6 +200,208 @@ enum class ECaveGeneratorType : uint8
|
|||||||
Underwater UMETA(DisplayName = "Underwater (flooded cavern system)"),
|
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.
|
* EVoxelStrateTransition — How two adjacent strates meet at their shared boundary.
|
||||||
*
|
*
|
||||||
@@ -2289,6 +2491,11 @@ struct VOXELFORGE_API FStrateLandmark
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
|
||||||
ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice;
|
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) -----
|
// ----- 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
|
// 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) {}
|
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
|
||||||
|
|
||||||
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
|
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
|
||||||
int32 StepVoxels() const { return 1 << Level; }
|
int32 StepVoxels() const
|
||||||
int32 ExtentVoxels() const { return CHUNK_SIZE << Level; }
|
{
|
||||||
|
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).
|
// 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).
|
// Tile centre in world cm (for distance sorting / LOD selection).
|
||||||
FVector CenterCm() const
|
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);
|
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.
|
// per-tile profiling remains opt-in.
|
||||||
uint64 GenerationStartCycles = 0;
|
uint64 GenerationStartCycles = 0;
|
||||||
uint64 GenerationEndCycles = 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;
|
uint64 ApplyStartCycles = 0;
|
||||||
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
|
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
|
||||||
// Ordinals make admitted order and actual worker start order auditable across MPSC results.
|
// Ordinals make admitted order and actual worker start order auditable across MPSC results.
|
||||||
@@ -352,6 +356,10 @@ public:
|
|||||||
* the first resolved collidable support tile. */
|
* the first resolved collidable support tile. */
|
||||||
TSet<FVoxelTileKey> CollisionNotRequiredTiles;
|
TSet<FVoxelTileKey> CollisionNotRequiredTiles;
|
||||||
|
|
||||||
|
/** Known-empty collision classifications survive unload for the tile's current edit epoch. */
|
||||||
|
TMap<FVoxelTileKey, uint32> KnownEmptyCollisionTileEpoch;
|
||||||
|
TSet<FVoxelTileKey> KnownEmptyCollisionSolidTiles;
|
||||||
|
|
||||||
/** Subset of CollisionNotRequiredTiles proved all-solid. A support probe inside one of these
|
/** Subset of CollisionNotRequiredTiles proved all-solid. A support probe inside one of these
|
||||||
* tiles searches upward for the boundary tile; all-air/empty tiles search downward. */
|
* tiles searches upward for the boundary tile; all-air/empty tiles search downward. */
|
||||||
TSet<FVoxelTileKey> CollisionSolidTiles;
|
TSet<FVoxelTileKey> CollisionSolidTiles;
|
||||||
@@ -614,6 +622,7 @@ public:
|
|||||||
FVector GetVoxelForgeCoreDefaultCenterWorld() const;
|
FVector GetVoxelForgeCoreDefaultCenterWorld() const;
|
||||||
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
|
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
|
||||||
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
|
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
|
||||||
|
bool GetVoxelForgeCoreResolvedLayout(FString& OutCanonicalJson, FString& OutStableIdentity) const;
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
|
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
|
||||||
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
||||||
@@ -778,6 +787,8 @@ private:
|
|||||||
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
|
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
|
||||||
FString VoxelForgeCoreWorldAssemblyJson;
|
FString VoxelForgeCoreWorldAssemblyJson;
|
||||||
FString VoxelForgeCoreWorldAssemblyHash;
|
FString VoxelForgeCoreWorldAssemblyHash;
|
||||||
|
FString VoxelForgeCoreResolvedLayoutJson;
|
||||||
|
FString VoxelForgeCoreResolvedLayoutIdentity;
|
||||||
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||||
bool bCoreRegionCompilePending = false;
|
bool bCoreRegionCompilePending = false;
|
||||||
TArray<FVoxelTileKey> PendingCoreRegionTiles;
|
TArray<FVoxelTileKey> PendingCoreRegionTiles;
|
||||||
@@ -1194,6 +1205,9 @@ public:
|
|||||||
// and records the ones ONLY a CollisionOnly anchor wants in CollisionOnlyTiles.
|
// and records the ones ONLY a CollisionOnly anchor wants in CollisionOnlyTiles.
|
||||||
void AddAnchorDesiredTiles();
|
void AddAnchorDesiredTiles();
|
||||||
void ReconcileCollisionDesiredTiles();
|
void ReconcileCollisionDesiredTiles();
|
||||||
|
void RestoreKnownEmptyCollisionTiles();
|
||||||
|
void RebaseKnownEmptyCollisionEpoch();
|
||||||
|
void InvalidateKnownEmptyCollisionTile(const FVoxelTileKey& Tile, const TCHAR* Reason);
|
||||||
// Forces a desired-set rebuild next Tick even if neither the player nor an anchor crossed a tile
|
// Forces a desired-set rebuild next Tick even if neither the player nor an anchor crossed a tile
|
||||||
// boundary (set by UnregisterStreamingAnchor so a removed anchor's tiles get culled).
|
// boundary (set by UnregisterStreamingAnchor so a removed anchor's tiles get culled).
|
||||||
bool bForceDesiredRebuild = false;
|
bool bForceDesiredRebuild = false;
|
||||||
@@ -1229,10 +1243,13 @@ public:
|
|||||||
bool bSampleStacksStarted = false;
|
bool bSampleStacksStarted = false;
|
||||||
bool bStartupApplyBoostStarted = false;
|
bool bStartupApplyBoostStarted = false;
|
||||||
bool bStartupApplyBoostEnded = false;
|
bool bStartupApplyBoostEnded = false;
|
||||||
|
uint64 StartupApplyBoostStartCycles = 0;
|
||||||
// Trace the resolved startup budget once per world. / Tracer une fois le budget de demarrage resolu par monde.
|
// Trace the resolved startup budget once per world. / Tracer une fois le budget de demarrage resolu par monde.
|
||||||
bool bStartupApplyConfigTraced = false;
|
bool bStartupApplyConfigTraced = false;
|
||||||
uint64 LastApplyFrameCycles = 0;
|
uint64 LastApplyFrameCycles = 0;
|
||||||
float StartupTraceSpeedCmPerSecond = 0.0f;
|
float StartupTraceSpeedCmPerSecond = 0.0f;
|
||||||
|
bool bSpeedAwareTravelMode = false;
|
||||||
|
bool bSpeedAwareFastMode = false;
|
||||||
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
||||||
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
||||||
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
||||||
@@ -1340,6 +1357,7 @@ public:
|
|||||||
double HeadlessStreamingTestDistanceCm = 0.0;
|
double HeadlessStreamingTestDistanceCm = 0.0;
|
||||||
float HeadlessStreamingTestSpeedCmPerSecond = 0.0f;
|
float HeadlessStreamingTestSpeedCmPerSecond = 0.0f;
|
||||||
float HeadlessStreamingTestStartSeconds = 0.0f;
|
float HeadlessStreamingTestStartSeconds = 0.0f;
|
||||||
|
float HeadlessStreamingTestMoveDurationSeconds = 0.0f;
|
||||||
float HeadlessStreamingTestExitSeconds = 0.0f;
|
float HeadlessStreamingTestExitSeconds = 0.0f;
|
||||||
float HeadlessStreamingTestTeleportAtSeconds = 0.0f;
|
float HeadlessStreamingTestTeleportAtSeconds = 0.0f;
|
||||||
float HeadlessStreamingTestTeleportDistanceCm = 0.0f;
|
float HeadlessStreamingTestTeleportDistanceCm = 0.0f;
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ public class VoxelForge : ModuleRules
|
|||||||
{
|
{
|
||||||
public VoxelForge(ReadOnlyTargetRules Target) : base(Target)
|
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
|
// PCH = Precompiled Headers - speeds up compilation
|
||||||
// UseExplicitOrSharedPCHs is the modern recommended setting
|
// UseExplicitOrSharedPCHs is the modern recommended setting
|
||||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ public class VoxelForgeCore : ModuleRules
|
|||||||
|
|
||||||
PublicDependencyModuleNames.Add("Core");
|
PublicDependencyModuleNames.Add("Core");
|
||||||
CppStandard = CppStandardVersion.Cpp20;
|
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.
|
// Canonical Standalone code predates UE's stricter warning defaults; keep this narrow to legal local shadowing.
|
||||||
CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off;
|
CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off;
|
||||||
FPSemantics = FPSemanticsMode.Precise;
|
FPSemantics = FPSemanticsMode.Precise;
|
||||||
|
|||||||
@@ -878,6 +878,8 @@ struct FExploreArguments
|
|||||||
bool bPassageFloorWritesOverride = false;
|
bool bPassageFloorWritesOverride = false;
|
||||||
int32 PassageFloorWrites = 1;
|
int32 PassageFloorWrites = 1;
|
||||||
bool bStandaloneWorldExport = false;
|
bool bStandaloneWorldExport = false;
|
||||||
|
bool bOwnerFourStratePlan = false;
|
||||||
|
bool bSkipStandaloneCoreAdapterParity = false;
|
||||||
bool bCoreWorldTest = false;
|
bool bCoreWorldTest = false;
|
||||||
bool bCoreWorldStreamTraceOnly = false;
|
bool bCoreWorldStreamTraceOnly = false;
|
||||||
bool bRenderCameraOverride = false;
|
bool bRenderCameraOverride = false;
|
||||||
@@ -922,6 +924,12 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
int32 StandaloneWorldExport = 0;
|
int32 StandaloneWorldExport = 0;
|
||||||
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
||||||
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
|
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;
|
int32 CoreWorldTest = 0;
|
||||||
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
|
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
|
||||||
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
|
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
|
||||||
@@ -1266,12 +1274,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The historical explorer contract needs two adjacent mouths around an interior slot. A
|
// Passage walks need both mouths; a render/export-only generation can inspect an end slot.
|
||||||
// parity export can instead target the game's authored single-strate layout, which has no
|
// Parity exports also accept the game's authored single-strate layout.
|
||||||
// such mouths and is valid for export-only mode. Build() still validates the slot against
|
const bool bRequiresInteriorSlot = OutArguments.bWalk;
|
||||||
// the loaded settings layout.
|
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot ? 0 : 1;
|
||||||
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig ? 0 : 1;
|
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot
|
||||||
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig
|
|
||||||
? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2;
|
? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2;
|
||||||
if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot)
|
if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot)
|
||||||
{
|
{
|
||||||
@@ -1284,8 +1291,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
OutError = FString::Printf(
|
OutError = FString::Printf(
|
||||||
TEXT("slot must be an interior slot in [1,%d] so arrival and departure mouths exist."),
|
TEXT("slot must be in [0,%d]; walk mode requires an interior slot in [1,%d] for both passage mouths."),
|
||||||
MaxSyntheticStrates - 2);
|
MaxSyntheticStrates - 1, MaxSyntheticStrates - 2);
|
||||||
}
|
}
|
||||||
return false;
|
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.");
|
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
|
||||||
return false;
|
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
|
if (OutArguments.bCoreWorldTest
|
||||||
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
|
&& (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
|
struct FExploreWorld
|
||||||
{
|
{
|
||||||
TStrongObjectPtr<UVoxelSettings> Settings;
|
TStrongObjectPtr<UVoxelSettings> Settings;
|
||||||
@@ -1834,7 +1912,7 @@ struct FExploreWorld
|
|||||||
Settings->InterStrateGapChunks, Settings->SkirtCells);
|
Settings->InterStrateGapChunks, Settings->SkirtCells);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int32 NumStrates = bUseAuthoredSettings
|
const int32 NumStrates = Arguments.bOwnerFourStratePlan ? 4 : bUseAuthoredSettings
|
||||||
? (Arguments.bGameRuntimeConfig
|
? (Arguments.bGameRuntimeConfig
|
||||||
? FMath::Max(Settings->TotalStrates, 1)
|
? FMath::Max(Settings->TotalStrates, 1)
|
||||||
: FMath::Max(Settings->TotalStrates, Arguments.Slot + 2))
|
: FMath::Max(Settings->TotalStrates, Arguments.Slot + 2))
|
||||||
@@ -1864,10 +1942,18 @@ struct FExploreWorld
|
|||||||
// loaded package remains untouched.
|
// loaded package remains untouched.
|
||||||
Settings->TotalStrates = NumStrates;
|
Settings->TotalStrates = NumStrates;
|
||||||
|
|
||||||
|
if (Arguments.bOwnerFourStratePlan)
|
||||||
|
{
|
||||||
|
if (!ApplyOwnerFourStratePlan(Settings.Get(), Definitions, true, OutError))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (bUseAuthoredStrate)
|
if (bUseAuthoredStrate)
|
||||||
{
|
{
|
||||||
// The owner’s snapshot states that every slot uses DA_Strate3. Clone it per slot so
|
// Keep the historical single-asset interior probe isolated from loaded packages.
|
||||||
// commandlet overrides never mutate a loaded package or leak between slots.
|
|
||||||
Settings->StratePool.Reset();
|
Settings->StratePool.Reset();
|
||||||
Settings->FixedStrates.Reset();
|
Settings->FixedStrates.Reset();
|
||||||
}
|
}
|
||||||
@@ -1923,6 +2009,7 @@ struct FExploreWorld
|
|||||||
}
|
}
|
||||||
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
|
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto ApplyTargetOverrides = [&Arguments](UVoxelStrateDefinition* Definition) -> bool
|
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> Actual = StandaloneCoreAggregateHashes(Rows);
|
||||||
const std::array<std::uint64_t, 4> Expected{
|
const std::array<std::uint64_t, 4> Expected{
|
||||||
0xdb2991c73215af4cull, 0x8236c5edd7fc8e4full,
|
0xde345030348f2c5dull, 0xcbf0888004618b87ull,
|
||||||
0x4c48fa75a543d58eull, 0xbea22e76b34a4dfcull};
|
0x7a11e28e052d69a8ull, 0x52b88607e8dbcbfdull};
|
||||||
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
|
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
|
||||||
const bool bPassageWalkabilityPassed = Stats.walkSamples == 5642 && Stats.walkFailures == 0;
|
constexpr std::uint64_t ExpectedWalkSamples = 21425;
|
||||||
if (Rows.size() != 2226 || Actual != Expected || !bPassageWalkabilityPassed)
|
const bool bPassageWalkabilityPassed = Stats.walkSamples == ExpectedWalkSamples
|
||||||
|
&& Stats.walkFailures == 0;
|
||||||
|
if (Rows.size() != 2578 || Actual != Expected || !bPassageWalkabilityPassed)
|
||||||
{
|
{
|
||||||
OutError = FString::Printf(
|
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>(WorkerLimit),
|
||||||
static_cast<unsigned long long>(Rows.size()),
|
static_cast<unsigned long long>(Rows.size()),
|
||||||
static_cast<unsigned long long>(Actual[0]),
|
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[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>(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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display,
|
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),
|
static_cast<unsigned long long>(WorkerLimit),
|
||||||
OwnerClipRadius,
|
OwnerClipRadius,
|
||||||
OwnerMaxLod,
|
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[0]),
|
||||||
static_cast<unsigned long long>(Actual[1]),
|
static_cast<unsigned long long>(Actual[1]),
|
||||||
static_cast<unsigned long long>(Actual[2]),
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -13515,6 +13607,24 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
*Arguments.SettingsReference);
|
*Arguments.SettingsReference);
|
||||||
return false;
|
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>(
|
UVoxelStrateManager* Manager = NewObject<UVoxelStrateManager>(
|
||||||
GetTransientPackage(), NAME_None, RF_Transient);
|
GetTransientPackage(), NAME_None, RF_Transient);
|
||||||
int32 Seed = AuthoredSettings->Seed;
|
int32 Seed = AuthoredSettings->Seed;
|
||||||
@@ -13526,7 +13636,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
OriginSpineRadius = Season->GetOriginSpineRadius();
|
OriginSpineRadius = Season->GetOriginSpineRadius();
|
||||||
WorldRadiusVoxels = Season->GetWorldRadiusVoxels();
|
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.");
|
OutError = TEXT("Owner settings did not produce a valid resolved strate layout.");
|
||||||
return false;
|
return false;
|
||||||
@@ -13534,7 +13644,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
Seed = Manager->GetWorldSeed();
|
Seed = Manager->GetWorldSeed();
|
||||||
UVoxelGenerator* Generator = NewObject<UVoxelGenerator>(
|
UVoxelGenerator* Generator = NewObject<UVoxelGenerator>(
|
||||||
GetTransientPackage(), NAME_None, RF_Transient);
|
GetTransientPackage(), NAME_None, RF_Transient);
|
||||||
Generator->InitializeSettings(AuthoredSettings);
|
Generator->InitializeSettings(ExportSettings);
|
||||||
Generator->SetStrateManager(Manager);
|
Generator->SetStrateManager(Manager);
|
||||||
|
|
||||||
TArray<FStandaloneExportSample> Samples;
|
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("source"), TEXT("VoxelForgeExplore standalone base-world export"));
|
||||||
Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm);
|
Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm);
|
||||||
Writer->WriteValue(TEXT("seed"), Seed);
|
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("origin_spine_radius"), OriginSpineRadius);
|
||||||
Writer->WriteValue(TEXT("world_radius_voxels"), WorldRadiusVoxels);
|
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("total_strates"), Manager->GetNumStrates());
|
||||||
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), AuthoredSettings->InterStrateGapChunks);
|
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), ExportSettings->InterStrateGapChunks);
|
||||||
Writer->WriteValue(TEXT("clip_radius"), AuthoredSettings->ClipRadius);
|
Writer->WriteValue(TEXT("clip_radius"), ExportSettings->ClipRadius);
|
||||||
Writer->WriteValue(TEXT("max_clip_level"), AuthoredSettings->MaxClipLevel);
|
Writer->WriteValue(TEXT("max_clip_level"), ExportSettings->MaxClipLevel);
|
||||||
Writer->WriteValue(TEXT("full_res_clip_levels"), AuthoredSettings->FullResClipLevels);
|
Writer->WriteValue(TEXT("full_res_clip_levels"), ExportSettings->FullResClipLevels);
|
||||||
Writer->WriteValue(TEXT("coarse_tile_cells"), AuthoredSettings->CoarseTileCells);
|
Writer->WriteValue(TEXT("coarse_tile_cells"), ExportSettings->CoarseTileCells);
|
||||||
Writer->WriteValue(TEXT("render_distance_chunks"), AuthoredSettings->RenderDistanceChunks);
|
Writer->WriteValue(TEXT("render_distance_chunks"), ExportSettings->RenderDistanceChunks);
|
||||||
Writer->WriteValue(TEXT("generate_skirts"), AuthoredSettings->bGenerateSkirts);
|
Writer->WriteValue(TEXT("generate_skirts"), ExportSettings->bGenerateSkirts);
|
||||||
Writer->WriteValue(TEXT("skirt_cells"), AuthoredSettings->SkirtCells);
|
Writer->WriteValue(TEXT("skirt_cells"), ExportSettings->SkirtCells);
|
||||||
Writer->WriteValue(TEXT("lod_octave_drop"), AuthoredSettings->LODOctaveDrop);
|
Writer->WriteValue(TEXT("lod_octave_drop"), ExportSettings->LODOctaveDrop);
|
||||||
Writer->WriteValue(TEXT("strate_content_cut_min_level"), AuthoredSettings->GetEffectiveStrateContentCutMinLevel());
|
Writer->WriteValue(TEXT("strate_content_cut_min_level"), ExportSettings->GetEffectiveStrateContentCutMinLevel());
|
||||||
TSet<const UObject*> ActiveObjects;
|
TSet<const UObject*> ActiveObjects;
|
||||||
StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"),
|
StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"),
|
||||||
StandaloneSerializeStruct(AuthoredSettings->GetClass(), AuthoredSettings,
|
StandaloneSerializeStruct(ExportSettings->GetClass(), ExportSettings,
|
||||||
ActiveObjects, AuthoredSettings));
|
ActiveObjects, ExportSettings));
|
||||||
|
|
||||||
Writer->WriteArrayStart(TEXT("strates"));
|
Writer->WriteArrayStart(TEXT("strates"));
|
||||||
for (const FStrateSlot& Slot : Manager->GetLayout())
|
for (const FStrateSlot& Slot : Manager->GetLayout())
|
||||||
@@ -13756,6 +13866,68 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
Writer->WriteObjectEnd();
|
Writer->WriteObjectEnd();
|
||||||
Writer->Close();
|
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"));
|
const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json"));
|
||||||
if (!FFileHelper::SaveStringToFile(
|
if (!FFileHelper::SaveStringToFile(
|
||||||
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||||
@@ -13764,11 +13936,16 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (OutJson) *OutJson = Json;
|
if (OutJson) *OutJson = Json;
|
||||||
if (!VerifyStandaloneCoreAdapter(Json, OutError))
|
if (!Arguments.bSkipStandaloneCoreAdapterParity && !VerifyStandaloneCoreAdapter(Json, OutError))
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
|
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
|
||||||
return false;
|
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,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
|
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(),
|
Seed, StandaloneCoreVoxelSizeCm, Manager->GetNumStrates(), Manager->GetPassages().Num(),
|
||||||
@@ -14808,7 +14985,7 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
|||||||
UVoxelSettings* Source, int32 Seed,
|
UVoxelSettings* Source, int32 Seed,
|
||||||
TStrongObjectPtr<UVoxelSettings>& OutSettings,
|
TStrongObjectPtr<UVoxelSettings>& OutSettings,
|
||||||
TStrongObjectPtr<UVoxelStrateDefinition>& OutSurfaceDefinition,
|
TStrongObjectPtr<UVoxelStrateDefinition>& OutSurfaceDefinition,
|
||||||
FString& OutError)
|
FString& OutError, bool bCompactTracePlan = false)
|
||||||
{
|
{
|
||||||
if (Source == nullptr)
|
if (Source == nullptr)
|
||||||
{
|
{
|
||||||
@@ -14824,11 +15001,10 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
|||||||
nullptr, TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"));
|
nullptr, TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"));
|
||||||
if (SurfaceDefinition == nullptr
|
if (SurfaceDefinition == nullptr
|
||||||
|| SurfaceDefinition->GeneratorType != ECaveGeneratorType::SurfaceWorld
|
|| SurfaceDefinition->GeneratorType != ECaveGeneratorType::SurfaceWorld
|
||||||
|| SurfaceDefinition->StrateHeightInChunks <= 0
|
|| SurfaceDefinition->StrateHeightInChunks <= 0)
|
||||||
|| Source->TotalStrates != 1)
|
|
||||||
{
|
{
|
||||||
OutError = FString::Printf(
|
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 ? 1 : 0,
|
||||||
SurfaceDefinition != nullptr ? static_cast<int32>(SurfaceDefinition->GeneratorType) : INDEX_NONE,
|
SurfaceDefinition != nullptr ? static_cast<int32>(SurfaceDefinition->GeneratorType) : INDEX_NONE,
|
||||||
SurfaceDefinition != nullptr ? SurfaceDefinition->StrateHeightInChunks : INDEX_NONE,
|
SurfaceDefinition != nullptr ? SurfaceDefinition->StrateHeightInChunks : INDEX_NONE,
|
||||||
@@ -14843,9 +15019,24 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
OutSettings = TStrongObjectPtr<UVoxelSettings>(SettingsCopy);
|
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);
|
OutSurfaceDefinition = TStrongObjectPtr<UVoxelStrateDefinition>(SurfaceDefinition);
|
||||||
SettingsCopy->Seed = Seed;
|
SettingsCopy->Seed = Seed;
|
||||||
SettingsCopy->TotalStrates = Source->TotalStrates;
|
SettingsCopy->TotalStrates = 1;
|
||||||
SettingsCopy->StratePool.Reset();
|
SettingsCopy->StratePool.Reset();
|
||||||
SettingsCopy->FixedStrates.Reset();
|
SettingsCopy->FixedStrates.Reset();
|
||||||
SettingsCopy->FixedStrates.Add(0,
|
SettingsCopy->FixedStrates.Add(0,
|
||||||
@@ -14855,8 +15046,10 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
|||||||
SettingsCopy->bUseWorldAssemblyManifestOverride = false;
|
SettingsCopy->bUseWorldAssemblyManifestOverride = false;
|
||||||
const int32 BottomChunkZ = 1 - SurfaceDefinition->StrateHeightInChunks;
|
const int32 BottomChunkZ = 1 - SurfaceDefinition->StrateHeightInChunks;
|
||||||
UE_LOG(LogTemp, Display,
|
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"),
|
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"),
|
||||||
Seed, BottomChunkZ, SurfaceDefinition->StrateHeightInChunks, SettingsCopy->InterStrateGapChunks);
|
static_cast<int32>(SurfaceDefinition->GeneratorType), Seed, BottomChunkZ,
|
||||||
|
SurfaceDefinition->StrateHeightInChunks,
|
||||||
|
bCompactTracePlan ? 1 : 0, SettingsCopy->InterStrateGapChunks);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16432,7 +16625,15 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
|||||||
}
|
}
|
||||||
float StagedWalkSpeedCmPerSecond = 0.0f;
|
float StagedWalkSpeedCmPerSecond = 0.0f;
|
||||||
float StagedSprintSpeedCmPerSecond = 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))
|
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -17065,7 +17266,8 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
|||||||
{
|
{
|
||||||
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
||||||
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
||||||
if (!World->LoadedTiles.Contains(Key)) return false;
|
if (!World->LoadedTiles.Contains(Key)
|
||||||
|
&& !World->CollisionNotRequiredTiles.Contains(Key)) return false;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const auto CollisionSetSettled = [](const AVoxelWorld* World)
|
const auto CollisionSetSettled = [](const AVoxelWorld* World)
|
||||||
@@ -17654,7 +17856,15 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
|
|
||||||
float StagedWalkSpeedCmPerSecond = 0.0f;
|
float StagedWalkSpeedCmPerSecond = 0.0f;
|
||||||
float StagedSprintSpeedCmPerSecond = 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))
|
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -17663,8 +17873,26 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings);
|
const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings);
|
||||||
TStrongObjectPtr<UVoxelSettings> OwnerSurfaceSettings;
|
TStrongObjectPtr<UVoxelSettings> OwnerSurfaceSettings;
|
||||||
TStrongObjectPtr<UVoxelStrateDefinition> OwnerSurfaceDefinition;
|
TStrongObjectPtr<UVoxelStrateDefinition> OwnerSurfaceDefinition;
|
||||||
if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed,
|
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
|
||||||
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError))
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -17822,6 +18050,25 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
|
OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
|
||||||
return false;
|
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);
|
USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient);
|
||||||
if (PawnRoot == nullptr)
|
if (PawnRoot == nullptr)
|
||||||
{
|
{
|
||||||
@@ -17830,7 +18077,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
}
|
}
|
||||||
PlayerPawn->SetRootComponent(PawnRoot);
|
PlayerPawn->SetRootComponent(PawnRoot);
|
||||||
PawnRoot->RegisterComponent();
|
PawnRoot->RegisterComponent();
|
||||||
PlayerPawn->SetActorLocation(FVector::ZeroVector, false, nullptr, ETeleportType::TeleportPhysics);
|
PlayerPawn->SetActorLocation(StartupTraceSpawnCm, false, nullptr, ETeleportType::TeleportPhysics);
|
||||||
PlayerController->Possess(PlayerPawn);
|
PlayerController->Possess(PlayerPawn);
|
||||||
if (TestWorld->GetFirstPlayerController() != PlayerController
|
if (TestWorld->GetFirstPlayerController() != PlayerController
|
||||||
|| PlayerController->GetPawn() != PlayerPawn)
|
|| PlayerController->GetPawn() != PlayerPawn)
|
||||||
@@ -17859,7 +18106,10 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1);
|
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1);
|
||||||
Actor->CoreClipRadius = 3;
|
Actor->CoreClipRadius = 3;
|
||||||
Actor->CoreMaxClipLevel = 8;
|
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);
|
Actor->FinishSpawning(ActorTransform);
|
||||||
TestWorld->InitializeActorsForPlay(FURL());
|
TestWorld->InitializeActorsForPlay(FURL());
|
||||||
TestWorld->BeginPlay();
|
TestWorld->BeginPlay();
|
||||||
@@ -17869,13 +18119,19 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Actor->SetActorTickEnabled(true);
|
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)
|
auto DesiredTilesLoaded = [](const AVoxelWorld* World)
|
||||||
{
|
{
|
||||||
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
||||||
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
||||||
{
|
{
|
||||||
if (!World->LoadedTiles.Contains(Key)) return false;
|
if (!World->LoadedTiles.Contains(Key)
|
||||||
|
&& !World->CollisionNotRequiredTiles.Contains(Key)) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -18023,6 +18279,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
const double CarveBrushRadiusCoreVoxels =
|
const double CarveBrushRadiusCoreVoxels =
|
||||||
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
||||||
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
|
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
|
||||||
|
FIntVector ColdStartCollisionCenter = FIntVector::ZeroValue;
|
||||||
uint64 ExpectedEditDigest = 0;
|
uint64 ExpectedEditDigest = 0;
|
||||||
std::array<std::uint64_t, 4> InitialAggregates{};
|
std::array<std::uint64_t, 4> InitialAggregates{};
|
||||||
std::array<std::uint64_t, 4> TravelAggregates{};
|
std::array<std::uint64_t, 4> TravelAggregates{};
|
||||||
@@ -18208,10 +18465,17 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
BiomeProbeSpanVoxels);
|
BiomeProbeSpanVoxels);
|
||||||
|
|
||||||
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
|
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())
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
ExpectedInitialTileCount = Desired.size();
|
ExpectedInitialTileCount = Desired.size();
|
||||||
@@ -18258,7 +18522,23 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||||
return false;
|
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)
|
if (Arguments.bCoreHybridCliffOnly)
|
||||||
{
|
{
|
||||||
@@ -18332,6 +18612,47 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
||||||
else
|
else
|
||||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
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;
|
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
|
||||||
AggregateRows.reserve(Desired.size());
|
AggregateRows.reserve(Desired.size());
|
||||||
bool bFoundCarveVertex = false;
|
bool bFoundCarveVertex = false;
|
||||||
@@ -19264,7 +19585,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
||||||
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
||||||
{
|
{
|
||||||
if (!World->LoadedTiles.Contains(Key)) return false;
|
if (!World->LoadedTiles.Contains(Key)
|
||||||
|
&& !World->CollisionNotRequiredTiles.Contains(Key)) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -19367,7 +19689,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
|||||||
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
||||||
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
|
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);
|
ExpectedCollisionSet.Add(Key);
|
||||||
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
|
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
|
||||||
if (Record == nullptr)
|
if (Record == nullptr)
|
||||||
@@ -21361,6 +21683,44 @@ int32 RunBatch(const FString& Params)
|
|||||||
|
|
||||||
int32 UVoxelForgeExploreCommandlet::Main(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")))
|
if (FParse::Param(*Params, TEXT("step2f")))
|
||||||
{
|
{
|
||||||
return RunStep2f(Params);
|
return RunStep2f(Params);
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ fi
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ROOT="$SCRIPT_DIR"
|
ROOT="$SCRIPT_DIR"
|
||||||
|
PROJECT_ROOT="$(cd -- "$ROOT/.." && pwd)"
|
||||||
|
VERIFY_DIR="$ROOT/Tools/verify"
|
||||||
COMPILER="clang++-19"
|
COMPILER="clang++-19"
|
||||||
VARIANT="clang19"
|
VARIANT="clang19"
|
||||||
FP_CONTRACT="off"
|
FP_CONTRACT="off"
|
||||||
JOBS=12
|
JOBS=12
|
||||||
|
SANITIZERS=""
|
||||||
|
EXCLUDE_UNSIGNED_OVERFLOW=0
|
||||||
|
|
||||||
while (($#)); do
|
while (($#)); do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -18,6 +22,8 @@ while (($#)); do
|
|||||||
--variant=*) VARIANT="${1#*=}" ;;
|
--variant=*) VARIANT="${1#*=}" ;;
|
||||||
--fp-contract=*) FP_CONTRACT="${1#*=}" ;;
|
--fp-contract=*) FP_CONTRACT="${1#*=}" ;;
|
||||||
--jobs=*) JOBS="${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 ;;
|
*) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
@@ -41,6 +47,15 @@ BIN_DIR="${VOXELFORGE_BIN_DIR:-$ROOT/bin/linux}"
|
|||||||
BUILD_DIR="$BUILD_ROOT/linux-$VARIANT"
|
BUILD_DIR="$BUILD_ROOT/linux-$VARIANT"
|
||||||
mkdir -p "$BUILD_DIR/wrappers" "$BUILD_DIR/objects" "$BIN_DIR"
|
mkdir -p "$BUILD_DIR/wrappers" "$BUILD_DIR/objects" "$BIN_DIR"
|
||||||
|
|
||||||
|
STAMP_HELPER="$VERIFY_DIR/source_stamp.py"
|
||||||
|
STAMP_ARGS=()
|
||||||
|
if [[ "${VOXELFORGE_INCLUDE_UNREAL_ADAPTER:-0}" == "1" ]]; then
|
||||||
|
STAMP_ARGS+=(--include-unreal-adapter)
|
||||||
|
fi
|
||||||
|
SOURCE_STAMP="$(python3 -B "$STAMP_HELPER" stamp --root "$PROJECT_ROOT" "${STAMP_ARGS[@]}")"
|
||||||
|
python3 -B "$STAMP_HELPER" write-manifest --root "$PROJECT_ROOT" --output "$BUILD_DIR/source-manifest.txt" "${STAMP_ARGS[@]}" >/dev/null
|
||||||
|
printf 'Source stamp: %s\n' "$SOURCE_STAMP"
|
||||||
|
|
||||||
INCLUDES=(
|
INCLUDES=(
|
||||||
"-I$ROOT/Core/include"
|
"-I$ROOT/Core/include"
|
||||||
"-I$ROOT/Core/src"
|
"-I$ROOT/Core/src"
|
||||||
@@ -49,13 +64,32 @@ INCLUDES=(
|
|||||||
"-I$ROOT/Source/VoxelForge/Public"
|
"-I$ROOT/Source/VoxelForge/Public"
|
||||||
"-I$ROOT/Source/VoxelForge/Private"
|
"-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=(
|
FLAGS=(
|
||||||
-std=c++20 -O2 -mavx2 -mfma -fno-fast-math
|
-std=c++20 "$OPT_LEVEL" -mavx2 -mfma -fno-fast-math
|
||||||
-fexceptions -pthread "${INCLUDES[@]}"
|
-fexceptions -pthread "${INCLUDES[@]}"
|
||||||
|
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\""
|
||||||
)
|
)
|
||||||
|
FLAGS+=("${LINK_FLAGS[@]}")
|
||||||
if [[ "$FP_CONTRACT" != "default" ]]; then
|
if [[ "$FP_CONTRACT" != "default" ]]; then
|
||||||
FLAGS+=("-ffp-contract=$FP_CONTRACT")
|
FLAGS+=("-ffp-contract=$FP_CONTRACT")
|
||||||
fi
|
fi
|
||||||
|
if [[ "${VOXELFORGE_PROFILE_LEGACY_TERMS:-0}" == "1" ]]; then
|
||||||
|
FLAGS+=(-DVF_L76_PROFILE_LEGACY_TERMS=1)
|
||||||
|
fi
|
||||||
|
if [[ "${VOXELFORGE_PROFILE_L77:-0}" == "1" ]]; then
|
||||||
|
FLAGS+=(-DVF_L77_PROFILE=1 -g)
|
||||||
|
LINK_FLAGS+=(-no-pie)
|
||||||
|
fi
|
||||||
if [[ "$($COMPILER --version | head -n 1)" == *clang* ]]; then
|
if [[ "$($COMPILER --version | head -n 1)" == *clang* ]]; then
|
||||||
FLAGS+=(-Wno-c++11-narrowing)
|
FLAGS+=(-Wno-c++11-narrowing)
|
||||||
else
|
else
|
||||||
@@ -112,6 +146,11 @@ done < "$ROOT/Core/core_sources.txt"
|
|||||||
headless_object="$BUILD_DIR/objects/StreamBenchmarkHeadless.o"
|
headless_object="$BUILD_DIR/objects/StreamBenchmarkHeadless.o"
|
||||||
compile_background "$ROOT/Source/StreamBenchmarkHeadless.cpp" "$headless_object"
|
compile_background "$ROOT/Source/StreamBenchmarkHeadless.cpp" "$headless_object"
|
||||||
LEGACY_OBJECTS+=("$headless_object")
|
LEGACY_OBJECTS+=("$headless_object")
|
||||||
|
if [[ "${VOXELFORGE_PROFILE_L77:-0}" == "1" ]]; then
|
||||||
|
l77_profiler_object="$BUILD_DIR/objects/Linux77Profiler.o"
|
||||||
|
compile_background "$ROOT/Source/Linux77Profiler.cpp" "$l77_profiler_object"
|
||||||
|
LEGACY_OBJECTS+=("$l77_profiler_object")
|
||||||
|
fi
|
||||||
|
|
||||||
for pid in "${PIDS[@]}"; do wait "$pid"; done
|
for pid in "${PIDS[@]}"; do wait "$pid"; done
|
||||||
PIDS=()
|
PIDS=()
|
||||||
@@ -119,11 +158,19 @@ PIDS=()
|
|||||||
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
|
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
|
||||||
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
|
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
|
||||||
OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT"
|
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"
|
CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT"
|
||||||
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
|
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
|
||||||
|
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" -I"$ROOT/Source" \
|
||||||
"$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \
|
"$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \
|
||||||
$(pkg-config --cflags --libs egl glesv2 libpng)
|
$(pkg-config --cflags --libs egl glesv2 libpng)
|
||||||
|
|
||||||
|
POST_BUILD_STAMP="$(python3 -B "$STAMP_HELPER" stamp --root "$PROJECT_ROOT" "${STAMP_ARGS[@]}")"
|
||||||
|
if [[ "$POST_BUILD_STAMP" != "$SOURCE_STAMP" ]]; then
|
||||||
|
printf 'Source changed during build: before=%s after=%s\n' "$SOURCE_STAMP" "$POST_BUILD_STAMP" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT"
|
printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT"
|
||||||
printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_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
|
#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.
|
// 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.
|
// This is a value/container compatibility layer; it does not start or link Unreal.
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -45,6 +50,9 @@
|
|||||||
#define GENERATED_UCLASS_BODY()
|
#define GENERATED_UCLASS_BODY()
|
||||||
#define VOXELFORGE_API
|
#define VOXELFORGE_API
|
||||||
#define FORCEINLINE inline
|
#define FORCEINLINE inline
|
||||||
|
#ifdef WITH_EDITOR
|
||||||
|
#undef WITH_EDITOR
|
||||||
|
#endif
|
||||||
#define WITH_EDITOR 0
|
#define WITH_EDITOR 0
|
||||||
#define TRACE_CPUPROFILER_EVENT_SCOPE(...)
|
#define TRACE_CPUPROFILER_EVENT_SCOPE(...)
|
||||||
#define PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS
|
#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);} };
|
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>;
|
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};};
|
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
|
#pragma once
|
||||||
|
|
||||||
|
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||||
|
&& (defined(__clang__) || defined(__GNUC__))
|
||||||
|
#include_next <Json.h>
|
||||||
|
#else
|
||||||
|
|
||||||
#include <charconv>
|
#include <charconv>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
@@ -162,3 +167,4 @@ namespace VFJson
|
|||||||
return Parser(std::move(Text)).Parse(Out, Error);
|
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
|
#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 STATCAT_Advanced 0
|
||||||
#define DECLARE_STATS_GROUP(Name,Group,Category) constexpr int32 Group=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
|
#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 SET_DWORD_STAT(Stat,Value) ((void)0)
|
||||||
#define SCOPE_CYCLE_COUNTER(Stat) ((void)0)
|
#define SCOPE_CYCLE_COUNTER(Stat) ((void)0)
|
||||||
#define QUICK_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
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))
|
||||||
|
#include <intrin.h>
|
||||||
|
#elif defined(__i386__) || defined(__x86_64__)
|
||||||
|
#include <immintrin.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace VoxelForgeCoreShared
|
||||||
|
{
|
||||||
|
inline std::uint64_t ReadCpuCycles() noexcept
|
||||||
|
{
|
||||||
|
#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))
|
||||||
|
_mm_lfence();
|
||||||
|
const std::uint64_t value=__rdtsc();
|
||||||
|
_mm_lfence();
|
||||||
|
return value;
|
||||||
|
#elif defined(__i386__) || defined(__x86_64__)
|
||||||
|
_mm_lfence();
|
||||||
|
const std::uint64_t value=__rdtsc();
|
||||||
|
_mm_lfence();
|
||||||
|
return value;
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::uint64_t CpuCycleReadPairOverhead() noexcept
|
||||||
|
{
|
||||||
|
static const std::uint64_t overhead=[]()
|
||||||
|
{
|
||||||
|
std::uint64_t minimum=std::numeric_limits<std::uint64_t>::max();
|
||||||
|
for(unsigned i=0;i<64;++i)
|
||||||
|
{
|
||||||
|
const std::uint64_t start=ReadCpuCycles();
|
||||||
|
const std::uint64_t end=ReadCpuCycles();
|
||||||
|
minimum=std::min(minimum,end-start);
|
||||||
|
}
|
||||||
|
return minimum==std::numeric_limits<std::uint64_t>::max()?0:minimum;
|
||||||
|
}();
|
||||||
|
return overhead;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::uint64_t AdjustedCpuCycleDelta(std::uint64_t start,
|
||||||
|
std::uint64_t end) noexcept
|
||||||
|
{
|
||||||
|
const std::uint64_t delta=end-start;
|
||||||
|
const std::uint64_t overhead=CpuCycleReadPairOverhead();
|
||||||
|
return delta>overhead?delta-overhead:0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VoxelForgeCore/CpuCycleCounter.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace VoxelForgeCore::floating_island_detail
|
||||||
|
{
|
||||||
|
enum class EIslandFieldCaller : std::uint8_t
|
||||||
|
{
|
||||||
|
GridFill=0,Skirt=1,Boundary=2,PassageFilter=3,Other=4,Count=5
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class EIslandAttributionBucket : std::uint8_t
|
||||||
|
{
|
||||||
|
Field=0,Island=1,Root=2,Connector=3,Helix=4,Column=5,
|
||||||
|
LocalProbe=6,SharedCache=7,MouthFilter=8,Boundary=9,CallerGrid=10,
|
||||||
|
CallerSkirt=11,CallerBoundary=12,CallerPassageFilter=13,CallerOther=14,
|
||||||
|
Count=15
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FIslandFieldAttributionInstrumentation
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
std::array<std::uint64_t,8> traceSet{};
|
||||||
|
std::array<std::int64_t,8> traceX{},traceY{},traceEvictedX{},traceEvictedY{};
|
||||||
|
std::array<std::uint64_t,8> traceIsland{},traceEvictedIsland{};
|
||||||
|
std::array<std::uint8_t,8> traceFlags{};
|
||||||
|
std::size_t traceCount=0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline FIslandFieldAttributionInstrumentation*& ActiveIslandFieldAttribution() noexcept
|
||||||
|
{
|
||||||
|
static thread_local FIslandFieldAttributionInstrumentation* active=nullptr;
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FIslandFieldAttributionInstrumentation* SetActiveIslandFieldAttribution(
|
||||||
|
FIslandFieldAttributionInstrumentation* value) noexcept
|
||||||
|
{
|
||||||
|
auto* previous=ActiveIslandFieldAttribution();
|
||||||
|
ActiveIslandFieldAttribution()=value;
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline EIslandFieldCaller& ActiveIslandFieldCaller() noexcept
|
||||||
|
{
|
||||||
|
static thread_local EIslandFieldCaller caller=EIslandFieldCaller::Other;
|
||||||
|
return caller;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FScopedIslandFieldCaller
|
||||||
|
{
|
||||||
|
explicit FScopedIslandFieldCaller(EIslandFieldCaller value) noexcept
|
||||||
|
:previous(ActiveIslandFieldCaller()),enabled(ActiveIslandFieldAttribution()!=nullptr)
|
||||||
|
{if(enabled)ActiveIslandFieldCaller()=value;}
|
||||||
|
~FScopedIslandFieldCaller(){if(enabled)ActiveIslandFieldCaller()=previous;}
|
||||||
|
EIslandFieldCaller previous;
|
||||||
|
bool enabled=false;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::array<std::uint64_t,
|
||||||
|
static_cast<std::size_t>(EIslandAttributionBucket::Count)>&
|
||||||
|
IslandComponentSampleSequence() noexcept
|
||||||
|
{
|
||||||
|
static thread_local std::array<std::uint64_t,
|
||||||
|
static_cast<std::size_t>(EIslandAttributionBucket::Count)> sequence{};
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::array<std::uint64_t,
|
||||||
|
static_cast<std::size_t>(EIslandFieldCaller::Count)>&
|
||||||
|
IslandFieldSampleSequence() noexcept
|
||||||
|
{
|
||||||
|
static thread_local std::array<std::uint64_t,
|
||||||
|
static_cast<std::size_t>(EIslandFieldCaller::Count)> sequence{};
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::array<std::uint64_t,
|
||||||
|
static_cast<std::size_t>(EIslandFieldCaller::Count)>&
|
||||||
|
IslandNativeSampleSequence() noexcept
|
||||||
|
{
|
||||||
|
static thread_local std::array<std::uint64_t,
|
||||||
|
static_cast<std::size_t>(EIslandFieldCaller::Count)> sequence{};
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FScopedIslandAttributionTimer
|
||||||
|
{
|
||||||
|
explicit FScopedIslandAttributionTimer(EIslandAttributionBucket bucket) noexcept
|
||||||
|
:metrics(ActiveIslandFieldAttribution()),bucketIndex(static_cast<std::size_t>(bucket))
|
||||||
|
{
|
||||||
|
if(!metrics)return;
|
||||||
|
if(bucketIndex==static_cast<std::size_t>(EIslandAttributionBucket::Field))
|
||||||
|
{
|
||||||
|
const auto callerIndex=static_cast<std::size_t>(ActiveIslandFieldCaller());
|
||||||
|
const auto calls=++IslandFieldSampleSequence()[callerIndex];
|
||||||
|
const std::uint64_t phase=(callerIndex*43u+17u)
|
||||||
|
&FIslandFieldAttributionInstrumentation::SampleMask;
|
||||||
|
sampled=(((calls+phase)&FIslandFieldAttributionInstrumentation::SampleMask)==0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto& calls=IslandComponentSampleSequence()[bucketIndex];
|
||||||
|
++calls;
|
||||||
|
const std::uint64_t phase=(static_cast<std::uint64_t>(bucketIndex)*29u+17u)
|
||||||
|
&FIslandFieldAttributionInstrumentation::SampleMask;
|
||||||
|
sampled=(((calls+phase)&FIslandFieldAttributionInstrumentation::SampleMask)==0);
|
||||||
|
}
|
||||||
|
if(sampled)
|
||||||
|
{
|
||||||
|
start=VoxelForgeCoreShared::ReadCpuCycles();
|
||||||
|
wallStart=std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
~FScopedIslandAttributionTimer(){Stop();}
|
||||||
|
std::uint64_t Stop() noexcept
|
||||||
|
{
|
||||||
|
if(!sampled)return 0;
|
||||||
|
sampled=false;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
FIslandFieldAttributionInstrumentation* metrics=nullptr;
|
||||||
|
std::size_t bucketIndex=0;
|
||||||
|
std::uint64_t start=0;
|
||||||
|
std::chrono::steady_clock::time_point wallStart{};
|
||||||
|
bool sampled=false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FScopedIslandAttributionSample
|
||||||
|
{
|
||||||
|
explicit FScopedIslandAttributionSample() noexcept
|
||||||
|
{
|
||||||
|
auto* attribution=ActiveIslandFieldAttribution();
|
||||||
|
if(!attribution)return;
|
||||||
|
const auto caller=ActiveIslandFieldCaller();
|
||||||
|
const std::size_t callerIndex=static_cast<std::size_t>(caller);
|
||||||
|
const std::uint64_t sequence=++IslandNativeSampleSequence()[callerIndex];
|
||||||
|
const std::uint64_t phase=(callerIndex*43u+93u)
|
||||||
|
&FIslandFieldAttributionInstrumentation::SampleMask;
|
||||||
|
if(((sequence+phase)&FIslandFieldAttributionInstrumentation::SampleMask)!=0)return;
|
||||||
|
sampledAttribution=attribution;
|
||||||
|
sampledCaller=caller;
|
||||||
|
sampled=true;
|
||||||
|
start=VoxelForgeCoreShared::ReadCpuCycles();
|
||||||
|
wallStart=std::chrono::steady_clock::now();
|
||||||
|
++attribution->sampledNativeCalls[callerIndex];
|
||||||
|
}
|
||||||
|
~FScopedIslandAttributionSample()
|
||||||
|
{
|
||||||
|
if(!sampled)return;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace VoxelForgeCore::Linux77Profiler
|
||||||
|
{
|
||||||
|
enum class ERoute : std::uint8_t
|
||||||
|
{
|
||||||
|
Dispatch=0,PackedScalar=1,PackedX8=2,AnalyticScalar=3,
|
||||||
|
AnalyticBatch=4,Native=5,LegacySurface=6,SharedSource=7,Count=8
|
||||||
|
};
|
||||||
|
enum class EStage : std::uint8_t
|
||||||
|
{
|
||||||
|
Other=0,GridFill=1,MarchingCubes=2,Skirt=3,Refine=4,Count=5
|
||||||
|
};
|
||||||
|
enum class EPhase : std::uint8_t
|
||||||
|
{
|
||||||
|
Dispatch=0,BaseField=1,CompositionGraph=2,Bands=3,Passage=4,
|
||||||
|
Support=5,Caps=6,Stamps=7,Edit=8,Other=9,Count=10
|
||||||
|
};
|
||||||
|
inline constexpr std::size_t RouteCount=static_cast<std::size_t>(ERoute::Count);
|
||||||
|
inline constexpr std::size_t StageCount=static_cast<std::size_t>(EStage::Count);
|
||||||
|
inline constexpr std::size_t PhaseCount=static_cast<std::size_t>(EPhase::Count);
|
||||||
|
inline thread_local bool ThreadContextActive=false;
|
||||||
|
|
||||||
|
struct FThreadContext
|
||||||
|
{
|
||||||
|
std::uint8_t lod=0xff;
|
||||||
|
ERoute route=ERoute::Count;
|
||||||
|
EStage stage=EStage::Other;
|
||||||
|
EPhase phase=EPhase::Other;
|
||||||
|
bool halo=false;
|
||||||
|
};
|
||||||
|
|
||||||
|
#if defined(VF_L77_PROFILE)
|
||||||
|
bool IsEnabled() noexcept;
|
||||||
|
void SetEnabled(bool enabled) noexcept;
|
||||||
|
void RegisterThread();
|
||||||
|
void Start(std::uint32_t periodMicroseconds=1000);
|
||||||
|
void StopAndPrint();
|
||||||
|
inline thread_local FThreadContext CurrentContext{};
|
||||||
|
|
||||||
|
struct FScopedRoute
|
||||||
|
{
|
||||||
|
explicit FScopedRoute(ERoute value) noexcept
|
||||||
|
:active(ThreadContextActive),previous(CurrentContext.route)
|
||||||
|
{if(active)CurrentContext.route=value;}
|
||||||
|
~FScopedRoute(){if(active)CurrentContext.route=previous;}
|
||||||
|
bool active=false;ERoute previous=ERoute::Dispatch;
|
||||||
|
};
|
||||||
|
struct FScopedStage
|
||||||
|
{
|
||||||
|
explicit FScopedStage(EStage value) noexcept
|
||||||
|
:active(ThreadContextActive),previous(CurrentContext.stage)
|
||||||
|
{if(active)CurrentContext.stage=value;}
|
||||||
|
~FScopedStage(){Restore();}
|
||||||
|
void Restore() noexcept
|
||||||
|
{if(active){CurrentContext.stage=previous;active=false;}}
|
||||||
|
bool active=false;EStage previous=EStage::Other;
|
||||||
|
};
|
||||||
|
struct FScopedPhase
|
||||||
|
{
|
||||||
|
explicit FScopedPhase(EPhase value) noexcept
|
||||||
|
:active(ThreadContextActive),previous(CurrentContext.phase)
|
||||||
|
{if(active)CurrentContext.phase=value;}
|
||||||
|
~FScopedPhase(){if(active)CurrentContext.phase=previous;}
|
||||||
|
bool active=false;EPhase previous=EPhase::Dispatch;
|
||||||
|
};
|
||||||
|
struct FScopedLod
|
||||||
|
{
|
||||||
|
explicit FScopedLod(int value) noexcept
|
||||||
|
:active(IsEnabled()),previous(CurrentContext.lod),previousContextActive(ThreadContextActive)
|
||||||
|
{
|
||||||
|
if(active)
|
||||||
|
{
|
||||||
|
ThreadContextActive=true;
|
||||||
|
RegisterThread();
|
||||||
|
CurrentContext.lod=static_cast<std::uint8_t>(value<0||value>8?0xff:value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
~FScopedLod()
|
||||||
|
{if(active){CurrentContext.lod=previous;ThreadContextActive=previousContextActive;}}
|
||||||
|
bool active=false;std::uint8_t previous=0xff;
|
||||||
|
bool previousContextActive=false;
|
||||||
|
};
|
||||||
|
struct FScopedHalo
|
||||||
|
{
|
||||||
|
explicit FScopedHalo(bool value) noexcept
|
||||||
|
:active(ThreadContextActive),previous(CurrentContext.halo)
|
||||||
|
{if(active)CurrentContext.halo=value;}
|
||||||
|
~FScopedHalo(){if(active)CurrentContext.halo=previous;}
|
||||||
|
bool active=false;bool previous=false;
|
||||||
|
};
|
||||||
|
#else
|
||||||
|
inline bool IsEnabled() noexcept{return false;}
|
||||||
|
inline void SetEnabled(bool) noexcept{}
|
||||||
|
inline void RegisterThread(){}
|
||||||
|
inline void Start(std::uint32_t=1000){}
|
||||||
|
inline void StopAndPrint(){}
|
||||||
|
struct FScopedRoute{explicit FScopedRoute(ERoute) noexcept{}};
|
||||||
|
struct FScopedStage
|
||||||
|
{explicit FScopedStage(EStage) noexcept{}void Restore() noexcept{}};
|
||||||
|
struct FScopedPhase{explicit FScopedPhase(EPhase) noexcept{}};
|
||||||
|
struct FScopedLod{explicit FScopedLod(int) noexcept{}};
|
||||||
|
struct FScopedHalo{explicit FScopedHalo(bool) noexcept{}};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct FDispatchCounter
|
||||||
|
{
|
||||||
|
std::uint64_t calls=0;
|
||||||
|
std::uint64_t points=0;
|
||||||
|
};
|
||||||
|
using FDispatchCounters=std::array<std::array<FDispatchCounter,RouteCount>,StageCount>;
|
||||||
|
|
||||||
|
inline const char* RouteName(ERoute value) noexcept
|
||||||
|
{
|
||||||
|
switch(value)
|
||||||
|
{
|
||||||
|
case ERoute::Dispatch:return "dispatch_lookup";
|
||||||
|
case ERoute::PackedScalar:return "packed_scalar";
|
||||||
|
case ERoute::PackedX8:return "packed_x8";
|
||||||
|
case ERoute::AnalyticScalar:return "analytic_scalar";
|
||||||
|
case ERoute::AnalyticBatch:return "analytic_batch";
|
||||||
|
case ERoute::Native:return "native";
|
||||||
|
case ERoute::LegacySurface:return "legacy_surface";
|
||||||
|
case ERoute::SharedSource:return "shared_source";
|
||||||
|
default:return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inline const char* StageName(EStage value) noexcept
|
||||||
|
{
|
||||||
|
switch(value)
|
||||||
|
{
|
||||||
|
case EStage::GridFill:return "grid_fill";
|
||||||
|
case EStage::MarchingCubes:return "marching_cubes";
|
||||||
|
case EStage::Skirt:return "skirt";
|
||||||
|
case EStage::Refine:return "refine";
|
||||||
|
default:return "other";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inline const char* PhaseName(EPhase value) noexcept
|
||||||
|
{
|
||||||
|
switch(value)
|
||||||
|
{
|
||||||
|
case EPhase::Dispatch:return "dispatch_lookup";
|
||||||
|
case EPhase::BaseField:return "base_field";
|
||||||
|
case EPhase::CompositionGraph:return "composition_graph";
|
||||||
|
case EPhase::Bands:return "composition_bands";
|
||||||
|
case EPhase::Passage:return "passages";
|
||||||
|
case EPhase::Support:return "support";
|
||||||
|
case EPhase::Caps:return "caps";
|
||||||
|
case EPhase::Stamps:return "stamps";
|
||||||
|
case EPhase::Edit:return "edits";
|
||||||
|
default:return "other";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "VoxelForgeCore/CpuCycleCounter.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <chrono>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -12,6 +15,23 @@
|
|||||||
|
|
||||||
namespace VoxelForgeCoreShared
|
namespace VoxelForgeCoreShared
|
||||||
{
|
{
|
||||||
|
struct FColumnCacheMetrics
|
||||||
|
{
|
||||||
|
std::uint64_t hits = 0;
|
||||||
|
std::uint64_t misses = 0;
|
||||||
|
std::uint64_t computes = 0;
|
||||||
|
std::uint64_t conflictReplacements = 0;
|
||||||
|
std::uint64_t lockWaitNanoseconds = 0;
|
||||||
|
std::uint64_t sampledInitialReadCycles = 0;
|
||||||
|
std::uint64_t sampledInitialReads = 0;
|
||||||
|
std::uint64_t sampledLockedReadCycles = 0;
|
||||||
|
std::uint64_t sampledLockedReads = 0;
|
||||||
|
std::uint64_t sampledLockCycles = 0;
|
||||||
|
std::uint64_t sampledLocks = 0;
|
||||||
|
std::uint64_t sampledBuildCycles = 0;
|
||||||
|
std::uint64_t sampledBuilds = 0;
|
||||||
|
};
|
||||||
|
|
||||||
// Exact input identity for a pure XY column value. LOD is deliberately not part of this key:
|
// Exact input identity for a pure XY column value. LOD is deliberately not part of this key:
|
||||||
// the same recipe/plan column has the same bits at every sampling step.
|
// the same recipe/plan column has the same bits at every sampling step.
|
||||||
struct FColumnCacheKey
|
struct FColumnCacheKey
|
||||||
@@ -69,13 +89,28 @@ namespace VoxelForgeCoreShared
|
|||||||
|
|
||||||
struct FSpinGuard
|
struct FSpinGuard
|
||||||
{
|
{
|
||||||
explicit FSpinGuard(std::atomic_flag& value) : Flag(value)
|
explicit FSpinGuard(std::atomic_flag& value, FColumnCacheMetrics* metrics,
|
||||||
|
bool sampleCycles)
|
||||||
|
: Flag(value), Metrics(metrics)
|
||||||
{
|
{
|
||||||
|
const auto started = Metrics ? std::chrono::steady_clock::now()
|
||||||
|
: std::chrono::steady_clock::time_point{};
|
||||||
|
const std::uint64_t cycleStart=sampleCycles?ReadCpuCycles():0;
|
||||||
while (Flag.test_and_set(std::memory_order_acquire))
|
while (Flag.test_and_set(std::memory_order_acquire))
|
||||||
std::this_thread::yield();
|
std::this_thread::yield();
|
||||||
|
if (Metrics)
|
||||||
|
Metrics->lockWaitNanoseconds += static_cast<std::uint64_t>(
|
||||||
|
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
|
std::chrono::steady_clock::now() - started).count());
|
||||||
|
if(sampleCycles)
|
||||||
|
{
|
||||||
|
Metrics->sampledLockCycles+=AdjustedCpuCycleDelta(cycleStart,ReadCpuCycles());
|
||||||
|
++Metrics->sampledLocks;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
~FSpinGuard() { Flag.clear(std::memory_order_release); }
|
~FSpinGuard() { Flag.clear(std::memory_order_release); }
|
||||||
std::atomic_flag& Flag;
|
std::atomic_flag& Flag;
|
||||||
|
FColumnCacheMetrics* Metrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -94,7 +129,7 @@ namespace VoxelForgeCoreShared
|
|||||||
|
|
||||||
template <typename TFactory>
|
template <typename TFactory>
|
||||||
TValue getOrCompute(const FColumnCacheKey& key, TFactory&& factory,
|
TValue getOrCompute(const FColumnCacheKey& key, TFactory&& factory,
|
||||||
bool& computed)
|
bool& computed, FColumnCacheMetrics* metrics = nullptr)
|
||||||
{
|
{
|
||||||
computed = false;
|
computed = false;
|
||||||
const FKeyWords keyWords = key.words();
|
const FKeyWords keyWords = key.words();
|
||||||
@@ -102,13 +137,47 @@ namespace VoxelForgeCoreShared
|
|||||||
const std::size_t setIndex = static_cast<std::size_t>(tag) & (SetCount - 1);
|
const std::size_t setIndex = static_cast<std::size_t>(tag) & (SetCount - 1);
|
||||||
FSet& set = Sets[setIndex];
|
FSet& set = Sets[setIndex];
|
||||||
TValue value{};
|
TValue value{};
|
||||||
if (TryReadSet(set, keyWords, tag, value)) return value;
|
static thread_local std::uint64_t timingSequence=0;
|
||||||
|
const bool sampleCycles=metrics&&((++timingSequence&127u)==0);
|
||||||
|
const std::uint64_t initialReadStart=sampleCycles?ReadCpuCycles():0;
|
||||||
|
const bool initialHit=TryReadSet(set, keyWords, tag, value);
|
||||||
|
if(sampleCycles)
|
||||||
|
{
|
||||||
|
metrics->sampledInitialReadCycles+=AdjustedCpuCycleDelta(
|
||||||
|
initialReadStart,ReadCpuCycles());
|
||||||
|
++metrics->sampledInitialReads;
|
||||||
|
}
|
||||||
|
if (initialHit)
|
||||||
|
{
|
||||||
|
if (metrics) ++metrics->hits;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (metrics) ++metrics->misses;
|
||||||
|
|
||||||
FSpinGuard lock(set.Writer);
|
FSpinGuard lock(set.Writer, metrics,sampleCycles);
|
||||||
if (TryReadSet(set, keyWords, tag, value)) return value;
|
const std::uint64_t lockedReadStart=sampleCycles?ReadCpuCycles():0;
|
||||||
|
const bool lockedHit=TryReadSet(set, keyWords, tag, value);
|
||||||
|
if(sampleCycles)
|
||||||
|
{
|
||||||
|
metrics->sampledLockedReadCycles+=AdjustedCpuCycleDelta(
|
||||||
|
lockedReadStart,ReadCpuCycles());
|
||||||
|
++metrics->sampledLockedReads;
|
||||||
|
}
|
||||||
|
if (lockedHit)
|
||||||
|
{
|
||||||
|
if (metrics) ++metrics->hits;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::uint64_t buildStart=sampleCycles?ReadCpuCycles():0;
|
||||||
value = std::forward<TFactory>(factory)();
|
value = std::forward<TFactory>(factory)();
|
||||||
|
if(sampleCycles)
|
||||||
|
{
|
||||||
|
metrics->sampledBuildCycles+=AdjustedCpuCycleDelta(buildStart,ReadCpuCycles());
|
||||||
|
++metrics->sampledBuilds;
|
||||||
|
}
|
||||||
computed = true;
|
computed = true;
|
||||||
|
if (metrics) ++metrics->computes;
|
||||||
FValueWords valueWords{};
|
FValueWords valueWords{};
|
||||||
std::memcpy(valueWords.data(), &value, sizeof(TValue));
|
std::memcpy(valueWords.data(), &value, sizeof(TValue));
|
||||||
|
|
||||||
@@ -118,6 +187,7 @@ namespace VoxelForgeCoreShared
|
|||||||
{ replacement = way; break; }
|
{ replacement = way; break; }
|
||||||
if (replacement == Ways)
|
if (replacement == Ways)
|
||||||
{
|
{
|
||||||
|
if (metrics) ++metrics->conflictReplacements;
|
||||||
replacement = set.NextWay;
|
replacement = set.NextWay;
|
||||||
set.NextWay = static_cast<std::uint8_t>((set.NextWay + 1) % Ways);
|
set.NextWay = static_cast<std::uint8_t>((set.NextWay + 1) % Ways);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "VoxelForgeCore/Linux77Profiler.h"
|
||||||
|
|
||||||
// UBT supplies this export/import macro for the runtime module. The standalone target has no
|
// UBT supplies this export/import macro for the runtime module. The standalone target has no
|
||||||
// module boundary, so leave it empty there. UBT fournit la macro; le viewer autonome reste neutre.
|
// module boundary, so leave it empty there. UBT fournit la macro; le viewer autonome reste neutre.
|
||||||
#ifndef VOXELFORGECORE_API
|
#ifndef VOXELFORGECORE_API
|
||||||
@@ -81,6 +83,9 @@ namespace VoxelForgeCore
|
|||||||
Vec3d normal;
|
Vec3d normal;
|
||||||
Vec2f uv;
|
Vec2f uv;
|
||||||
ColorRgba8 color;
|
ColorRgba8 color;
|
||||||
|
std::uint16_t hostMaterialId = 1;
|
||||||
|
std::array<std::uint16_t, 2> resourceMaterialIds{};
|
||||||
|
std::array<std::uint16_t, 2> resourceWeightsQ16{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MeshGroups
|
struct MeshGroups
|
||||||
@@ -128,14 +133,74 @@ namespace VoxelForgeCore
|
|||||||
std::uint64_t mixedBoxes = 0;
|
std::uint64_t mixedBoxes = 0;
|
||||||
std::uint64_t unknownBoxes = 0;
|
std::uint64_t unknownBoxes = 0;
|
||||||
std::uint64_t fieldCalls = 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 resolvedBoundaryBandQueries = 0;
|
||||||
|
std::uint64_t passageComposerQueries = 0;
|
||||||
|
std::uint64_t passageIterations = 0;
|
||||||
|
std::uint64_t passageTileBoundChecks = 0;
|
||||||
|
std::uint64_t passageTileBoundRejects = 0;
|
||||||
|
std::uint64_t passageTileBoundSkippedIterations = 0;
|
||||||
|
std::uint64_t passageUpperMouthFilterChecks = 0;
|
||||||
|
std::uint64_t passageColumnCandidates = 0;
|
||||||
|
std::uint64_t passageColumnDistanceCalls = 0;
|
||||||
|
std::uint64_t passageNegativeBoundChecks = 0;
|
||||||
|
std::uint64_t passageNegativeBoundRejects = 0;
|
||||||
|
std::uint64_t passagePositiveBoundChecks = 0;
|
||||||
|
std::uint64_t passagePositiveBoundRejects = 0;
|
||||||
|
std::uint64_t passageCarveSegmentVisits = 0;
|
||||||
|
std::uint64_t passageSmoothCarveJoins = 0;
|
||||||
|
std::uint64_t passageSupportSegmentVisits = 0;
|
||||||
|
std::uint64_t passageSmoothSupportJoins = 0;
|
||||||
|
std::uint64_t passageSupportCapsuleChecks = 0;
|
||||||
|
std::uint64_t passageLaterBoreChecks = 0;
|
||||||
|
std::uint64_t passageOtherCapsuleChecks = 0;
|
||||||
|
std::uint64_t passageMouthSupportCalls = 0;
|
||||||
|
std::uint64_t passageMouthRouteSegmentChecks = 0;
|
||||||
|
std::uint64_t passageReliefFieldCalls = 0;
|
||||||
|
std::uint64_t passageSmoothstepEvaluations = 0;
|
||||||
std::uint64_t floatFieldCalls = 0;
|
std::uint64_t floatFieldCalls = 0;
|
||||||
std::uint64_t latticeFieldCalls = 0;
|
std::uint64_t latticeFieldCalls = 0;
|
||||||
std::uint64_t unclampedFieldCalls = 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 legacyFieldEvalCalls = 0;
|
||||||
|
std::array<std::uint64_t,9> legacyFieldTermCalls{};
|
||||||
|
std::array<std::uint64_t,9> legacyFieldTermNanoseconds{};
|
||||||
|
std::array<std::uint64_t,9> legacyFieldTermTimedCalls{};
|
||||||
|
// Linux77-only mutually-exclusive route counts, flattened as
|
||||||
|
// (stage * route_count + route) * 2 + {calls,points}.
|
||||||
|
std::array<std::uint64_t,
|
||||||
|
VoxelForgeCore::Linux77Profiler::StageCount
|
||||||
|
*VoxelForgeCore::Linux77Profiler::RouteCount*2> linux77DispatchCounters{};
|
||||||
|
std::uint64_t linux77GridInteriorSamples = 0;
|
||||||
|
std::uint64_t linux77GridHaloSamples = 0;
|
||||||
|
std::uint64_t legacyPrimitiveCandidateVisits = 0;
|
||||||
|
std::uint64_t legacyPrimitiveDistanceEvaluations = 0;
|
||||||
|
std::uint64_t legacySmoothMinCalls = 0;
|
||||||
|
std::uint64_t legacySmoothMinNanoseconds = 0;
|
||||||
|
std::uint64_t legacySmoothMinTimedCalls = 0;
|
||||||
std::uint64_t columnCacheHits = 0;
|
std::uint64_t columnCacheHits = 0;
|
||||||
std::uint64_t columnCacheMisses = 0;
|
std::uint64_t columnCacheMisses = 0;
|
||||||
std::uint64_t columnCacheConflictMisses = 0;
|
std::uint64_t columnCacheConflictMisses = 0;
|
||||||
std::uint64_t sharedColumnCacheHits = 0;
|
std::uint64_t sharedColumnCacheHits = 0;
|
||||||
std::uint64_t columnEvaluations = 0;
|
std::uint64_t columnEvaluations = 0;
|
||||||
|
std::uint64_t islandLocalCacheHits = 0;
|
||||||
|
std::uint64_t islandLocalCacheMisses = 0;
|
||||||
|
std::uint64_t islandLocalCacheConflictReplacements = 0;
|
||||||
|
std::uint64_t islandSharedCacheHits = 0;
|
||||||
|
std::uint64_t islandSharedCacheMisses = 0;
|
||||||
|
std::uint64_t islandSharedCacheComputes = 0;
|
||||||
|
std::uint64_t islandSharedCacheConflictReplacements = 0;
|
||||||
|
std::uint64_t islandSharedCacheLockWaitNanoseconds = 0;
|
||||||
std::uint64_t uniformBlockProofsChecked = 0;
|
std::uint64_t uniformBlockProofsChecked = 0;
|
||||||
std::uint64_t uniformBlockProofSamplesChecked = 0;
|
std::uint64_t uniformBlockProofSamplesChecked = 0;
|
||||||
std::uint64_t uniformBlockProofFallbackBlocks = 0;
|
std::uint64_t uniformBlockProofFallbackBlocks = 0;
|
||||||
@@ -150,6 +215,7 @@ namespace VoxelForgeCore
|
|||||||
double classificationExclusiveMilliseconds = 0.0;
|
double classificationExclusiveMilliseconds = 0.0;
|
||||||
double marchingCubesExclusiveMilliseconds = 0.0;
|
double marchingCubesExclusiveMilliseconds = 0.0;
|
||||||
double marchingCubesDensityGridMilliseconds = 0.0;
|
double marchingCubesDensityGridMilliseconds = 0.0;
|
||||||
|
std::uint32_t marchingCubesGridFillWorkers = 1;
|
||||||
double marchingCubesCellClassificationMilliseconds = 0.0;
|
double marchingCubesCellClassificationMilliseconds = 0.0;
|
||||||
double marchingCubesGradientNormalsMilliseconds = 0.0;
|
double marchingCubesGradientNormalsMilliseconds = 0.0;
|
||||||
double marchingCubesEdgeInterpolationMilliseconds = 0.0;
|
double marchingCubesEdgeInterpolationMilliseconds = 0.0;
|
||||||
@@ -157,12 +223,30 @@ namespace VoxelForgeCore
|
|||||||
double marchingCubesSkirtMilliseconds = 0.0;
|
double marchingCubesSkirtMilliseconds = 0.0;
|
||||||
double marchingCubesOtherMilliseconds = 0.0;
|
double marchingCubesOtherMilliseconds = 0.0;
|
||||||
std::uint64_t marchingCubesGradientFallbackDensityCalls = 0;
|
std::uint64_t marchingCubesGradientFallbackDensityCalls = 0;
|
||||||
|
std::uint64_t marchingCubesGradientCalls = 0;
|
||||||
std::uint64_t marchingCubesEdgeMidpointDensityCalls = 0;
|
std::uint64_t marchingCubesEdgeMidpointDensityCalls = 0;
|
||||||
|
std::uint64_t marchingCubesEdgeMidpointDensityMemoHits = 0;
|
||||||
std::uint64_t marchingCubesGradientMemoHits = 0;
|
std::uint64_t marchingCubesGradientMemoHits = 0;
|
||||||
|
std::uint64_t marchingCubesX8DensityCalls = 0;
|
||||||
|
std::uint64_t marchingCubesX8ActiveLanes = 0;
|
||||||
|
std::uint64_t marchingCubesCandidateCells = 0;
|
||||||
|
std::uint64_t marchingCubesZBandRejectedCells = 0;
|
||||||
|
std::uint64_t marchingCubesCellClassificationCalls = 0;
|
||||||
|
std::uint64_t marchingCubesSurfaceCellCalls = 0;
|
||||||
|
std::uint64_t marchingCubesProofSkippedSolidCells = 0;
|
||||||
|
std::uint64_t marchingCubesProofSkippedAirCells = 0;
|
||||||
|
std::uint64_t marchingCubesProofSkippedDensitySamples = 0;
|
||||||
|
std::uint64_t marchingCubesSkirtEdgeCandidates = 0;
|
||||||
|
std::uint64_t marchingCubesSkirtBoundaryCandidates = 0;
|
||||||
|
std::uint64_t marchingCubesSkirtDensityProbes = 0;
|
||||||
double groundRefinementExclusiveMilliseconds = 0.0;
|
double groundRefinementExclusiveMilliseconds = 0.0;
|
||||||
double capRefinementExclusiveMilliseconds = 0.0;
|
double capRefinementExclusiveMilliseconds = 0.0;
|
||||||
double meshAssemblyMilliseconds = 0.0;
|
double meshAssemblyMilliseconds = 0.0;
|
||||||
double meshingMilliseconds = 0.0;
|
double meshingMilliseconds = 0.0;
|
||||||
|
double materialQueryMilliseconds = 0.0;
|
||||||
|
std::uint64_t materialQueries = 0;
|
||||||
|
std::uint64_t materialCandidateChecks = 0;
|
||||||
|
bool materialTileOutsideEveryVein = true;
|
||||||
double featureListMilliseconds = 0.0;
|
double featureListMilliseconds = 0.0;
|
||||||
double farShellMilliseconds = 0.0;
|
double farShellMilliseconds = 0.0;
|
||||||
double farStratumShellMilliseconds = 0.0;
|
double farStratumShellMilliseconds = 0.0;
|
||||||
@@ -191,6 +275,9 @@ namespace VoxelForgeCore
|
|||||||
std::uint32_t farHorizonVersion = 0;
|
std::uint32_t farHorizonVersion = 0;
|
||||||
bool farShellRepairSkipped = false;
|
bool farShellRepairSkipped = false;
|
||||||
bool tileWasClassified = false;
|
bool tileWasClassified = false;
|
||||||
|
std::uint8_t tileProofVetoReason = 0;
|
||||||
|
bool tileProofSafe = false;
|
||||||
|
bool tileLegacyBlockProofRelevant = false;
|
||||||
bool meshingSkipped = false;
|
bool meshingSkipped = false;
|
||||||
bool featureRefinedResolution = false;
|
bool featureRefinedResolution = false;
|
||||||
bool heightfieldBandLimited = false;
|
bool heightfieldBandLimited = false;
|
||||||
@@ -216,7 +303,9 @@ namespace VoxelForgeCore
|
|||||||
std::uint64_t densitySampleCount = 0;
|
std::uint64_t densitySampleCount = 0;
|
||||||
std::uint64_t meshPayloadAllocatedBytes = 0;
|
std::uint64_t meshPayloadAllocatedBytes = 0;
|
||||||
std::uint64_t densityHash = 0, meshHash = 0, payloadHash = 0, topologyHash = 0;
|
std::uint64_t densityHash = 0, meshHash = 0, payloadHash = 0, topologyHash = 0;
|
||||||
bool hasDensityHash = false, hasMeshHash = false, hasPayloadHash = false, hasTopologyHash = false;
|
std::uint64_t materialHash = 0;
|
||||||
|
bool hasDensityHash = false, hasMeshHash = false, hasPayloadHash = false,
|
||||||
|
hasTopologyHash = false, hasMaterialHash = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct WorldConfiguration
|
struct WorldConfiguration
|
||||||
@@ -369,7 +458,10 @@ namespace VoxelForgeCore
|
|||||||
// version 169 sank every connector 2 m; it left four FI route support probes low.
|
// 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 170 sinks only connectors touching the anchored root by 1 m.
|
||||||
// Version 208 keeps the crater tread scoped to style-zero FI mouths.
|
// Version 208 keeps the crater tread scoped to style-zero FI mouths.
|
||||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 208;
|
// 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 = 214;
|
||||||
|
|
||||||
enum class FieldChannel : std::uint32_t
|
enum class FieldChannel : std::uint32_t
|
||||||
{
|
{
|
||||||
@@ -416,6 +508,59 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t capRidgeWavelengthSubvoxels = 256 * 64;
|
std::int64_t capRidgeWavelengthSubvoxels = 256 * 64;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class WorldAssemblySurfaceProfileKindV1 : std::uint32_t
|
||||||
|
{
|
||||||
|
natural = 0,
|
||||||
|
periodicHills = 1,
|
||||||
|
mirroredRelief = 2,
|
||||||
|
flatField = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
// Version zero is the historical noise terrain and is intentionally omitted from
|
||||||
|
// canonical JSON and hashes. Version one uses metre-derived fixed-scale dimensions;
|
||||||
|
// phases are Q0.32 turns generated from the recipe seed during plan construction.
|
||||||
|
struct WorldAssemblySurfaceProfileV1
|
||||||
|
{
|
||||||
|
std::uint32_t version = 0;
|
||||||
|
WorldAssemblySurfaceProfileKindV1 kind = WorldAssemblySurfaceProfileKindV1::natural;
|
||||||
|
std::int64_t h0Subvoxels = 0;
|
||||||
|
std::int64_t amplitudeXSubvoxels = 0;
|
||||||
|
std::int64_t amplitudeYSubvoxels = 0;
|
||||||
|
std::int64_t wavelengthXSubvoxels = 0;
|
||||||
|
std::int64_t wavelengthYSubvoxels = 0;
|
||||||
|
std::uint32_t phaseXTurnsQ32 = 0;
|
||||||
|
std::uint32_t phaseYTurnsQ32 = 0;
|
||||||
|
std::int64_t mirrorLambdaQ16 = 0;
|
||||||
|
std::int64_t minimumArenaClearanceSubvoxels = 0;
|
||||||
|
|
||||||
|
friend bool operator==(const WorldAssemblySurfaceProfileV1&,
|
||||||
|
const WorldAssemblySurfaceProfileV1&) = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::uint32_t WorldAssemblySurfaceProfileSeededPhaseQ32(
|
||||||
|
std::uint32_t seed, std::uint32_t axis) noexcept
|
||||||
|
{
|
||||||
|
std::uint64_t value = static_cast<std::uint64_t>(seed)
|
||||||
|
^ (axis == 0 ? 0x706572696f645831ull : 0x706572696f645931ull);
|
||||||
|
value += 0x9E3779B97F4A7C15ull;
|
||||||
|
value = (value ^ (value >> 30)) * 0xBF58476D1CE4E5B9ull;
|
||||||
|
value = (value ^ (value >> 27)) * 0x94D049BB133111EBull;
|
||||||
|
value ^= value >> 31;
|
||||||
|
return static_cast<std::uint32_t>(value >> 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void ResolveWorldAssemblySurfaceProfilePhasesV1(
|
||||||
|
WorldAssemblySurfaceProfileV1& profile, std::uint32_t seed) noexcept
|
||||||
|
{
|
||||||
|
if (profile.version == 1
|
||||||
|
&& (profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills
|
||||||
|
|| profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief))
|
||||||
|
{
|
||||||
|
profile.phaseXTurnsQ32 = WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 0);
|
||||||
|
profile.phaseYTurnsQ32 = WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct WorldAssemblyRecipeV1
|
struct WorldAssemblyRecipeV1
|
||||||
{
|
{
|
||||||
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
||||||
@@ -433,6 +578,7 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t biomeClimateReliefWavelengthSubvoxels = 256 * 3333;
|
std::int64_t biomeClimateReliefWavelengthSubvoxels = 256 * 3333;
|
||||||
std::int64_t biomeClimateMoistureWavelengthSubvoxels = 256 * 2500;
|
std::int64_t biomeClimateMoistureWavelengthSubvoxels = 256 * 2500;
|
||||||
std::int64_t biomeClimateReliefContrastQ16 = 2ll << 16;
|
std::int64_t biomeClimateReliefContrastQ16 = 2ll << 16;
|
||||||
|
WorldAssemblySurfaceProfileV1 surfaceProfile;
|
||||||
std::vector<WorldAssemblyBiomeProfileV1> biomes;
|
std::vector<WorldAssemblyBiomeProfileV1> biomes;
|
||||||
std::uint64_t contentHash = 0;
|
std::uint64_t contentHash = 0;
|
||||||
};
|
};
|
||||||
@@ -492,6 +638,7 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t capRidgeWavelengthSubvoxels = 256 * 64;
|
std::int64_t capRidgeWavelengthSubvoxels = 256 * 64;
|
||||||
std::int64_t waterSurfaceSubvoxels = 0;
|
std::int64_t waterSurfaceSubvoxels = 0;
|
||||||
bool waterEnabled = false;
|
bool waterEnabled = false;
|
||||||
|
WorldAssemblySurfaceProfileV1 surfaceProfile;
|
||||||
std::vector<WorldAssemblyFeatureV1> features;
|
std::vector<WorldAssemblyFeatureV1> features;
|
||||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||||
std::vector<WorldAssemblyRoutePointV1> route;
|
std::vector<WorldAssemblyRoutePointV1> route;
|
||||||
@@ -705,8 +852,139 @@ namespace VoxelForgeCore
|
|||||||
// Core adapter. Coordinates are half-open micrometre intervals; recipe anchors retain their
|
// Core adapter. Coordinates are half-open micrometre intervals; recipe anchors retain their
|
||||||
// source fixed-point units so this record never rounds the recipe itself.
|
// source fixed-point units so this record never rounds the recipe itself.
|
||||||
inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV3 = 3;
|
inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV3 = 3;
|
||||||
|
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1;
|
||||||
|
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||||
|
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3;
|
||||||
|
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||||
|
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
||||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||||
|
|
||||||
|
// Stable, bounded geometry edits composed into every Core field after the shared
|
||||||
|
// strate boundaries and passages. Coordinates and extents use integer micrometres.
|
||||||
|
enum class ResolvedSdfStampKindV1 : std::uint8_t
|
||||||
|
{
|
||||||
|
carveEllipsoid = 1,
|
||||||
|
carveArch = 2,
|
||||||
|
fillPillar = 3,
|
||||||
|
fillDais = 4,
|
||||||
|
fillMonolith = 5,
|
||||||
|
fillPlatform = 6
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResolvedLayoutStampRequestV1
|
||||||
|
{
|
||||||
|
std::string stableId;
|
||||||
|
std::int32_t strateIndex = -1;
|
||||||
|
ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid;
|
||||||
|
// A preferred center and a bounded deterministic search radius.
|
||||||
|
std::array<std::int64_t, 3> anchorPositionMicrometres{};
|
||||||
|
std::int64_t placementRadiusMicrometres = 0;
|
||||||
|
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||||
|
std::int64_t smoothWidthMicrometres = 0;
|
||||||
|
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||||
|
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||||
|
std::int64_t roundingRadiusMicrometres = 0;
|
||||||
|
std::int64_t minimumSpacingMicrometres = 0;
|
||||||
|
// 0 = free placement, 1 = seated on the authored flat profile floor.
|
||||||
|
std::uint8_t attachmentPolicy = 0;
|
||||||
|
std::int64_t floorMicrometres = 0;
|
||||||
|
std::uint8_t yawQuarterTurns = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResolvedLayoutStampV1
|
||||||
|
{
|
||||||
|
std::string stableId;
|
||||||
|
std::int32_t strateIndex = -1;
|
||||||
|
ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid;
|
||||||
|
std::array<std::int64_t, 3> positionMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||||
|
std::int64_t smoothWidthMicrometres = 0;
|
||||||
|
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||||
|
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||||
|
std::int64_t roundingRadiusMicrometres = 0;
|
||||||
|
std::int64_t minimumSpacingMicrometres = 0;
|
||||||
|
std::uint8_t attachmentPolicy = 0;
|
||||||
|
std::int64_t floorMicrometres = 0;
|
||||||
|
std::uint64_t irregularitySeed = 0;
|
||||||
|
std::uint8_t yawQuarterTurns = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional room and swept-edge records for a resolved cavern graph. Geometry is stored
|
||||||
|
// in integer micrometres; bounds include smooth blending, relief, and floor support.
|
||||||
|
struct ResolvedCavernRoomV1
|
||||||
|
{
|
||||||
|
std::string stableId;
|
||||||
|
std::int32_t strateIndex = -1;
|
||||||
|
std::array<std::int64_t, 3> centerMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||||
|
std::int64_t floorMicrometres = 0;
|
||||||
|
std::int64_t smoothWidthMicrometres = 0;
|
||||||
|
// Optional feature-to-feature union width. Zero preserves the historical
|
||||||
|
// behavior where smoothWidthMicrometres also controls the graph union.
|
||||||
|
std::int64_t unionWidthMicrometres = 0;
|
||||||
|
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||||
|
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||||
|
std::uint64_t irregularitySeed = 0;
|
||||||
|
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResolvedCavernEdgeV1
|
||||||
|
{
|
||||||
|
std::string stableId;
|
||||||
|
std::string startAnchorStableId;
|
||||||
|
std::string endRoomStableId;
|
||||||
|
std::int32_t strateIndex = -1;
|
||||||
|
// A single cubic Bezier in XY. Its four Z values are the cubic floor profile.
|
||||||
|
std::array<std::array<std::int64_t, 3>, 4> controlPointsMicrometres{};
|
||||||
|
// Resolve-time, error-bounded approximation of the cubic centerline. Each
|
||||||
|
// consecutive pair is a physical segment used by the native cave field.
|
||||||
|
std::vector<std::array<std::int64_t, 3>> polylineMicrometres;
|
||||||
|
std::int64_t boreRadiusMicrometres = 0;
|
||||||
|
std::int64_t boreHeightMicrometres = 0;
|
||||||
|
std::int64_t floorSupportDepthMicrometres = 0;
|
||||||
|
std::int64_t smoothWidthMicrometres = 0;
|
||||||
|
// Optional feature-to-feature union width; floor fillets still use smoothWidth.
|
||||||
|
std::int64_t unionWidthMicrometres = 0;
|
||||||
|
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||||
|
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||||
|
std::uint64_t irregularitySeed = 0;
|
||||||
|
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Immutable material-only deposits. The capsule and its conservative bounds are in
|
||||||
|
// micrometres; bounds include irregularity and the blend envelope.
|
||||||
|
struct ResolvedLayoutOreVeinRequestV1
|
||||||
|
{
|
||||||
|
std::string stableId;
|
||||||
|
std::int32_t strateIndex = -1;
|
||||||
|
std::uint16_t resourceMaterialId = 0;
|
||||||
|
std::array<std::array<std::int64_t, 3>, 2> anchorEndpointsMicrometres{};
|
||||||
|
std::int64_t placementRadiusMicrometres = 0;
|
||||||
|
std::int64_t radiusMicrometres = 0;
|
||||||
|
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||||
|
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||||
|
std::int64_t blendWidthMicrometres = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResolvedLayoutOreVeinV1
|
||||||
|
{
|
||||||
|
std::string stableId;
|
||||||
|
std::int32_t strateIndex = -1;
|
||||||
|
std::uint16_t resourceMaterialId = 0;
|
||||||
|
std::array<std::array<std::int64_t, 3>, 2> endpointsMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||||
|
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||||
|
std::int64_t radiusMicrometres = 0;
|
||||||
|
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||||
|
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||||
|
std::int64_t blendWidthMicrometres = 0;
|
||||||
|
std::uint64_t irregularitySeed = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct ResolvedLayoutAnchorV2
|
struct ResolvedLayoutAnchorV2
|
||||||
{
|
{
|
||||||
std::string kind;
|
std::string kind;
|
||||||
@@ -729,6 +1007,24 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t solidDensityQ16 = 1ll << 16;
|
std::int64_t solidDensityQ16 = 1ll << 16;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Runtime policy resolved once from the director's immutable strate/interface
|
||||||
|
// description. These derived capabilities are deliberately omitted from canonical
|
||||||
|
// JSON: their source fields are already hashed and the records preserve that identity.
|
||||||
|
struct ResolvedLayoutStratePassageCapabilitiesV1
|
||||||
|
{
|
||||||
|
bool hasSurfaceLanding = false;
|
||||||
|
bool hasFloatingIslandLanding = false;
|
||||||
|
bool usePassageAnchorForColumns = false;
|
||||||
|
bool hasNativeCaveField = false;
|
||||||
|
bool hasNativeLatticeField = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResolvedLayoutInterfaceCapabilitiesV1
|
||||||
|
{
|
||||||
|
bool preserveUpperBedrockAirGap = false;
|
||||||
|
bool surfaceToTunnelLandingSupport = false;
|
||||||
|
};
|
||||||
|
|
||||||
struct ResolvedLayoutStratumV2
|
struct ResolvedLayoutStratumV2
|
||||||
{
|
{
|
||||||
std::string stableId;
|
std::string stableId;
|
||||||
@@ -758,8 +1054,29 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t managerChunkWidthMicrometres = 0;
|
std::int64_t managerChunkWidthMicrometres = 0;
|
||||||
std::int64_t dispatchChunkWidthMicrometres = 0;
|
std::int64_t dispatchChunkWidthMicrometres = 0;
|
||||||
std::vector<ResolvedLayoutAnchorV2> anchors;
|
std::vector<ResolvedLayoutAnchorV2> anchors;
|
||||||
|
ResolvedLayoutStratePassageCapabilitiesV1 passageCapabilities;
|
||||||
|
ResolvedLayoutInterfaceCapabilitiesV1 lowerInterfaceCapabilities;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inline bool IsNativeCaveFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
|
||||||
|
{
|
||||||
|
return stratum.generatorKind=="Caves"
|
||||||
|
&&stratum.recipeId=="Core.Caves.PrimitiveField"
|
||||||
|
&&stratum.recipeVersion==1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool IsNativeLatticeFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
|
||||||
|
{
|
||||||
|
return (stratum.generatorKind=="Maze"||stratum.generatorKind=="VerticalShafts")
|
||||||
|
&&stratum.recipeId=="Core.Lattice.PrimitiveField"
|
||||||
|
&&stratum.recipeVersion==1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool HasNativeGraphField(const ResolvedLayoutStratumV2& stratum) noexcept
|
||||||
|
{
|
||||||
|
return IsNativeCaveFieldStratum(stratum)||IsNativeLatticeFieldStratum(stratum);
|
||||||
|
}
|
||||||
|
|
||||||
/** Per-column resolved bands for one adjacent bedrock/ceiling interface.
|
/** Per-column resolved bands for one adjacent bedrock/ceiling interface.
|
||||||
* crossingDepthMicrometres is the actual overlap depth between the two facing bands.
|
* crossingDepthMicrometres is the actual overlap depth between the two facing bands.
|
||||||
*/
|
*/
|
||||||
@@ -854,6 +1171,16 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t strateLayoutChunkWidthSubvoxels = 8192;
|
std::int64_t strateLayoutChunkWidthSubvoxels = 8192;
|
||||||
std::vector<ResolvedLayoutStratumV2> strates;
|
std::vector<ResolvedLayoutStratumV2> strates;
|
||||||
std::vector<ResolvedLayoutPassageV1> passages;
|
std::vector<ResolvedLayoutPassageV1> passages;
|
||||||
|
// Kept absent from canonical layout identity when no feature requests stamps.
|
||||||
|
std::uint32_t stampVocabularyVersion = 0;
|
||||||
|
std::vector<ResolvedLayoutStampV1> stamps;
|
||||||
|
// Kept absent from canonical identity when there are no graph records.
|
||||||
|
std::uint32_t cavernGraphVocabularyVersion = 0;
|
||||||
|
std::vector<ResolvedCavernRoomV1> cavernRooms;
|
||||||
|
std::vector<ResolvedCavernEdgeV1> cavernEdges;
|
||||||
|
// Omitted from canonical identity when no records are present.
|
||||||
|
std::uint32_t oreVeinVocabularyVersion = 0;
|
||||||
|
std::vector<ResolvedLayoutOreVeinV1> oreVeins;
|
||||||
std::string stableIdentity;
|
std::string stableIdentity;
|
||||||
|
|
||||||
bool finalize(std::string& error);
|
bool finalize(std::string& error);
|
||||||
@@ -889,6 +1216,68 @@ namespace VoxelForgeCore
|
|||||||
const std::vector<ResolvedLayoutPassageConfigV1>& configs,
|
const std::vector<ResolvedLayoutPassageConfigV1>& configs,
|
||||||
std::string& error);
|
std::string& error);
|
||||||
|
|
||||||
|
struct ResolvedLayoutStampPlacementAuditV1
|
||||||
|
{
|
||||||
|
std::uint32_t negativeControlsRun = 0;
|
||||||
|
std::uint32_t negativeControlsRejected = 0;
|
||||||
|
bool passed = false;
|
||||||
|
};
|
||||||
|
VFCORE_API bool ResolveResolvedLayoutStampsV1(
|
||||||
|
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||||
|
std::span<const ResolvedLayoutStampRequestV1> requests,
|
||||||
|
std::string& error);
|
||||||
|
VFCORE_API bool ResolveResolvedLayoutStampsV2(
|
||||||
|
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||||
|
std::span<const ResolvedLayoutStampRequestV1> requests,
|
||||||
|
std::string& error);
|
||||||
|
// Vocabulary 3 adds explicit rounded capped-cylinder monoliths and rounded-box platforms.
|
||||||
|
VFCORE_API bool ResolveResolvedLayoutStampsV3(
|
||||||
|
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||||
|
std::span<const ResolvedLayoutStampRequestV1> requests,
|
||||||
|
std::string& error);
|
||||||
|
VFCORE_API ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
|
||||||
|
const ResolvedLayoutSnapshotV3& layout);
|
||||||
|
|
||||||
|
VFCORE_API bool ResolveResolvedCavernGraphV1(
|
||||||
|
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||||
|
std::span<const ResolvedCavernRoomV1> rooms,
|
||||||
|
std::span<const ResolvedCavernEdgeV1> edges,
|
||||||
|
std::string& error);
|
||||||
|
VFCORE_API bool ResolveResolvedLayoutOreVeinsV1(
|
||||||
|
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||||
|
std::span<const ResolvedLayoutOreVeinRequestV1> requests,
|
||||||
|
std::string& error);
|
||||||
|
|
||||||
|
struct ResolvedLayoutOreVeinPlacementAuditV1
|
||||||
|
{
|
||||||
|
std::uint32_t negativeControlsRun = 0;
|
||||||
|
std::uint32_t negativeControlsRejected = 0;
|
||||||
|
bool passed = false;
|
||||||
|
};
|
||||||
|
VFCORE_API ResolvedLayoutOreVeinPlacementAuditV1 AuditResolvedLayoutOreVeinPlacementV1(
|
||||||
|
const ResolvedLayoutSnapshotV3& layout);
|
||||||
|
|
||||||
|
struct MaterialSampleV1
|
||||||
|
{
|
||||||
|
std::uint16_t hostMaterialId = 1;
|
||||||
|
std::array<std::uint16_t, 2> resourceMaterialIds{};
|
||||||
|
std::array<std::uint16_t, 2> resourceWeightsQ16{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResourceVolumeV1
|
||||||
|
{
|
||||||
|
std::uint16_t resourceMaterialId = 0;
|
||||||
|
std::uint64_t weightedSamplesQ16 = 0;
|
||||||
|
double volumeCubicMetres = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OreMiningYieldV1
|
||||||
|
{
|
||||||
|
std::uint64_t solidSamplesRemoved = 0;
|
||||||
|
double sampleSpacingMetres = 0.0;
|
||||||
|
std::vector<ResourceVolumeV1> resources;
|
||||||
|
};
|
||||||
|
|
||||||
struct FloatingIslandBoundsAuditResult
|
struct FloatingIslandBoundsAuditResult
|
||||||
{
|
{
|
||||||
std::uint64_t boxesTested = 0;
|
std::uint64_t boxesTested = 0;
|
||||||
@@ -1075,6 +1464,7 @@ namespace VoxelForgeCore
|
|||||||
static void setThreadColumnInterpolationStrideSubvoxels(std::int64_t stride) noexcept;
|
static void setThreadColumnInterpolationStrideSubvoxels(std::int64_t stride) noexcept;
|
||||||
static std::int64_t threadColumnInterpolationStrideSubvoxels() noexcept;
|
static std::int64_t threadColumnInterpolationStrideSubvoxels() noexcept;
|
||||||
static void setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept;
|
static void setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept;
|
||||||
|
static bool threadColumnCacheLegacyDirectMapped() noexcept;
|
||||||
static const HeightfieldColumnCacheStats& threadColumnCacheStats() noexcept;
|
static const HeightfieldColumnCacheStats& threadColumnCacheStats() noexcept;
|
||||||
FieldInterval interval(std::int64_t minimumXSubvoxels,
|
FieldInterval interval(std::int64_t minimumXSubvoxels,
|
||||||
std::int64_t minimumYSubvoxels,
|
std::int64_t minimumYSubvoxels,
|
||||||
@@ -1167,6 +1557,7 @@ namespace VoxelForgeCore
|
|||||||
const WorldConfiguration& configuration() const;
|
const WorldConfiguration& configuration() const;
|
||||||
const WorldAssemblyManifest* worldAssembly() const noexcept;
|
const WorldAssemblyManifest* worldAssembly() const noexcept;
|
||||||
const ResolvedLayoutSnapshotV3* resolvedLayout() const noexcept;
|
const ResolvedLayoutSnapshotV3* resolvedLayout() const noexcept;
|
||||||
|
ResolvedWorldSnapshot withResolvedLayout(const ResolvedLayoutSnapshotV3& layout) const;
|
||||||
ResolvedWorldSnapshot withoutResolvedLayoutPassages() const;
|
ResolvedWorldSnapshot withoutResolvedLayoutPassages() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -1210,6 +1601,7 @@ namespace VoxelForgeCore
|
|||||||
bool roundTunnels = false;
|
bool roundTunnels = false;
|
||||||
bool enhancedTunnelWalls = true;
|
bool enhancedTunnelWalls = true;
|
||||||
bool profileDetailBand = false;
|
bool profileDetailBand = false;
|
||||||
|
bool parallelSignEdgeRepair = false;
|
||||||
// World-wide walkability is a validation pass, not an input to the compiled field.
|
// World-wide walkability is a validation pass, not an input to the compiled field.
|
||||||
// Keep it enabled for benchmarks/audits; the interactive viewer can defer it.
|
// Keep it enabled for benchmarks/audits; the interactive viewer can defer it.
|
||||||
bool validatePassageRoutes = true;
|
bool validatePassageRoutes = true;
|
||||||
@@ -1394,6 +1786,9 @@ namespace VoxelForgeCore
|
|||||||
std::int32_t bandMinZVoxel = -2147483647 - 1;
|
std::int32_t bandMinZVoxel = -2147483647 - 1;
|
||||||
std::int32_t bandMaxZVoxel = 2147483647;
|
std::int32_t bandMaxZVoxel = 2147483647;
|
||||||
bool hashDensity = false;
|
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;
|
bool captureDensitySamples = false;
|
||||||
// Diagnostic only; materialize the full density lattice even when block proofs
|
// Diagnostic only; materialize the full density lattice even when block proofs
|
||||||
// establish that the tile has no surface.
|
// establish that the tile has no surface.
|
||||||
@@ -1412,6 +1807,24 @@ namespace VoxelForgeCore
|
|||||||
// Separate cold pass only; samples the MC inner stages without changing output.
|
// 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.
|
// Passe froide separee seulement; mesure les etapes internes MC sans changer la sortie.
|
||||||
bool profileMesherStages = false;
|
bool profileMesherStages = false;
|
||||||
|
// Research-only: count passage-composer work per generated tile.
|
||||||
|
bool profilePassageComposer = false;
|
||||||
|
// Research-only field comparison. A nonnegative value clamps the source density
|
||||||
|
// entering passage composition to at least -N voxel units for this tile.
|
||||||
|
// A negative value leaves the process-level diagnostic override in control.
|
||||||
|
double diagnosticPassageSourceFloorVoxels = -1.0;
|
||||||
|
// Research-only field comparison. Zero keeps the unbounded passage carve;
|
||||||
|
// a positive value bounds its signed-distance reach in metres. A negative
|
||||||
|
// value leaves the process-level L93 setting in control.
|
||||||
|
double diagnosticPassageCarveReachMetres = -1.0;
|
||||||
|
// 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;
|
||||||
|
// Linux headless measurement only. Compiled out of ordinary builds.
|
||||||
|
bool linux77Profile = false;
|
||||||
|
bool exactMidpointDensityMemo = false;
|
||||||
|
bool activeLaneBatching = false;
|
||||||
|
std::uint32_t proofBlockCells = 8;
|
||||||
// Diagnostic only; fills and checks every lattice sample in blocks certified uniform.
|
// Diagnostic only; fills and checks every lattice sample in blocks certified uniform.
|
||||||
// Diagnostic seulement; evalue et verifie tous les echantillons des blocs uniformes.
|
// Diagnostic seulement; evalue et verifie tous les echantillons des blocs uniformes.
|
||||||
bool auditUniformBlockProofs = false;
|
bool auditUniformBlockProofs = false;
|
||||||
@@ -1421,6 +1834,10 @@ namespace VoxelForgeCore
|
|||||||
// Diagnostic/performance control; memoizes exact MC lattice gradients at LOD0.
|
// Diagnostic/performance control; memoizes exact MC lattice gradients at LOD0.
|
||||||
// Controle de mesure/performance; memorise les gradients MC exacts au LOD0.
|
// Controle de mesure/performance; memorise les gradients MC exacts au LOD0.
|
||||||
bool memoizeMcGradients = true;
|
bool memoizeMcGradients = true;
|
||||||
|
// Optional host-owned executor for predicted-heavy density-grid fills.
|
||||||
|
// Hosts leave this null for ordinary tiles and set a small limit for heavy tiles.
|
||||||
|
const JobScheduler* gridFillScheduler = nullptr;
|
||||||
|
std::uint32_t gridFillWorkerLimit = 1;
|
||||||
// LOD8-only planar horizon interior grid; zero follows the MC step, 128 restores S9.
|
// LOD8-only planar horizon interior grid; zero follows the MC step, 128 restores S9.
|
||||||
// Grille interieure LOD8 seulement; zero suit la maille MC, 128 restaure S9.
|
// Grille interieure LOD8 seulement; zero suit la maille MC, 128 restaure S9.
|
||||||
std::int32_t lod8HorizonInteriorStepVoxels = 0;
|
std::int32_t lod8HorizonInteriorStepVoxels = 0;
|
||||||
@@ -1428,7 +1845,7 @@ namespace VoxelForgeCore
|
|||||||
// LOD4-8 mesh hashes are salted; density and LOD0-3 keep their direct identities.
|
// LOD4-8 mesh hashes are salted; density and LOD0-3 keep their direct identities.
|
||||||
// Incremente quand les plafonds partages ou l'horizon lointain changent.
|
// Incremente quand les plafonds partages ou l'horizon lointain changent.
|
||||||
// Les hashes de maillage LOD4-8 incluent cette version; LOD0-3 gardent leur identite directe.
|
// Les hashes de maillage LOD4-8 incluent cette version; LOD0-3 gardent leur identite directe.
|
||||||
std::uint32_t farHorizonVersion = 4;
|
std::uint32_t farHorizonVersion = 5;
|
||||||
// Linux research controls for reproducing L5 negative controls. Defaults preserve output.
|
// Linux research controls for reproducing L5 negative controls. Defaults preserve output.
|
||||||
std::uint32_t diagnosticGlobalOctaveDrop = 0;
|
std::uint32_t diagnosticGlobalOctaveDrop = 0;
|
||||||
std::uint32_t diagnosticCellStride = 1;
|
std::uint32_t diagnosticCellStride = 1;
|
||||||
@@ -1476,12 +1893,18 @@ namespace VoxelForgeCore
|
|||||||
bool analyticOnly = true,
|
bool analyticOnly = true,
|
||||||
bool diagnosticIncludeBaseCoveredTiles = false);
|
bool diagnosticIncludeBaseCoveredTiles = false);
|
||||||
void retainTileRegions(std::span<const TileKey> desiredTiles);
|
void retainTileRegions(std::span<const TileKey> desiredTiles);
|
||||||
void buildFarWorld();
|
bool buildFarWorld();
|
||||||
bool isCompiled() const noexcept;
|
bool isCompiled() const noexcept;
|
||||||
bool hasFarWorld() const noexcept;
|
bool hasFarWorld() const noexcept;
|
||||||
ShapeBuildStats buildStats() const;
|
ShapeBuildStats buildStats() const;
|
||||||
|
|
||||||
float densityAt(Vec3d voxelPosition) const;
|
float densityAt(Vec3d voxelPosition) const;
|
||||||
|
// Companion material query for the bound strate evaluator; coordinates use Core voxels.
|
||||||
|
MaterialSampleV1 materialAt(Vec3d voxelPosition) const;
|
||||||
|
// Measure-only carve estimate. This does not apply an edit or grant inventory.
|
||||||
|
OreMiningYieldV1 measureOreMiningYieldForSphere(
|
||||||
|
Vec3d centerVoxel, double radiusMetres,
|
||||||
|
double sampleSpacingMetres = 0.25) const;
|
||||||
// Audit-only view of density-field columns and short spans supporting passage voids.
|
// Audit-only view of density-field columns and short spans supporting passage voids.
|
||||||
std::vector<PassageVoidCrossingAuditV1> passageVoidCrossings() const;
|
std::vector<PassageVoidCrossingAuditV1> passageVoidCrossings() const;
|
||||||
bool isPassageColumnSample(std::size_t passageIndex, std::size_t segmentIndex,
|
bool isPassageColumnSample(std::size_t passageIndex, std::size_t segmentIndex,
|
||||||
@@ -1492,6 +1915,8 @@ namespace VoxelForgeCore
|
|||||||
std::string densityDispatchPathAt(Vec3d voxelPosition) const;
|
std::string densityDispatchPathAt(Vec3d voxelPosition) const;
|
||||||
// Analytic source distance without the occupancy-preserving near-zero clamp or packed proxy.
|
// Analytic source distance without the occupancy-preserving near-zero clamp or packed proxy.
|
||||||
float unclampedDensityAt(Vec3d voxelPosition) const;
|
float unclampedDensityAt(Vec3d voxelPosition) const;
|
||||||
|
// Research-only source field entering passage composition, with passage carving omitted.
|
||||||
|
float passageSourceDensityAt(Vec3d voxelPosition) const;
|
||||||
DetailFieldSample detailSampleAt(Vec3d voxelPosition) const;
|
DetailFieldSample detailSampleAt(Vec3d voxelPosition) const;
|
||||||
float densityAtLattice(Vec3i voxelPosition) const;
|
float densityAtLattice(Vec3i voxelPosition) const;
|
||||||
bool isSolidAt(Vec3d voxelPosition) const;
|
bool isSolidAt(Vec3d voxelPosition) const;
|
||||||
@@ -1501,6 +1926,9 @@ namespace VoxelForgeCore
|
|||||||
std::vector<TileKey> desiredTiles(Vec3d cameraPositionVoxels) const;
|
std::vector<TileKey> desiredTiles(Vec3d cameraPositionVoxels) const;
|
||||||
static std::vector<TileKey> prioritizeTiles(std::span<const TileKey> tiles,
|
static std::vector<TileKey> prioritizeTiles(std::span<const TileKey> tiles,
|
||||||
Vec3i centerChunk);
|
Vec3i centerChunk);
|
||||||
|
// Count expanded resolved-passage route bounds intersecting the full tile bounds.
|
||||||
|
// Hosts use this stable estimate to prioritize and split expensive near tiles.
|
||||||
|
std::uint32_t passageOverlapScore(const TileKey& key) const noexcept;
|
||||||
std::optional<TileMesh> generateTile(const TileKey& key,
|
std::optional<TileMesh> generateTile(const TileKey& key,
|
||||||
const TileGenerationOptions& options,
|
const TileGenerationOptions& options,
|
||||||
std::string& error) const;
|
std::string& error) const;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
#pragma once
|
#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
|
// 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
|
// 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.
|
// private namespace when the implementation includes its Unreal-shaped compatibility headers.
|
||||||
@@ -56,6 +60,7 @@
|
|||||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||||
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
||||||
#include "VoxelForgeCore/SharedColumnCache.h"
|
#include "VoxelForgeCore/SharedColumnCache.h"
|
||||||
|
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
|
||||||
|
|
||||||
// Keep Core's expression-level FMA behavior fixed regardless of the toolchain setting.
|
// Keep Core's expression-level FMA behavior fixed regardless of the toolchain setting.
|
||||||
// Clang's "fast" mode ignores the pragma, so Core's runtime self-test rejects that build.
|
// Clang's "fast" mode ignores the pragma, so Core's runtime self-test rejects that build.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
||||||
|
#include "VoxelForgeCore/CpuCycleCounter.h"
|
||||||
|
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
|
||||||
|
#include "VoxelForgeCore/SharedColumnCache.h"
|
||||||
#include "Json.h"
|
#include "Json.h"
|
||||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||||
|
|
||||||
@@ -471,13 +474,69 @@ namespace floating_island_detail
|
|||||||
double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0;
|
double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct FIslandColumnCacheInstrumentation
|
||||||
|
{
|
||||||
|
std::uint64_t localHits=0,localMisses=0,localConflictReplacements=0;
|
||||||
|
std::uint64_t localProbeCycles=0,localProbeSamples=0;
|
||||||
|
std::uint64_t tileStartCycles=0,tileCycles=0;
|
||||||
|
VoxelForgeCoreShared::FColumnCacheMetrics shared;
|
||||||
|
FIslandFieldAttributionInstrumentation attribution;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline FIslandColumnCacheInstrumentation*& ActiveIslandColumnCacheInstrumentation() noexcept
|
||||||
|
{
|
||||||
|
static thread_local FIslandColumnCacheInstrumentation* active=nullptr;
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FIslandColumnCacheInstrumentation* SetIslandColumnCacheInstrumentation(
|
||||||
|
FIslandColumnCacheInstrumentation* value) noexcept
|
||||||
|
{
|
||||||
|
FIslandColumnCacheInstrumentation* previous=ActiveIslandColumnCacheInstrumentation();
|
||||||
|
ActiveIslandColumnCacheInstrumentation()=value;
|
||||||
|
SetActiveIslandFieldAttribution(value?&value->attribution:nullptr);
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
using FSharedIslandColumnCache =
|
using FSharedIslandColumnCache =
|
||||||
VoxelForgeCoreShared::TSharedColumnValueCache<FIslandColumnCacheEntry,16384,4>;
|
VoxelForgeCoreShared::TSharedColumnValueCache<FIslandColumnCacheEntry,16384,4>;
|
||||||
|
|
||||||
|
struct FIslandColumnTraceKey
|
||||||
|
{
|
||||||
|
bool valid=false;
|
||||||
|
std::uint64_t recipeHash=0,planHash=0,island=0;
|
||||||
|
std::int64_t x=0,y=0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FIslandColumnReuseTrace
|
||||||
|
{
|
||||||
|
std::array<FIslandColumnTraceKey,128> recentMisses{};
|
||||||
|
std::array<FIslandColumnTraceKey,128> recentEvictions{};
|
||||||
|
std::uint64_t missSequence=0,printedEvents=0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool SameTraceKey(const FIslandColumnTraceKey& left,
|
||||||
|
const FIslandColumnTraceKey& right) noexcept
|
||||||
|
{
|
||||||
|
return left.valid&&right.valid&&left.recipeHash==right.recipeHash
|
||||||
|
&&left.planHash==right.planHash&&left.island==right.island
|
||||||
|
&&left.x==right.x&&left.y==right.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::size_t TraceKeyIndex(const FIslandColumnTraceKey& key) noexcept
|
||||||
|
{
|
||||||
|
std::uint64_t hash=key.island^static_cast<std::uint64_t>(key.x)*0x9e3779b97f4a7c15ull
|
||||||
|
^static_cast<std::uint64_t>(key.y)*0xc2b2ae3d27d4eb4full
|
||||||
|
^key.recipeHash^std::rotl(key.planHash,17);
|
||||||
|
hash^=hash>>33;hash*=0xff51afd7ed558ccdull;hash^=hash>>33;
|
||||||
|
return static_cast<std::size_t>(hash&127u);
|
||||||
|
}
|
||||||
|
|
||||||
inline const FIslandColumnCacheEntry& IslandColumnCache(
|
inline const FIslandColumnCacheEntry& IslandColumnCache(
|
||||||
const FloatingIslandRecipeV1& recipe,const FloatingIslandPlanV1& plan,
|
const FloatingIslandRecipeV1& recipe,const FloatingIslandPlanV1& plan,
|
||||||
const FloatingIslandAnchorV1& island,double x,double y)
|
const FloatingIslandAnchorV1& island,double x,double y)
|
||||||
{
|
{
|
||||||
|
FScopedIslandAttributionTimer columnTimer(EIslandAttributionBucket::Column);
|
||||||
// Density samples arrive on integer Core lattice columns; reuse the exact column metrics
|
// Density samples arrive on integer Core lattice columns; reuse the exact column metrics
|
||||||
// across Z samples without changing the field value or touching shared world state.
|
// across Z samples without changing the field value or touching shared world state.
|
||||||
// Les echantillons suivent les colonnes Core entieres; reutiliser les memes metriques XY.
|
// Les echantillons suivent les colonnes Core entieres; reutiliser les memes metriques XY.
|
||||||
@@ -487,6 +546,7 @@ namespace floating_island_detail
|
|||||||
static thread_local VoxelForgeCoreThreadLocal::FLazy<
|
static thread_local VoxelForgeCoreThreadLocal::FLazy<
|
||||||
std::array<FIslandColumnCacheEntry,SetCount*Ways>> Cache;
|
std::array<FIslandColumnCacheEntry,SetCount*Ways>> Cache;
|
||||||
auto& cache=Cache.Get();
|
auto& cache=Cache.Get();
|
||||||
|
FScopedIslandAttributionTimer localProbeTimer(EIslandAttributionBucket::LocalProbe);
|
||||||
const std::int64_t SampleX=static_cast<std::int64_t>(x);
|
const std::int64_t SampleX=static_cast<std::int64_t>(x);
|
||||||
const std::int64_t SampleY=static_cast<std::int64_t>(y);
|
const std::int64_t SampleY=static_cast<std::int64_t>(y);
|
||||||
std::uint64_t IndexKey=island.stableId
|
std::uint64_t IndexKey=island.stableId
|
||||||
@@ -501,6 +561,7 @@ namespace floating_island_detail
|
|||||||
IndexKey^=IndexKey>>33;
|
IndexKey^=IndexKey>>33;
|
||||||
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
|
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
|
||||||
FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))];
|
FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))];
|
||||||
|
bool bFoundInvalid=false;
|
||||||
for(std::size_t Way=0;Way<Ways;++Way)
|
for(std::size_t Way=0;Way<Ways;++Way)
|
||||||
{
|
{
|
||||||
FIslandColumnCacheEntry& Entry=cache[Base+Way];
|
FIslandColumnCacheEntry& Entry=cache[Base+Way];
|
||||||
@@ -516,8 +577,64 @@ namespace floating_island_detail
|
|||||||
&&Entry.TopHalf==island.topHalfSubvoxels
|
&&Entry.TopHalf==island.topHalfSubvoxels
|
||||||
&&Entry.TopFlattenQ16==recipe.topFlattenQ16
|
&&Entry.TopFlattenQ16==recipe.topFlattenQ16
|
||||||
&&Entry.ContourWarp==recipe.contourWarpSubvoxels;
|
&&Entry.ContourWarp==recipe.contourWarpSubvoxels;
|
||||||
if(bKeyMatches)return Entry;
|
if(bKeyMatches)
|
||||||
if(!Entry.bValid)Replacement=&Entry;
|
{
|
||||||
|
if(auto* stats=ActiveIslandColumnCacheInstrumentation())++stats->localHits;
|
||||||
|
return Entry;
|
||||||
|
}
|
||||||
|
if(!Entry.bValid&&!bFoundInvalid)
|
||||||
|
{Replacement=&Entry;bFoundInvalid=true;}
|
||||||
|
}
|
||||||
|
localProbeTimer.Stop();
|
||||||
|
if(auto* stats=ActiveIslandColumnCacheInstrumentation())
|
||||||
|
{
|
||||||
|
++stats->localMisses;
|
||||||
|
if(!bFoundInvalid)++stats->localConflictReplacements;
|
||||||
|
static thread_local FIslandColumnReuseTrace trace;
|
||||||
|
if((++trace.missSequence&1023u)==0)
|
||||||
|
{
|
||||||
|
auto& a=stats->attribution;
|
||||||
|
++a.traceSampledMisses;
|
||||||
|
const FIslandColumnTraceKey requested{true,recipe.contentHash,
|
||||||
|
plan.contentHash,island.stableId,SampleX,SampleY};
|
||||||
|
const std::size_t requestSlot=TraceKeyIndex(requested);
|
||||||
|
const bool seenBefore=SameTraceKey(trace.recentMisses[requestSlot],requested);
|
||||||
|
const bool reusedEviction=SameTraceKey(
|
||||||
|
trace.recentEvictions[requestSlot],requested);
|
||||||
|
if(seenBefore)++a.traceSeenBefore;
|
||||||
|
if(reusedEviction)
|
||||||
|
{
|
||||||
|
++a.traceReusedEvictions;
|
||||||
|
trace.recentEvictions[requestSlot].valid=false;
|
||||||
|
}
|
||||||
|
FIslandColumnTraceKey evicted{};
|
||||||
|
std::size_t replacementWay=0;
|
||||||
|
if(!bFoundInvalid&&Replacement)
|
||||||
|
{
|
||||||
|
evicted={Replacement->bValid,Replacement->RecipeContentHash,
|
||||||
|
Replacement->PlanContentHash,Replacement->StableId,
|
||||||
|
Replacement->SampleX,Replacement->SampleY};
|
||||||
|
replacementWay=static_cast<std::size_t>(Replacement-&cache[Base]);
|
||||||
|
++a.traceEvictions;
|
||||||
|
if(evicted.valid)
|
||||||
|
trace.recentEvictions[TraceKeyIndex(evicted)]=evicted;
|
||||||
|
}
|
||||||
|
trace.recentMisses[requestSlot]=requested;
|
||||||
|
if(trace.printedEvents<16&&a.traceCount<a.traceSet.size())
|
||||||
|
{
|
||||||
|
const std::size_t event=a.traceCount++;
|
||||||
|
++trace.printedEvents;
|
||||||
|
a.traceSet[event]=Base/Ways;
|
||||||
|
a.traceX[event]=SampleX;a.traceY[event]=SampleY;
|
||||||
|
a.traceIsland[event]=island.stableId;
|
||||||
|
a.traceEvictedX[event]=evicted.valid?evicted.x:0;
|
||||||
|
a.traceEvictedY[event]=evicted.valid?evicted.y:0;
|
||||||
|
a.traceEvictedIsland[event]=evicted.valid?evicted.island:0;
|
||||||
|
a.traceFlags[event]=static_cast<std::uint8_t>(
|
||||||
|
(!bFoundInvalid?1u:0u)|(seenBefore?2u:0u)|(reusedEviction?4u:0u)
|
||||||
|
|(replacementWay<<3));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto build=[&]()
|
const auto build=[&]()
|
||||||
@@ -544,9 +661,14 @@ namespace floating_island_detail
|
|||||||
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
|
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
|
||||||
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
|
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
|
||||||
Entry.LobePhase=LobePhase(recipe,island);
|
Entry.LobePhase=LobePhase(recipe,island);
|
||||||
|
double contourOffset=0.0;
|
||||||
|
{
|
||||||
|
FScopedIslandAttributionTimer helixTimer(EIslandAttributionBucket::Helix);
|
||||||
|
contourOffset=LobeOffsetWithPhase(recipe,Entry.Angle,
|
||||||
|
static_cast<double>(island.topZSubvoxels),Entry.LobePhase);
|
||||||
|
}
|
||||||
const double EffectiveRadius=std::max(1.0,static_cast<double>(island.radiusSubvoxels)
|
const double EffectiveRadius=std::max(1.0,static_cast<double>(island.radiusSubvoxels)
|
||||||
+LobeOffsetWithPhase(recipe,Entry.Angle,static_cast<double>(island.topZSubvoxels),
|
+contourOffset);
|
||||||
Entry.LobePhase));
|
|
||||||
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
|
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
|
||||||
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
||||||
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
|
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
|
||||||
@@ -562,7 +684,12 @@ namespace floating_island_detail
|
|||||||
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
|
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
|
||||||
plan.contentHash,SampleX,SampleY,island.stableId};
|
plan.contentHash,SampleX,SampleY,island.stableId};
|
||||||
bool computed=false;
|
bool computed=false;
|
||||||
Entry=shared->getOrCompute(key,build,computed);
|
auto* stats=ActiveIslandColumnCacheInstrumentation();
|
||||||
|
{
|
||||||
|
FScopedIslandAttributionTimer sharedCacheTimer(EIslandAttributionBucket::SharedCache);
|
||||||
|
Entry=shared->getOrCompute(key,build,computed,
|
||||||
|
stats?&stats->shared:nullptr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else Entry=build();
|
else Entry=build();
|
||||||
*Replacement=Entry;
|
*Replacement=Entry;
|
||||||
@@ -625,6 +752,7 @@ namespace floating_island_detail
|
|||||||
const FloatingIslandAnchorV1& island,
|
const FloatingIslandAnchorV1& island,
|
||||||
std::int32_t rootIndex, double x, double y, double z)
|
std::int32_t rootIndex, double x, double y, double z)
|
||||||
{
|
{
|
||||||
|
FScopedIslandAttributionTimer attributionTimer(EIslandAttributionBucket::Root);
|
||||||
// Cache derived root constants per worker. The complete immutable input key keeps this
|
// Cache derived root constants per worker. The complete immutable input key keeps this
|
||||||
// value-only cache safe across worlds, epochs and worker schedules.
|
// value-only cache safe across worlds, epochs and worker schedules.
|
||||||
// Mettre en cache les constantes par worker; la cle complete evite tout partage d'etat.
|
// Mettre en cache les constantes par worker; la cle complete evite tout partage d'etat.
|
||||||
@@ -695,6 +823,38 @@ namespace floating_island_detail
|
|||||||
return (std::sqrt(std::max(q, 0.0)) - 1.0) * Parameters.MinimumRadius;
|
return (std::sqrt(std::max(q, 0.0)) - 1.0) * Parameters.MinimumRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline FRootLobeParameters RootLobeParametersForBounds(
|
||||||
|
const FloatingIslandRecipeV1& recipe,const FloatingIslandAnchorV1& island,
|
||||||
|
std::int32_t rootIndex)
|
||||||
|
{
|
||||||
|
constexpr std::array<std::array<double,2>,4> pattern{{
|
||||||
|
{{-0.92,-0.12}},{{0.28,0.98}},{{0.83,-0.42}},{{-0.16,-0.94}}}};
|
||||||
|
const std::uint64_t salt=0x524f4f540000ull+static_cast<std::uint64_t>(rootIndex);
|
||||||
|
const std::uint64_t roll=Roll(recipe.seed,island.stableId,salt);
|
||||||
|
const double radius=static_cast<double>(island.radiusSubvoxels);
|
||||||
|
const double rootDepth=radius*Q16ToDouble(recipe.rootDepthRatioQ16);
|
||||||
|
const double crustBottom=static_cast<double>(IslandCrustBottom(recipe,island));
|
||||||
|
const bool centralButtress=rootIndex==4;
|
||||||
|
const double orbit=centralButtress?0.0:radius*(0.17+0.07*UnitHash(roll));
|
||||||
|
const double rootX=static_cast<double>(island.centerSubvoxels[0])
|
||||||
|
+(centralButtress?0.0:pattern[static_cast<std::size_t>(rootIndex)][0]*orbit);
|
||||||
|
const double rootY=static_cast<double>(island.centerSubvoxels[1])
|
||||||
|
+(centralButtress?0.0:pattern[static_cast<std::size_t>(rootIndex)][1]*orbit);
|
||||||
|
const double shortRootVariation=UnitHash(Roll(recipe.seed,island.stableId,salt^0x4c454eull));
|
||||||
|
const double lengthRatio=centralButtress?1.0:(rootIndex==0?1.0
|
||||||
|
:Q16ToDouble(island.taperEndQ16)*(0.82+0.18*shortRootVariation));
|
||||||
|
const double lowerDepth=rootDepth*lengthRatio;
|
||||||
|
const double embedDepth=rootDepth*(centralButtress?0.22:(rootIndex==0?0.30:0.22));
|
||||||
|
const double verticalRadius=0.5*(lowerDepth+embedDepth);
|
||||||
|
const double centerZ=crustBottom+0.5*(embedDepth-lowerDepth);
|
||||||
|
const double rx=radius*(centralButtress?0.25
|
||||||
|
:0.15+0.055*UnitHash(Roll(recipe.seed,island.stableId,salt^0x5258ull)));
|
||||||
|
const double ry=radius*(centralButtress?0.25
|
||||||
|
:0.15+0.065*UnitHash(Roll(recipe.seed,island.stableId,salt^0x5259ull)));
|
||||||
|
return {rootX,rootY,centerZ,rx*rx,ry*ry,verticalRadius*verticalRadius,
|
||||||
|
std::min({rx,ry,verticalRadius})};
|
||||||
|
}
|
||||||
|
|
||||||
inline double ApplySurfaceDetail(const FloatingIslandRecipeV1& recipe,
|
inline double ApplySurfaceDetail(const FloatingIslandRecipeV1& recipe,
|
||||||
std::uint64_t featureId, double structural,
|
std::uint64_t featureId, double structural,
|
||||||
double x, double y, double z, double detailScale = 1.0)
|
double x, double y, double z, double detailScale = 1.0)
|
||||||
@@ -713,6 +873,7 @@ namespace floating_island_detail
|
|||||||
const FloatingIslandAnchorV1& island,
|
const FloatingIslandAnchorV1& island,
|
||||||
double x, double y, double z)
|
double x, double y, double z)
|
||||||
{
|
{
|
||||||
|
FScopedIslandAttributionTimer islandTimer(EIslandAttributionBucket::Island);
|
||||||
if (recipe.geometryVersion < 2)
|
if (recipe.geometryVersion < 2)
|
||||||
{
|
{
|
||||||
const double dx = x - static_cast<double>(island.centerSubvoxels[0]);
|
const double dx = x - static_cast<double>(island.centerSubvoxels[0]);
|
||||||
@@ -740,8 +901,13 @@ namespace floating_island_detail
|
|||||||
const double radial=Column.Radial;
|
const double radial=Column.Radial;
|
||||||
const double topZ = static_cast<double>(island.topZSubvoxels);
|
const double topZ = static_cast<double>(island.topZSubvoxels);
|
||||||
const double radius = static_cast<double>(island.radiusSubvoxels);
|
const double radius = static_cast<double>(island.radiusSubvoxels);
|
||||||
|
double contourOffset=0.0;
|
||||||
|
{
|
||||||
|
FScopedIslandAttributionTimer helixTimer(EIslandAttributionBucket::Helix);
|
||||||
|
contourOffset=LobeOffsetWithPhase(recipe,Column.Angle,z,Column.LobePhase);
|
||||||
|
}
|
||||||
const double effectiveRadius = std::max(0.0,
|
const double effectiveRadius = std::max(0.0,
|
||||||
radius + LobeOffsetWithPhase(recipe,Column.Angle,z,Column.LobePhase));
|
radius + contourOffset);
|
||||||
const double edge = std::clamp(radial / std::max(effectiveRadius, 1.0), 0.0, 1.0);
|
const double edge = std::clamp(radial / std::max(effectiveRadius, 1.0), 0.0, 1.0);
|
||||||
const double lowerSurface = static_cast<double>(IslandCrustBottom(recipe, island))
|
const double lowerSurface = static_cast<double>(IslandCrustBottom(recipe, island))
|
||||||
+ radius * 0.16 * edge * edge;
|
+ radius * 0.16 * edge * edge;
|
||||||
@@ -788,6 +954,7 @@ namespace floating_island_detail
|
|||||||
bool* topSurface = nullptr,
|
bool* topSurface = nullptr,
|
||||||
double* alongFraction = nullptr)
|
double* alongFraction = nullptr)
|
||||||
{
|
{
|
||||||
|
FScopedIslandAttributionTimer connectorTimer(EIslandAttributionBucket::Connector);
|
||||||
const double ax = static_cast<double>(edge.startSubvoxels[0]);
|
const double ax = static_cast<double>(edge.startSubvoxels[0]);
|
||||||
const double ay = static_cast<double>(edge.startSubvoxels[1]);
|
const double ay = static_cast<double>(edge.startSubvoxels[1]);
|
||||||
const double az = static_cast<double>(edge.startSubvoxels[2]);
|
const double az = static_cast<double>(edge.startSubvoxels[2]);
|
||||||
@@ -845,6 +1012,227 @@ namespace floating_island_detail
|
|||||||
return std::max({horizontal, upper, lower});
|
return std::max({horizontal, upper, lower});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FScalarRange
|
||||||
|
{
|
||||||
|
double minimum=0.0,maximum=0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FDensityRange
|
||||||
|
{
|
||||||
|
double minimum=0.0,maximum=0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline double DistanceToRange(double minimum,double maximum,double value)
|
||||||
|
{
|
||||||
|
if(value<minimum)return minimum-value;
|
||||||
|
if(value>maximum)return value-maximum;
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline double MaximumDistanceToRange(double minimum,double maximum,double value)
|
||||||
|
{
|
||||||
|
return std::max(std::abs(minimum-value),std::abs(maximum-value));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FScalarRange AffineRange(double xMinimum,double xMaximum,double xOffset,
|
||||||
|
double xScale,double yMinimum,double yMaximum,double yOffset,double yScale)
|
||||||
|
{
|
||||||
|
const double xa=(xMinimum-xOffset)*xScale,xb=(xMaximum-xOffset)*xScale;
|
||||||
|
const double ya=(yMinimum-yOffset)*yScale,yb=(yMaximum-yOffset)*yScale;
|
||||||
|
return {std::min(xa,xb)+std::min(ya,yb),
|
||||||
|
std::max(xa,xb)+std::max(ya,yb)};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FScalarRange AbsoluteRange(double minimum,double maximum)
|
||||||
|
{
|
||||||
|
const double minimumAbsolute=minimum<=0.0&&maximum>=0.0
|
||||||
|
?0.0:std::min(std::abs(minimum),std::abs(maximum));
|
||||||
|
return {minimumAbsolute,std::max(std::abs(minimum),std::abs(maximum))};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline double RectangleSdf(double x,double y)
|
||||||
|
{
|
||||||
|
return std::hypot(std::max(x,0.0),std::max(y,0.0))
|
||||||
|
+std::min(std::max(x,y),0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline double RoundedBoxSdf(double x,double y,double z)
|
||||||
|
{
|
||||||
|
return std::sqrt(std::max(x,0.0)*std::max(x,0.0)
|
||||||
|
+std::max(y,0.0)*std::max(y,0.0)
|
||||||
|
+std::max(z,0.0)*std::max(z,0.0))
|
||||||
|
+std::min(std::max(x,std::max(y,z)),0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FDensityRange RootLobeRange(const FloatingIslandRecipeV1& recipe,
|
||||||
|
const FloatingIslandAnchorV1& island,std::int32_t rootIndex,
|
||||||
|
const std::array<double,3>& minimum,const std::array<double,3>& maximum)
|
||||||
|
{
|
||||||
|
const FRootLobeParameters root=RootLobeParametersForBounds(recipe,island,rootIndex);
|
||||||
|
const double dxMinimum=DistanceToRange(minimum[0],maximum[0],root.RootX);
|
||||||
|
const double dyMinimum=DistanceToRange(minimum[1],maximum[1],root.RootY);
|
||||||
|
const double dzMinimum=DistanceToRange(minimum[2],maximum[2],root.CenterZ);
|
||||||
|
const double dxMaximum=MaximumDistanceToRange(minimum[0],maximum[0],root.RootX);
|
||||||
|
const double dyMaximum=MaximumDistanceToRange(minimum[1],maximum[1],root.RootY);
|
||||||
|
const double dzMaximum=MaximumDistanceToRange(minimum[2],maximum[2],root.CenterZ);
|
||||||
|
const double qMinimum=dxMinimum*dxMinimum/root.RadiusXSquared
|
||||||
|
+dyMinimum*dyMinimum/root.RadiusYSquared
|
||||||
|
+dzMinimum*dzMinimum/root.VerticalRadiusSquared;
|
||||||
|
const double qMaximum=dxMaximum*dxMaximum/root.RadiusXSquared
|
||||||
|
+dyMaximum*dyMaximum/root.RadiusYSquared
|
||||||
|
+dzMaximum*dzMaximum/root.VerticalRadiusSquared;
|
||||||
|
return {(std::sqrt(std::max(qMinimum,0.0))-1.0)*root.MinimumRadius,
|
||||||
|
(std::sqrt(std::max(qMaximum,0.0))-1.0)*root.MinimumRadius};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FDensityRange IslandDensityRange(const FloatingIslandRecipeV1& recipe,
|
||||||
|
const FloatingIslandAnchorV1& island,const std::array<double,3>& minimum,
|
||||||
|
const std::array<double,3>& maximum)
|
||||||
|
{
|
||||||
|
const double radius=static_cast<double>(island.radiusSubvoxels);
|
||||||
|
const double contour=static_cast<double>(recipe.contourWarpSubvoxels);
|
||||||
|
const double radialMinimum=std::hypot(
|
||||||
|
DistanceToRange(minimum[0],maximum[0],
|
||||||
|
static_cast<double>(island.centerSubvoxels[0])),
|
||||||
|
DistanceToRange(minimum[1],maximum[1],
|
||||||
|
static_cast<double>(island.centerSubvoxels[1])));
|
||||||
|
const double radialMaximum=std::max({
|
||||||
|
std::hypot(minimum[0]-island.centerSubvoxels[0],minimum[1]-island.centerSubvoxels[1]),
|
||||||
|
std::hypot(minimum[0]-island.centerSubvoxels[0],maximum[1]-island.centerSubvoxels[1]),
|
||||||
|
std::hypot(maximum[0]-island.centerSubvoxels[0],minimum[1]-island.centerSubvoxels[1]),
|
||||||
|
std::hypot(maximum[0]-island.centerSubvoxels[0],maximum[1]-island.centerSubvoxels[1])});
|
||||||
|
|
||||||
|
const double top=static_cast<double>(island.topZSubvoxels);
|
||||||
|
const double topDrop=std::max(0.0,(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
||||||
|
*static_cast<double>(island.topHalfSubvoxels)*2.0);
|
||||||
|
const double topDistanceMinimum=minimum[2]-top;
|
||||||
|
const double topDistanceMaximum=maximum[2]-(top-topDrop);
|
||||||
|
const double sideDistanceMinimum=radialMinimum-std::max(0.0,radius+contour);
|
||||||
|
const double sideDistanceMaximum=recipe.geometryVersion<2
|
||||||
|
?radialMaximum:radialMaximum-std::max(0.0,radius-contour);
|
||||||
|
double lowerMinimum=0.0,lowerMaximum=0.0;
|
||||||
|
if(recipe.geometryVersion<2)
|
||||||
|
{
|
||||||
|
const double bottom=static_cast<double>(island.bottomZSubvoxels);
|
||||||
|
lowerMinimum=bottom-maximum[2];
|
||||||
|
lowerMaximum=bottom-minimum[2];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const double crust=static_cast<double>(IslandCrustBottom(recipe,island));
|
||||||
|
lowerMinimum=crust-maximum[2];
|
||||||
|
lowerMaximum=crust+radius*0.16-minimum[2];
|
||||||
|
}
|
||||||
|
double structuralMinimum=std::max({topDistanceMinimum,
|
||||||
|
sideDistanceMinimum,lowerMinimum});
|
||||||
|
double structuralMaximum=std::max({topDistanceMaximum,
|
||||||
|
sideDistanceMaximum,lowerMaximum});
|
||||||
|
|
||||||
|
if(recipe.geometryVersion>=2)
|
||||||
|
{
|
||||||
|
const double rootBlend=std::clamp(radius*0.12,2.0*Q,5.0*Q);
|
||||||
|
for(std::int32_t root=0;root<RootCount(recipe,island);++root)
|
||||||
|
{
|
||||||
|
const FDensityRange rootRange=RootLobeRange(recipe,island,root,minimum,maximum);
|
||||||
|
structuralMinimum=std::min(structuralMinimum,rootRange.minimum)-rootBlend*0.25;
|
||||||
|
structuralMaximum=std::min(structuralMaximum,rootRange.maximum);
|
||||||
|
}
|
||||||
|
if(recipe.geometryVersion>=5&&recipe.geometryVersion<8
|
||||||
|
&&island.ordinal==recipe.anchorIslandOrdinal
|
||||||
|
&&recipe.anchorRootBottomZSubvoxels<island.bottomZSubvoxels)
|
||||||
|
{
|
||||||
|
const double rootTop=static_cast<double>(IslandCrustBottom(recipe,island));
|
||||||
|
const double rootBottom=static_cast<double>(recipe.anchorRootBottomZSubvoxels);
|
||||||
|
const double rootRadius=static_cast<double>(recipe.anchorRootRadiusSubvoxels);
|
||||||
|
const double anchorMinimum=std::max({radialMinimum-rootRadius,
|
||||||
|
rootBottom-maximum[2],minimum[2]-rootTop});
|
||||||
|
const double anchorMaximum=std::max({radialMaximum-0.92*rootRadius,
|
||||||
|
rootBottom-minimum[2],maximum[2]-rootTop});
|
||||||
|
const double anchorDetail=0.75*static_cast<double>(recipe.roughnessSubvoxels);
|
||||||
|
const double anchorBlend=std::clamp(rootRadius*0.12,3.0*Q,6.0*Q);
|
||||||
|
structuralMinimum=std::min(structuralMinimum,anchorMinimum-anchorDetail)
|
||||||
|
-anchorBlend*0.25;
|
||||||
|
structuralMaximum=std::min(structuralMaximum,anchorMaximum+anchorDetail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const double finalDetail=static_cast<double>(recipe.roughnessSubvoxels);
|
||||||
|
return {structuralMinimum-finalDetail,structuralMaximum+finalDetail};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FDensityRange ConnectorDensityRange(const FloatingIslandRecipeV1& recipe,
|
||||||
|
const FloatingIslandConnectorV1& edge,const std::array<double,3>& minimum,
|
||||||
|
const std::array<double,3>& maximum)
|
||||||
|
{
|
||||||
|
const double ax=static_cast<double>(edge.startSubvoxels[0]);
|
||||||
|
const double ay=static_cast<double>(edge.startSubvoxels[1]);
|
||||||
|
const double az=static_cast<double>(edge.startSubvoxels[2]);
|
||||||
|
const double dx=static_cast<double>(edge.endSubvoxels[0])-ax;
|
||||||
|
const double dy=static_cast<double>(edge.endSubvoxels[1])-ay;
|
||||||
|
const double dz=static_cast<double>(edge.endSubvoxels[2])-az;
|
||||||
|
const double length=std::hypot(dx,dy);
|
||||||
|
if(!(length>0.0))return {static_cast<double>(MaxDensity),static_cast<double>(MaxDensity)};
|
||||||
|
const double ux=dx/length,uy=dy/length;
|
||||||
|
const FScalarRange along=AffineRange(minimum[0],maximum[0],ax,ux,
|
||||||
|
minimum[1],maximum[1],ay,uy);
|
||||||
|
const FScalarRange signedSide=AffineRange(minimum[0],maximum[0],ax,-uy,
|
||||||
|
minimum[1],maximum[1],ay,ux);
|
||||||
|
const FScalarRange side=AbsoluteRange(signedSide.minimum,signedSide.maximum);
|
||||||
|
const double qxMinimum=side.minimum-static_cast<double>(edge.halfWidthSubvoxels);
|
||||||
|
const double qxMaximum=side.maximum-static_cast<double>(edge.halfWidthSubvoxels);
|
||||||
|
const FScalarRange alongCenter=AbsoluteRange(along.minimum-0.5*length,
|
||||||
|
along.maximum-0.5*length);
|
||||||
|
const double qyMinimum=alongCenter.minimum-0.5*length;
|
||||||
|
const double qyMaximum=alongCenter.maximum-0.5*length;
|
||||||
|
const double horizontalMinimum=RectangleSdf(qxMinimum,qyMinimum);
|
||||||
|
const double horizontalMaximum=RectangleSdf(qxMaximum,qyMaximum);
|
||||||
|
|
||||||
|
const double tMinimum=std::clamp(along.minimum/length,0.0,1.0);
|
||||||
|
const double tMaximum=std::clamp(along.maximum/length,0.0,1.0);
|
||||||
|
const double deckAtMinimum=az+dz*tMinimum;
|
||||||
|
const double deckAtMaximum=az+dz*tMaximum;
|
||||||
|
const double deckMinimum=std::min(deckAtMinimum,deckAtMaximum);
|
||||||
|
const double deckMaximum=std::max(deckAtMinimum,deckAtMaximum);
|
||||||
|
const std::uint64_t westernEdgeId=StableId(recipe.seed,0x200000000ull,
|
||||||
|
0x425249444745ull);
|
||||||
|
const bool westernSupportConnector=edge.stableId==westernEdgeId;
|
||||||
|
if(recipe.geometryVersion>=2&&WorldAssemblyEvaluatorVersion>=183
|
||||||
|
&&westernSupportConnector)
|
||||||
|
{
|
||||||
|
const double minimumHalfWidth=static_cast<double>(recipe.routeConstraints.capsuleRadiusSubvoxels+Q);
|
||||||
|
const double baseHalfWidth=static_cast<double>(edge.halfWidthSubvoxels);
|
||||||
|
const double halfWidthMinimum=std::max(minimumHalfWidth,baseHalfWidth-0.48*Q);
|
||||||
|
const double halfWidthMaximum=std::max(minimumHalfWidth,baseHalfWidth+0.48*Q);
|
||||||
|
const double roundingMinimum=std::min({1.5*Q,halfWidthMinimum*0.6,
|
||||||
|
static_cast<double>(edge.thicknessSubvoxels)*0.45});
|
||||||
|
const double roundingMaximum=std::min({1.5*Q,halfWidthMaximum*0.6,
|
||||||
|
static_cast<double>(edge.thicknessSubvoxels)*0.45});
|
||||||
|
const double roundedXMinimum=side.minimum-halfWidthMaximum+roundingMinimum;
|
||||||
|
const double roundedXMaximum=side.maximum-halfWidthMinimum+roundingMaximum;
|
||||||
|
const double roundedYMinimum=alongCenter.minimum-0.5*length+roundingMinimum;
|
||||||
|
const double roundedYMaximum=alongCenter.maximum-0.5*length+roundingMaximum;
|
||||||
|
const double halfThickness=0.5*static_cast<double>(edge.thicknessSubvoxels);
|
||||||
|
const double centerMinimum=deckMinimum-halfThickness;
|
||||||
|
const double centerMaximum=deckMaximum-halfThickness;
|
||||||
|
const double differenceMinimum=minimum[2]-centerMaximum;
|
||||||
|
const double differenceMaximum=maximum[2]-centerMinimum;
|
||||||
|
const FScalarRange absoluteDifference=AbsoluteRange(differenceMinimum,differenceMaximum);
|
||||||
|
const double roundedZMinimum=absoluteDifference.minimum-halfThickness+roundingMinimum;
|
||||||
|
const double roundedZMaximum=absoluteDifference.maximum-halfThickness+roundingMaximum;
|
||||||
|
return {RoundedBoxSdf(roundedXMinimum,roundedYMinimum,roundedZMinimum),
|
||||||
|
RoundedBoxSdf(roundedXMaximum,roundedYMaximum,roundedZMaximum)};
|
||||||
|
}
|
||||||
|
|
||||||
|
const double thickness=static_cast<double>(edge.thicknessSubvoxels);
|
||||||
|
const double maximumArch=recipe.geometryVersion>=2&&WorldAssemblyEvaluatorVersion<172
|
||||||
|
?2.0*thickness:0.0;
|
||||||
|
const double upperMinimum=minimum[2]-deckMaximum;
|
||||||
|
const double upperMaximum=maximum[2]-deckMinimum;
|
||||||
|
const double lowerMinimum=deckMinimum-thickness-maximumArch-maximum[2];
|
||||||
|
const double lowerMaximum=deckMaximum-thickness-minimum[2];
|
||||||
|
return {std::max({horizontalMinimum,upperMinimum,lowerMinimum}),
|
||||||
|
std::max({horizontalMaximum,upperMaximum,lowerMaximum})};
|
||||||
|
}
|
||||||
|
|
||||||
inline std::int64_t FieldDensity(const FloatingIslandRecipeV1& recipe,
|
inline std::int64_t FieldDensity(const FloatingIslandRecipeV1& recipe,
|
||||||
const FloatingIslandPlanV1& plan,
|
const FloatingIslandPlanV1& plan,
|
||||||
std::int64_t x, std::int64_t y, std::int64_t z)
|
std::int64_t x, std::int64_t y, std::int64_t z)
|
||||||
@@ -1898,14 +2286,19 @@ bool FloatingIslandFieldV1::isValid() const noexcept
|
|||||||
std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x,
|
std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x,
|
||||||
std::int64_t y, std::int64_t z) const noexcept
|
std::int64_t y, std::int64_t z) const noexcept
|
||||||
{
|
{
|
||||||
|
floating_island_detail::FScopedIslandAttributionTimer fieldTimer(
|
||||||
|
floating_island_detail::EIslandAttributionBucket::Field);
|
||||||
// The selected evaluator validates once; do not scan the plan twice per scoped sample.
|
// The selected evaluator validates once; do not scan the plan twice per scoped sample.
|
||||||
// L'evaluateur choisi valide une seule fois; ne pas reparcourir le plan a chaque echantillon.
|
// L'evaluateur choisi valide une seule fois; ne pas reparcourir le plan a chaque echantillon.
|
||||||
|
std::int64_t density=floating_island_detail::MaxDensity;
|
||||||
if (const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find(recipe_, plan_, x, y, z))
|
if (const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find(recipe_, plan_, x, y, z))
|
||||||
return densitySubvoxelsForTile(x, y, z, *list);
|
density=densitySubvoxelsForTile(x, y, z, *list);
|
||||||
if (!isValid() || !floating_island_detail::ValidScalar(x)
|
else if (!isValid() || !floating_island_detail::ValidScalar(x)
|
||||||
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
||||||
return floating_island_detail::MaxDensity;
|
density=floating_island_detail::MaxDensity;
|
||||||
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
else density=floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
||||||
|
fieldTimer.Stop();
|
||||||
|
return density;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<WorldAssemblyFeatureBoundsV1> FloatingIslandFieldV1::solidSupportBounds() const
|
std::vector<WorldAssemblyFeatureBoundsV1> FloatingIslandFieldV1::solidSupportBounds() const
|
||||||
@@ -2104,19 +2497,46 @@ FieldInterval FloatingIslandFieldV1::interval(std::int64_t minX, std::int64_t mi
|
|||||||
|| !floating_island_detail::ValidScalar(minX) || !floating_island_detail::ValidScalar(minY)
|
|| !floating_island_detail::ValidScalar(minX) || !floating_island_detail::ValidScalar(minY)
|
||||||
|| !floating_island_detail::ValidScalar(minZ) || !floating_island_detail::ValidScalar(maxX)
|
|| !floating_island_detail::ValidScalar(minZ) || !floating_island_detail::ValidScalar(maxX)
|
||||||
|| !floating_island_detail::ValidScalar(maxY) || !floating_island_detail::ValidScalar(maxZ)) return result;
|
|| !floating_island_detail::ValidScalar(maxY) || !floating_island_detail::ValidScalar(maxZ)) return result;
|
||||||
const std::array<std::int64_t, 3> minimum{minX, minY, minZ};
|
using namespace floating_island_detail;
|
||||||
const std::array<std::int64_t, 3> maximum{maxX, maxY, maxZ};
|
if (plan_->islands.empty())
|
||||||
for (const auto& island : plan_->islands)
|
return {true,MaxDensity,MaxDensity};
|
||||||
if (floating_island_detail::Intersects(minimum, maximum,
|
|
||||||
island.influenceMinimumSubvoxels, island.influenceMaximumSubvoxels)) return result;
|
const std::array<double,3> minimum{static_cast<double>(minX),
|
||||||
for (const auto& edge : plan_->clusterEdges)
|
static_cast<double>(minY),static_cast<double>(minZ)};
|
||||||
if (floating_island_detail::Intersects(minimum, maximum,
|
const std::array<double,3> maximum{static_cast<double>(maxX),
|
||||||
edge.influenceMinimumSubvoxels, edge.influenceMaximumSubvoxels)) return result;
|
static_cast<double>(maxY),static_cast<double>(maxZ)};
|
||||||
// Root is void. A box outside every lobe/taper/blend/connector influence bound proves air.
|
double fieldMinimum=static_cast<double>(MaxDensity);
|
||||||
// La racine est le vide : une boite hors des influences bornees prouve l'air.
|
double fieldMaximum=static_cast<double>(MaxDensity);
|
||||||
result.known = true;
|
for(const auto& island:plan_->islands)
|
||||||
result.minimumDensitySubvoxels = 0;
|
{
|
||||||
result.maximumDensitySubvoxels = floating_island_detail::MaxDensity;
|
FDensityRange group=IslandDensityRange(*recipe_,island,minimum,maximum);
|
||||||
|
for(const auto& edge:plan_->clusterEdges)
|
||||||
|
{
|
||||||
|
if(edge.fromIslandId!=island.stableId)continue;
|
||||||
|
const FDensityRange connector=ConnectorDensityRange(*recipe_,edge,minimum,maximum);
|
||||||
|
const double detail=recipe_->geometryVersion>=2
|
||||||
|
?static_cast<double>(recipe_->roughnessSubvoxels):0.0;
|
||||||
|
group.minimum=std::min(group.minimum,connector.minimum-detail)
|
||||||
|
-static_cast<double>(recipe_->blendRadiusSubvoxels)*0.25;
|
||||||
|
group.maximum=std::min(group.maximum,connector.maximum+detail);
|
||||||
|
}
|
||||||
|
fieldMinimum=std::min(fieldMinimum,group.minimum);
|
||||||
|
fieldMaximum=std::min(fieldMaximum,group.maximum);
|
||||||
|
}
|
||||||
|
if(!std::isfinite(fieldMinimum)||!std::isfinite(fieldMaximum))return result;
|
||||||
|
|
||||||
|
// These interval operations are shared by native and future generator classifiers: SDF
|
||||||
|
// intersections take max, unions take min, and smooth unions subtract at most k/4.
|
||||||
|
// Keep two subvoxels of slack for floating bounds followed by the field's integer rounding.
|
||||||
|
// Ces bornes servent les generateurs actuels et futurs : intersections=max, unions=min.
|
||||||
|
const double minimumRounded=std::clamp(std::floor(fieldMinimum)-2.0,
|
||||||
|
-static_cast<double>(MaxDensity),static_cast<double>(MaxDensity));
|
||||||
|
const double maximumRounded=std::clamp(std::ceil(fieldMaximum)+2.0,
|
||||||
|
-static_cast<double>(MaxDensity),static_cast<double>(MaxDensity));
|
||||||
|
if(minimumRounded>maximumRounded)return result;
|
||||||
|
result.known=true;
|
||||||
|
result.minimumDensitySubvoxels=static_cast<std::int64_t>(minimumRounded);
|
||||||
|
result.maximumDensitySubvoxels=static_cast<std::int64_t>(maximumRounded);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -11,5 +11,6 @@ namespace VoxelForgeCore
|
|||||||
// the same command-line overrides as the viewer's reference loader.
|
// the same command-line overrides as the viewer's reference loader.
|
||||||
ResolvedWorldSnapshot MakeStandaloneSnapshotFromParsedJson(const VFJson::Value& jsonRoot,
|
ResolvedWorldSnapshot MakeStandaloneSnapshotFromParsedJson(const VFJson::Value& jsonRoot,
|
||||||
std::int32_t seed,std::int32_t clipRadius,std::int32_t maxLodOverride,
|
std::int32_t seed,std::int32_t clipRadius,std::int32_t maxLodOverride,
|
||||||
bool roundTunnels,bool enhancedTunnelWalls,std::string& error);
|
bool roundTunnels,bool enhancedTunnelWalls,std::string& error,
|
||||||
|
bool hasUnrealResolvedCorePlan=false);
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -20,6 +20,7 @@
|
|||||||
#define VF_FIELD_CLAMP(v,a,b) clamp(v,a,b)
|
#define VF_FIELD_CLAMP(v,a,b) clamp(v,a,b)
|
||||||
#define VF_FIELD_LERP(a,b,t) lerp(a,b,t)
|
#define VF_FIELD_LERP(a,b,t) lerp(a,b,t)
|
||||||
#else
|
#else
|
||||||
|
#include <array>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -244,11 +245,70 @@ VF_FIELD_INLINE float VF_FieldValueNoise(float x, float y, float z,
|
|||||||
return VF_FieldLerp(VF_FieldLerp(z00, z10, ty), VF_FieldLerp(z01, z11, ty), tz);
|
return VF_FieldLerp(VF_FieldLerp(z00, z10, ty), VF_FieldLerp(z01, z11, ty), tz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !defined(VF_FIELD_HLSL)
|
||||||
|
struct VF_FieldValueNoiseCacheEntry
|
||||||
|
{
|
||||||
|
VF_FieldI32 x=0,y=0,z=0,wavelength=0;
|
||||||
|
VF_FieldU32 seed=0;
|
||||||
|
bool valid=false;
|
||||||
|
std::array<float,8> corners{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reuse the eight lattice values while adjacent voxel samples remain in the same
|
||||||
|
// value-noise cell. Only the integer hash lookups are shared; interpolation keeps
|
||||||
|
// the same inputs and operation order as VF_FieldValueNoise.
|
||||||
|
VF_FIELD_INLINE float VF_FieldCachedValueNoise(float x,float y,float z,
|
||||||
|
VF_FieldI32 wavelength,VF_FieldU32 seed)
|
||||||
|
{
|
||||||
|
static thread_local std::array<VF_FieldValueNoiseCacheEntry,64> cache{};
|
||||||
|
const float inv=1.0f/VF_FIELD_CAST_F32(wavelength);
|
||||||
|
const float fx=x*inv,fy=y*inv,fz=z*inv;
|
||||||
|
const VF_FieldI32 x0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fx));
|
||||||
|
const VF_FieldI32 y0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fy));
|
||||||
|
const VF_FieldI32 z0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fz));
|
||||||
|
const VF_FieldU32 cellHash=VF_FieldMix(seed
|
||||||
|
^VF_FIELD_CAST_U32(x0)*0x68E31DA4u
|
||||||
|
^VF_FIELD_CAST_U32(y0)*0x1B56C4E9u
|
||||||
|
^VF_FIELD_CAST_U32(z0)*0x9E3779B9u
|
||||||
|
^VF_FIELD_CAST_U32(wavelength)*0x85EBCA77u);
|
||||||
|
auto& entry=cache[cellHash&(cache.size()-1)];
|
||||||
|
if(!entry.valid||entry.x!=x0||entry.y!=y0||entry.z!=z0
|
||||||
|
||entry.wavelength!=wavelength||entry.seed!=seed)
|
||||||
|
{
|
||||||
|
entry.x=x0;entry.y=y0;entry.z=z0;entry.wavelength=wavelength;
|
||||||
|
entry.seed=seed;entry.valid=true;
|
||||||
|
entry.corners={
|
||||||
|
VF_FieldNoiseValue(x0,y0,z0,seed),
|
||||||
|
VF_FieldNoiseValue(x0+1,y0,z0,seed),
|
||||||
|
VF_FieldNoiseValue(x0,y0+1,z0,seed),
|
||||||
|
VF_FieldNoiseValue(x0+1,y0+1,z0,seed),
|
||||||
|
VF_FieldNoiseValue(x0,y0,z0+1,seed),
|
||||||
|
VF_FieldNoiseValue(x0+1,y0,z0+1,seed),
|
||||||
|
VF_FieldNoiseValue(x0,y0+1,z0+1,seed),
|
||||||
|
VF_FieldNoiseValue(x0+1,y0+1,z0+1,seed)};
|
||||||
|
}
|
||||||
|
const float tx=VF_FieldFade(fx-VF_FIELD_CAST_F32(x0));
|
||||||
|
const float ty=VF_FieldFade(fy-VF_FIELD_CAST_F32(y0));
|
||||||
|
const float tz=VF_FieldFade(fz-VF_FIELD_CAST_F32(z0));
|
||||||
|
const float z00=VF_FieldLerp(entry.corners[0],entry.corners[1],tx);
|
||||||
|
const float z10=VF_FieldLerp(entry.corners[2],entry.corners[3],tx);
|
||||||
|
const float z01=VF_FieldLerp(entry.corners[4],entry.corners[5],tx);
|
||||||
|
const float z11=VF_FieldLerp(entry.corners[6],entry.corners[7],tx);
|
||||||
|
return VF_FieldLerp(VF_FieldLerp(z00,z10,ty),VF_FieldLerp(z01,z11,ty),tz);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
VF_FIELD_INLINE float VF_FieldSurfaceRelief(float x, float y, float z, VF_FieldU32 seed)
|
VF_FIELD_INLINE float VF_FieldSurfaceRelief(float x, float y, float z, VF_FieldU32 seed)
|
||||||
{
|
{
|
||||||
|
#if defined(VF_FIELD_HLSL)
|
||||||
const float macro = VF_FieldValueNoise(x, y, z, 32, seed ^ 0xB5297A4Du);
|
const float macro = VF_FieldValueNoise(x, y, z, 32, seed ^ 0xB5297A4Du);
|
||||||
const float mid = VF_FieldValueNoise(x, y, z, 16, seed ^ 0x63D83595u);
|
const float mid = VF_FieldValueNoise(x, y, z, 16, seed ^ 0x63D83595u);
|
||||||
const float fine = VF_FieldValueNoise(x, y, z, 4, seed ^ 0xA511E9B3u);
|
const float fine = VF_FieldValueNoise(x, y, z, 4, seed ^ 0xA511E9B3u);
|
||||||
|
#else
|
||||||
|
const float macro=VF_FieldCachedValueNoise(x,y,z,32,seed^0xB5297A4Du);
|
||||||
|
const float mid=VF_FieldCachedValueNoise(x,y,z,16,seed^0x63D83595u);
|
||||||
|
const float fine=VF_FieldCachedValueNoise(x,y,z,4,seed^0xA511E9B3u);
|
||||||
|
#endif
|
||||||
return 1.20f * macro + (0.80f + 0.25f * macro) * (0.40f * mid + 0.60f * fine);
|
return 1.20f * macro + (0.80f + 0.25f * macro) * (0.40f * mid + 0.60f * fine);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,12 +395,21 @@ VF_FIELD_INLINE float VF_FieldPassageWallRelief(float x,float y,float z,VF_Field
|
|||||||
{
|
{
|
||||||
// Use long, non-commensurate rotated scales. The shared passage wall keeps broad,
|
// Use long, non-commensurate rotated scales. The shared passage wall keeps broad,
|
||||||
// uneven rock relief without a short wavelength that can read as repeated rings.
|
// uneven rock relief without a short wavelength that can read as repeated rings.
|
||||||
|
#if defined(VF_FIELD_HLSL)
|
||||||
const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
|
const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
|
||||||
seed^0xB5297A4Du);
|
seed^0xB5297A4Du);
|
||||||
const float mid=VF_FieldValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
|
const float mid=VF_FieldValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
|
||||||
seed^0x63D83595u);
|
seed^0x63D83595u);
|
||||||
const float fine=VF_FieldValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
|
const float fine=VF_FieldValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
|
||||||
seed^0xA511E9B3u);
|
seed^0xA511E9B3u);
|
||||||
|
#else
|
||||||
|
const float broad=VF_FieldCachedValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
|
||||||
|
seed^0xB5297A4Du);
|
||||||
|
const float mid=VF_FieldCachedValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
|
||||||
|
seed^0x63D83595u);
|
||||||
|
const float fine=VF_FieldCachedValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
|
||||||
|
seed^0xA511E9B3u);
|
||||||
|
#endif
|
||||||
return 0.50f*broad+0.35f*mid+0.15f*fine;
|
return 0.50f*broad+0.35f*mid+0.15f*fine;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,9 +463,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_FieldI32 nx=cellX+dx,ny=cellY+dy,nz=cellZ+dz;
|
||||||
const VF_FieldU32 h=VF_FieldVoxelHashMix(
|
const VF_FieldU32 h=VF_FieldVoxelHashMix(
|
||||||
VF_FIELD_CAST_U32(nx+0x7FFFFFFF)
|
(VF_FIELD_CAST_U32(nx)+0x7FFFFFFFu)
|
||||||
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(ny+0x7FFFFFFF)*2654435761u)
|
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(ny)+0x7FFFFFFFu)*2654435761u)
|
||||||
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(nz+0x7FFFFFFF)*374761393u));
|
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(nz)+0x7FFFFFFFu)*374761393u));
|
||||||
const float fpx=VF_FIELD_CAST_F32(dx)
|
const float fpx=VF_FIELD_CAST_F32(dx)
|
||||||
+VF_FIELD_CAST_F32(h&0xFFFFFFu)*(1.0f/16777216.0f)-fracX;
|
+VF_FIELD_CAST_F32(h&0xFFFFFFu)*(1.0f/16777216.0f)-fracX;
|
||||||
const VF_FieldU32 hy=VF_FieldVoxelHashMix(h^0x12345678u);
|
const VF_FieldU32 hy=VF_FieldVoxelHashMix(h^0x12345678u);
|
||||||
|
|||||||
@@ -145,6 +145,12 @@ namespace world_assembly_detail
|
|||||||
};
|
};
|
||||||
|
|
||||||
constexpr std::int64_t Q16 = 1ll << 16;
|
constexpr std::int64_t Q16 = 1ll << 16;
|
||||||
|
constexpr std::int64_t Q32 = 1ll << 32;
|
||||||
|
constexpr std::int64_t SurfaceProfileSubvoxelsPerMetre =
|
||||||
|
WorldAssemblySubvoxelsPerVoxel * 4;
|
||||||
|
constexpr std::int64_t SurfaceProfileMinimumClearanceSubvoxels = 2253;
|
||||||
|
constexpr std::int64_t SurfaceProfileFightClearanceSubvoxels =
|
||||||
|
8 * SurfaceProfileSubvoxelsPerMetre;
|
||||||
constexpr std::int64_t MinimumCliffShelfThicknessSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel;
|
constexpr std::int64_t MinimumCliffShelfThicknessSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel;
|
||||||
constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll;
|
constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll;
|
||||||
constexpr std::size_t MaxBiomeCount = 8;
|
constexpr std::size_t MaxBiomeCount = 8;
|
||||||
@@ -228,6 +234,114 @@ namespace world_assembly_detail
|
|||||||
inline bool OutsideRange(std::int64_t value)
|
inline bool OutsideRange(std::int64_t value)
|
||||||
{ return value < -MaxSerializedMagnitude || value > MaxSerializedMagnitude; }
|
{ return value < -MaxSerializedMagnitude || value > MaxSerializedMagnitude; }
|
||||||
|
|
||||||
|
inline bool IsNaturalSurfaceProfile(const WorldAssemblySurfaceProfileV1& profile)
|
||||||
|
{ return profile.version == 0 && profile == WorldAssemblySurfaceProfileV1{}; }
|
||||||
|
|
||||||
|
inline bool ValidateSurfaceProfile(const WorldAssemblySurfaceProfileV1& profile,
|
||||||
|
std::uint32_t seed, std::string& error)
|
||||||
|
{
|
||||||
|
if (profile.version == 0)
|
||||||
|
{
|
||||||
|
if (!IsNaturalSurfaceProfile(profile))
|
||||||
|
{ error = "natural SurfaceWorld profile must use the all-zero default payload"; return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (profile.version != 1
|
||||||
|
|| !ValidScalar(profile.h0Subvoxels)
|
||||||
|
|| !ValidScalar(profile.amplitudeXSubvoxels)
|
||||||
|
|| !ValidScalar(profile.amplitudeYSubvoxels)
|
||||||
|
|| !ValidScalar(profile.wavelengthXSubvoxels)
|
||||||
|
|| !ValidScalar(profile.wavelengthYSubvoxels)
|
||||||
|
|| !ValidScalar(profile.mirrorLambdaQ16)
|
||||||
|
|| !ValidScalar(profile.minimumArenaClearanceSubvoxels)
|
||||||
|
|| profile.minimumArenaClearanceSubvoxels < 0
|
||||||
|
|| profile.minimumArenaClearanceSubvoxels > 32 * SurfaceProfileSubvoxelsPerMetre)
|
||||||
|
{ error = "SurfaceWorld profile version or fixed-scale value is outside its supported range"; return false; }
|
||||||
|
|
||||||
|
constexpr std::int64_t MinimumPeriodAmplitude = SurfaceProfileSubvoxelsPerMetre / 2;
|
||||||
|
constexpr std::int64_t MaximumPeriodicAmplitude = 3 * SurfaceProfileSubvoxelsPerMetre;
|
||||||
|
constexpr std::int64_t MaximumMirrorAmplitude = 2 * SurfaceProfileSubvoxelsPerMetre;
|
||||||
|
constexpr std::int64_t MinimumWavelength = 80 * SurfaceProfileSubvoxelsPerMetre;
|
||||||
|
constexpr std::int64_t MaximumWavelength = 160 * SurfaceProfileSubvoxelsPerMetre;
|
||||||
|
const bool hasArenaClearance = profile.minimumArenaClearanceSubvoxels == 0
|
||||||
|
|| profile.minimumArenaClearanceSubvoxels >= SurfaceProfileFightClearanceSubvoxels;
|
||||||
|
if (!hasArenaClearance)
|
||||||
|
{ error = "SurfaceWorld arena clearance must be omitted or at least eight metres"; return false; }
|
||||||
|
|
||||||
|
if (profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills
|
||||||
|
|| profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
||||||
|
{
|
||||||
|
const std::int64_t maximumAmplitude =
|
||||||
|
profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills
|
||||||
|
? MaximumPeriodicAmplitude : MaximumMirrorAmplitude;
|
||||||
|
if (profile.amplitudeXSubvoxels < MinimumPeriodAmplitude
|
||||||
|
|| profile.amplitudeYSubvoxels < MinimumPeriodAmplitude
|
||||||
|
|| profile.amplitudeXSubvoxels > maximumAmplitude
|
||||||
|
|| profile.amplitudeYSubvoxels > maximumAmplitude
|
||||||
|
|| profile.wavelengthXSubvoxels < MinimumWavelength
|
||||||
|
|| profile.wavelengthYSubvoxels < MinimumWavelength
|
||||||
|
|| profile.wavelengthXSubvoxels > MaximumWavelength
|
||||||
|
|| profile.wavelengthYSubvoxels > MaximumWavelength)
|
||||||
|
{ error = "periodic SurfaceWorld amplitudes or wavelengths are outside the R20 metre ranges"; return false; }
|
||||||
|
|
||||||
|
// Round every gradient component upward before applying the 15-degree bound.
|
||||||
|
// This makes the integer plan-time check conservative around division boundaries.
|
||||||
|
constexpr std::int64_t TwoPiQ16Ceiling = 411775;
|
||||||
|
constexpr std::int64_t Tan15Q16Floor = 17560;
|
||||||
|
const auto component = [](std::int64_t amplitude, std::int64_t wavelength)
|
||||||
|
{
|
||||||
|
const std::int64_t numerator = TwoPiQ16Ceiling * amplitude;
|
||||||
|
return (numerator + wavelength - 1) / wavelength;
|
||||||
|
};
|
||||||
|
const std::int64_t gx = component(profile.amplitudeXSubvoxels,
|
||||||
|
profile.wavelengthXSubvoxels);
|
||||||
|
const std::int64_t gy = component(profile.amplitudeYSubvoxels,
|
||||||
|
profile.wavelengthYSubvoxels);
|
||||||
|
if (gx * gx + gy * gy > Tan15Q16Floor * Tan15Q16Floor)
|
||||||
|
{ error = "periodic SurfaceWorld maximum walk grade exceeds tan(15 degrees)"; return false; }
|
||||||
|
|
||||||
|
if (profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills)
|
||||||
|
{
|
||||||
|
if (profile.mirrorLambdaQ16 != 0
|
||||||
|
|| profile.phaseXTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 0)
|
||||||
|
|| profile.phaseYTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 1))
|
||||||
|
{ error = "periodic-hill phases must be the stable seed-derived Q0.32 phases"; return false; }
|
||||||
|
}
|
||||||
|
else if (profile.mirrorLambdaQ16 < 49152 || profile.mirrorLambdaQ16 > Q16
|
||||||
|
|| profile.phaseXTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 0)
|
||||||
|
|| profile.phaseYTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 1))
|
||||||
|
{ error = "mirrored-relief lambda or seeded phases are outside their supported range"; return false; }
|
||||||
|
}
|
||||||
|
else if (profile.kind == WorldAssemblySurfaceProfileKindV1::flatField)
|
||||||
|
{
|
||||||
|
if (profile.amplitudeXSubvoxels != 0 || profile.amplitudeYSubvoxels != 0
|
||||||
|
|| profile.wavelengthXSubvoxels != 0 || profile.wavelengthYSubvoxels != 0
|
||||||
|
|| profile.phaseXTurnsQ32 != 0 || profile.phaseYTurnsQ32 != 0
|
||||||
|
|| profile.mirrorLambdaQ16 != 0)
|
||||||
|
{ error = "flatField must not carry periodic or mirrored relief parameters"; return false; }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ error = "SurfaceWorld profile kind is unsupported"; return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void HashSurfaceProfile(std::uint64_t& hash,
|
||||||
|
const WorldAssemblySurfaceProfileV1& profile)
|
||||||
|
{
|
||||||
|
HashWord(hash, 0x5355524650524f31ull, 8);
|
||||||
|
HashWord(hash, profile.version, 4);
|
||||||
|
HashWord(hash, static_cast<std::uint32_t>(profile.kind), 4);
|
||||||
|
HashWord(hash, static_cast<std::uint64_t>(profile.h0Subvoxels), 8);
|
||||||
|
HashWord(hash, static_cast<std::uint64_t>(profile.amplitudeXSubvoxels), 8);
|
||||||
|
HashWord(hash, static_cast<std::uint64_t>(profile.amplitudeYSubvoxels), 8);
|
||||||
|
HashWord(hash, static_cast<std::uint64_t>(profile.wavelengthXSubvoxels), 8);
|
||||||
|
HashWord(hash, static_cast<std::uint64_t>(profile.wavelengthYSubvoxels), 8);
|
||||||
|
HashWord(hash, profile.phaseXTurnsQ32, 4);
|
||||||
|
HashWord(hash, profile.phaseYTurnsQ32, 4);
|
||||||
|
HashWord(hash, static_cast<std::uint64_t>(profile.mirrorLambdaQ16), 8);
|
||||||
|
HashWord(hash, static_cast<std::uint64_t>(profile.minimumArenaClearanceSubvoxels), 8);
|
||||||
|
}
|
||||||
|
|
||||||
inline bool ValidateRecipe(const WorldAssemblyRecipeV1& recipe, std::string& error)
|
inline bool ValidateRecipe(const WorldAssemblyRecipeV1& recipe, std::string& error)
|
||||||
{
|
{
|
||||||
if (recipe.schemaVersion != WorldAssemblySchemaVersion
|
if (recipe.schemaVersion != WorldAssemblySchemaVersion
|
||||||
@@ -296,16 +410,57 @@ namespace world_assembly_detail
|
|||||||
|| recipe.biomeClimateMoistureWavelengthSubvoxels <= 0
|
|| recipe.biomeClimateMoistureWavelengthSubvoxels <= 0
|
||||||
|| recipe.biomeClimateReliefContrastQ16 < 0))
|
|| recipe.biomeClimateReliefContrastQ16 < 0))
|
||||||
{ error = "world assembly climate parameters are outside the supported fixed-scale range"; return false; }
|
{ error = "world assembly climate parameters are outside the supported fixed-scale range"; return false; }
|
||||||
|
if (!ValidateSurfaceProfile(recipe.surfaceProfile, recipe.seed, error)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool ValidateSurfaceProfileGeometry(const WorldAssemblySurfaceProfileV1& profile,
|
||||||
|
const WorldAssemblyStratumPlanV1& stratum, std::int64_t layoutChunkWidthSubvoxels,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
if (profile.version == 0) return true;
|
||||||
|
const std::int64_t relief = profile.amplitudeXSubvoxels + profile.amplitudeYSubvoxels;
|
||||||
|
const std::int64_t groundMinimum = profile.h0Subvoxels - relief - 2;
|
||||||
|
const std::int64_t groundMaximum = profile.h0Subvoxels + relief + 2;
|
||||||
|
std::int64_t capSwing = 0;
|
||||||
|
if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
||||||
|
{
|
||||||
|
const std::int64_t product = relief * profile.mirrorLambdaQ16;
|
||||||
|
capSwing = (product + Q16 - 1) / Q16 + 2;
|
||||||
|
}
|
||||||
|
const std::int64_t capMinimum = stratum.capHeightSubvoxels - capSwing;
|
||||||
|
const std::int64_t capMaximum = stratum.capHeightSubvoxels + capSwing;
|
||||||
|
const std::int64_t clearance = capMinimum - groundMaximum;
|
||||||
|
const std::int64_t requiredClearance = std::max(
|
||||||
|
SurfaceProfileMinimumClearanceSubvoxels,
|
||||||
|
profile.minimumArenaClearanceSubvoxels);
|
||||||
|
if (clearance < requiredClearance)
|
||||||
|
{ error = "SurfaceWorld profile does not preserve its required floor-to-cap clearance"; return false; }
|
||||||
|
|
||||||
|
const std::int64_t stratumMinimum = static_cast<std::int64_t>(stratum.bottomChunkZ)
|
||||||
|
* layoutChunkWidthSubvoxels;
|
||||||
|
const std::int64_t stratumMaximum = (static_cast<std::int64_t>(stratum.topChunkZ) + 1)
|
||||||
|
* layoutChunkWidthSubvoxels;
|
||||||
|
if (groundMinimum <= stratumMinimum || capMaximum >= stratumMaximum)
|
||||||
|
{ error = "SurfaceWorld profile escapes its finite stratum Z bounds"; return false; }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool ValidateStratum(const WorldAssemblyStratumPlanV1& stratum,
|
inline bool ValidateStratum(const WorldAssemblyStratumPlanV1& stratum,
|
||||||
std::int64_t layoutChunkWidthSubvoxels, std::string& error)
|
const WorldAssemblyRecipeV1& recipe, std::string& error)
|
||||||
{
|
{
|
||||||
|
const std::int64_t layoutChunkWidthSubvoxels = recipe.layoutChunkWidthSubvoxels;
|
||||||
if (stratum.strateIndex < 0 || stratum.topChunkZ < stratum.bottomChunkZ
|
if (stratum.strateIndex < 0 || stratum.topChunkZ < stratum.bottomChunkZ
|
||||||
|| stratum.capWavelengthSubvoxels <= 0 || stratum.capUndulationSubvoxels < 0
|
|| stratum.capWavelengthSubvoxels <= 0 || stratum.capUndulationSubvoxels < 0
|
||||||
|| stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount)
|
|| stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount)
|
||||||
{ error = "world assembly stratum plan has an invalid extent or exceeds its bounded payload"; return false; }
|
{ error = "world assembly stratum plan has an invalid extent or exceeds its bounded payload"; return false; }
|
||||||
|
const WorldAssemblyRecipeV1& effectiveRecipe = stratum.hasRecipeOverride
|
||||||
|
? stratum.recipeOverride : recipe;
|
||||||
|
if (!ValidateSurfaceProfile(stratum.surfaceProfile, effectiveRecipe.seed, error)) return false;
|
||||||
|
if (stratum.surfaceProfile != effectiveRecipe.surfaceProfile)
|
||||||
|
{ error = "SurfaceWorld recipe and stratum plan profile payloads do not match"; return false; }
|
||||||
|
if (!ValidateSurfaceProfileGeometry(stratum.surfaceProfile, stratum,
|
||||||
|
layoutChunkWidthSubvoxels, error)) return false;
|
||||||
if (!ValidScalar(stratum.capHeightSubvoxels) || !ValidScalar(stratum.capUndulationSubvoxels)
|
if (!ValidScalar(stratum.capHeightSubvoxels) || !ValidScalar(stratum.capUndulationSubvoxels)
|
||||||
|| !ValidScalar(stratum.capWavelengthSubvoxels)
|
|| !ValidScalar(stratum.capWavelengthSubvoxels)
|
||||||
|| !ValidScalar(stratum.capDetailAmplitudeSubvoxels)
|
|| !ValidScalar(stratum.capDetailAmplitudeSubvoxels)
|
||||||
@@ -466,6 +621,8 @@ namespace world_assembly_detail
|
|||||||
HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (recipe.surfaceProfile.version != 0)
|
||||||
|
HashSurfaceProfile(hash, recipe.surfaceProfile);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,6 +682,8 @@ namespace world_assembly_detail
|
|||||||
for (const auto value : point.positionSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
for (const auto value : point.positionSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(point.radiusSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(point.radiusSubvoxels), 8);
|
||||||
}
|
}
|
||||||
|
if (stratum.surfaceProfile.version != 0)
|
||||||
|
HashSurfaceProfile(hash, stratum.surfaceProfile);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,6 +786,59 @@ namespace world_assembly_detail
|
|||||||
return out.str();
|
return out.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline const char* SurfaceProfileKindName(WorldAssemblySurfaceProfileKindV1 kind)
|
||||||
|
{
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case WorldAssemblySurfaceProfileKindV1::natural: return "natural";
|
||||||
|
case WorldAssemblySurfaceProfileKindV1::periodicHills: return "periodicHills";
|
||||||
|
case WorldAssemblySurfaceProfileKindV1::mirroredRelief: return "mirroredRelief";
|
||||||
|
case WorldAssemblySurfaceProfileKindV1::flatField: return "flatField";
|
||||||
|
}
|
||||||
|
return "unsupported";
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool ParseSurfaceProfile(const VFJson::Value& value,
|
||||||
|
WorldAssemblySurfaceProfileV1& profile)
|
||||||
|
{
|
||||||
|
if (!value.IsObject() || !ReadU32(value.Get("version"), profile.version)) return false;
|
||||||
|
const auto* kind = std::get_if<std::string>(&value.Get("kind").Data);
|
||||||
|
if (kind == nullptr) return false;
|
||||||
|
if (*kind == "periodicHills") profile.kind = WorldAssemblySurfaceProfileKindV1::periodicHills;
|
||||||
|
else if (*kind == "mirroredRelief") profile.kind = WorldAssemblySurfaceProfileKindV1::mirroredRelief;
|
||||||
|
else if (*kind == "flatField") profile.kind = WorldAssemblySurfaceProfileKindV1::flatField;
|
||||||
|
else if (*kind == "natural") profile.kind = WorldAssemblySurfaceProfileKindV1::natural;
|
||||||
|
else return false;
|
||||||
|
return ReadInteger(value.Get("h0_q"), profile.h0Subvoxels)
|
||||||
|
&& ReadInteger(value.Get("amplitude_x_q"), profile.amplitudeXSubvoxels)
|
||||||
|
&& ReadInteger(value.Get("amplitude_y_q"), profile.amplitudeYSubvoxels)
|
||||||
|
&& ReadInteger(value.Get("wavelength_x_q"), profile.wavelengthXSubvoxels)
|
||||||
|
&& ReadInteger(value.Get("wavelength_y_q"), profile.wavelengthYSubvoxels)
|
||||||
|
&& ReadU32(value.Get("phase_x_turns_q32"), profile.phaseXTurnsQ32)
|
||||||
|
&& ReadU32(value.Get("phase_y_turns_q32"), profile.phaseYTurnsQ32)
|
||||||
|
&& ReadInteger(value.Get("mirror_lambda_q16"), profile.mirrorLambdaQ16)
|
||||||
|
&& ReadInteger(value.Get("minimum_arena_clearance_q"),
|
||||||
|
profile.minimumArenaClearanceSubvoxels);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string SurfaceProfileJson(const WorldAssemblySurfaceProfileV1& profile)
|
||||||
|
{
|
||||||
|
std::ostringstream out;
|
||||||
|
out << "{\"version\":" << profile.version
|
||||||
|
<< ",\"kind\":\"" << SurfaceProfileKindName(profile.kind) << "\""
|
||||||
|
<< ",\"h0_q\":" << profile.h0Subvoxels
|
||||||
|
<< ",\"amplitude_x_q\":" << profile.amplitudeXSubvoxels
|
||||||
|
<< ",\"amplitude_y_q\":" << profile.amplitudeYSubvoxels
|
||||||
|
<< ",\"wavelength_x_q\":" << profile.wavelengthXSubvoxels
|
||||||
|
<< ",\"wavelength_y_q\":" << profile.wavelengthYSubvoxels
|
||||||
|
<< ",\"phase_x_turns_q32\":" << profile.phaseXTurnsQ32
|
||||||
|
<< ",\"phase_y_turns_q32\":" << profile.phaseYTurnsQ32
|
||||||
|
<< ",\"mirror_lambda_q16\":" << profile.mirrorLambdaQ16
|
||||||
|
<< ",\"minimum_arena_clearance_q\":"
|
||||||
|
<< profile.minimumArenaClearanceSubvoxels << '}';
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
inline bool ParseRecipe(const VFJson::Value& value, WorldAssemblyRecipeV1& recipe,
|
inline bool ParseRecipe(const VFJson::Value& value, WorldAssemblyRecipeV1& recipe,
|
||||||
std::string& error)
|
std::string& error)
|
||||||
{
|
{
|
||||||
@@ -647,6 +859,10 @@ namespace world_assembly_detail
|
|||||||
|| !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels)
|
|| !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels)
|
||||||
|| !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16)))
|
|| !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16)))
|
||||||
{ error = "world assembly recipe is missing evaluator-2 climate parameters"; return false; }
|
{ error = "world assembly recipe is missing evaluator-2 climate parameters"; return false; }
|
||||||
|
const VFJson::Value& surfaceProfileValue = value.Get("surface_profile");
|
||||||
|
if (!std::holds_alternative<std::nullptr_t>(surfaceProfileValue.Data)
|
||||||
|
&& !ParseSurfaceProfile(surfaceProfileValue, recipe.surfaceProfile))
|
||||||
|
{ error = "world assembly recipe has a malformed versioned SurfaceWorld profile"; return false; }
|
||||||
for (const auto& item : value.Get("biomes").AsArray())
|
for (const auto& item : value.Get("biomes").AsArray())
|
||||||
{
|
{
|
||||||
WorldAssemblyBiomeProfileV1 biome;
|
WorldAssemblyBiomeProfileV1 biome;
|
||||||
@@ -709,6 +925,8 @@ namespace world_assembly_detail
|
|||||||
out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels
|
out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels
|
||||||
<< ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels
|
<< ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels
|
||||||
<< ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16;
|
<< ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16;
|
||||||
|
if (recipe.surfaceProfile.version != 0)
|
||||||
|
out << ",\"surface_profile\":" << SurfaceProfileJson(recipe.surfaceProfile);
|
||||||
out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":[";
|
out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":[";
|
||||||
for (std::size_t i = 0; i < recipe.biomes.size(); ++i)
|
for (std::size_t i = 0; i < recipe.biomes.size(); ++i)
|
||||||
{
|
{
|
||||||
@@ -797,6 +1015,10 @@ namespace world_assembly_detail
|
|||||||
{ error = "world assembly stratum has malformed optional evaluator-2 fields"; return false; }
|
{ error = "world assembly stratum has malformed optional evaluator-2 fields"; return false; }
|
||||||
if(hasWaterBoolean)stratum.waterEnabled=waterEnabled.Boolean(false);
|
if(hasWaterBoolean)stratum.waterEnabled=waterEnabled.Boolean(false);
|
||||||
if(hasWaterNumber)stratum.waterEnabled=waterEnabled.Number()!=0.0;
|
if(hasWaterNumber)stratum.waterEnabled=waterEnabled.Number()!=0.0;
|
||||||
|
const VFJson::Value& surfaceProfileValue = item.Get("surface_profile");
|
||||||
|
if (!std::holds_alternative<std::nullptr_t>(surfaceProfileValue.Data)
|
||||||
|
&& !ParseSurfaceProfile(surfaceProfileValue, stratum.surfaceProfile))
|
||||||
|
{ error = "world assembly stratum has a malformed versioned SurfaceWorld profile"; return false; }
|
||||||
const VFJson::Value& routeConstraintsValue = item.Get("route_constraints");
|
const VFJson::Value& routeConstraintsValue = item.Get("route_constraints");
|
||||||
if (!std::holds_alternative<std::nullptr_t>(routeConstraintsValue.Data))
|
if (!std::holds_alternative<std::nullptr_t>(routeConstraintsValue.Data))
|
||||||
{
|
{
|
||||||
@@ -1367,6 +1589,196 @@ namespace world_assembly_detail
|
|||||||
return SmoothQ16(static_cast<std::uint32_t>(std::clamp<std::int64_t>(edge*Q16/feather,0,Q16)));
|
return SmoothQ16(static_cast<std::uint32_t>(std::clamp<std::int64_t>(edge*Q16/feather,0,Q16)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr std::int64_t Q30 = 1ll << 30;
|
||||||
|
constexpr std::array<std::int64_t, 30> CordicAtanTurnsQ32{{
|
||||||
|
536870912,316933406,167458907,85004756,42667331,21354465,10679838,5340245,
|
||||||
|
2670163,1335087,667544,333772,166886,83443,41722,20861,10430,5215,2608,
|
||||||
|
1304,652,326,163,81,41,20,10,5,3,1}};
|
||||||
|
|
||||||
|
inline std::array<std::int64_t, 2> CordicSinCosQuarterQ30(std::uint32_t angleQ32)
|
||||||
|
{
|
||||||
|
if (angleQ32 == 0) return {Q30, 0};
|
||||||
|
std::int64_t x = 652032874, y = 0;
|
||||||
|
std::int64_t z = angleQ32;
|
||||||
|
for (std::size_t i = 0; i < CordicAtanTurnsQ32.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::int64_t oldX = x;
|
||||||
|
if (z >= 0)
|
||||||
|
{
|
||||||
|
x -= y >> i;
|
||||||
|
y += oldX >> i;
|
||||||
|
z -= CordicAtanTurnsQ32[i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
x += y >> i;
|
||||||
|
y -= oldX >> i;
|
||||||
|
z += CordicAtanTurnsQ32[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {std::clamp<std::int64_t>(x, -Q30, Q30),
|
||||||
|
std::clamp<std::int64_t>(y, -Q30, Q30)};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::int64_t CosineQ30(std::uint32_t phaseTurnsQ32)
|
||||||
|
{
|
||||||
|
constexpr std::uint32_t Quarter = 1u << 30;
|
||||||
|
if (phaseTurnsQ32 == 0) return Q30;
|
||||||
|
if (phaseTurnsQ32 == Quarter) return 0;
|
||||||
|
if (phaseTurnsQ32 == 2u * Quarter) return -Q30;
|
||||||
|
if (phaseTurnsQ32 == 3u * Quarter) return 0;
|
||||||
|
constexpr std::uint32_t TableBits = 16;
|
||||||
|
constexpr std::uint32_t TableSize = 1u << TableBits;
|
||||||
|
constexpr std::uint32_t FractionMask = (1u << (32 - TableBits)) - 1u;
|
||||||
|
static const std::array<std::int32_t, TableSize> table = []
|
||||||
|
{
|
||||||
|
std::array<std::int32_t, TableSize> values{};
|
||||||
|
constexpr std::uint32_t QuarterTableSize = TableSize / 4;
|
||||||
|
constexpr std::uint32_t QuarterStepQ32 = 1u << (30 - (TableBits - 2));
|
||||||
|
std::array<std::int32_t, QuarterTableSize + 1> quarter{};
|
||||||
|
for (std::uint32_t offset = 0; offset <= QuarterTableSize; ++offset)
|
||||||
|
{
|
||||||
|
const std::int64_t cosine = CordicSinCosQuarterQ30(
|
||||||
|
offset * QuarterStepQ32)[0];
|
||||||
|
const std::int64_t monotone = offset == 0 ? cosine
|
||||||
|
: std::min<std::int64_t>(quarter[offset - 1], cosine);
|
||||||
|
quarter[offset] = static_cast<std::int32_t>(monotone);
|
||||||
|
}
|
||||||
|
for (std::uint32_t index = 0; index < TableSize; ++index)
|
||||||
|
{
|
||||||
|
const std::uint32_t quadrant = index / QuarterTableSize;
|
||||||
|
const std::uint32_t offset = index % QuarterTableSize;
|
||||||
|
const std::uint32_t mirroredOffset = QuarterTableSize - offset;
|
||||||
|
const std::int32_t value = quadrant == 0 ? quarter[offset]
|
||||||
|
: quadrant == 1 ? -quarter[mirroredOffset]
|
||||||
|
: quadrant == 2 ? -quarter[offset] : quarter[mirroredOffset];
|
||||||
|
values[index] = value;
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}();
|
||||||
|
const std::uint32_t index = phaseTurnsQ32 >> (32 - TableBits);
|
||||||
|
const std::uint32_t fraction = phaseTurnsQ32 & FractionMask;
|
||||||
|
const std::int64_t first = table[index];
|
||||||
|
const std::int64_t second = table[(index + 1u) & (TableSize - 1u)];
|
||||||
|
const std::int64_t numerator = (second - first) * fraction;
|
||||||
|
const std::int64_t rounded = numerator >= 0
|
||||||
|
? numerator + (1ll << (31 - TableBits))
|
||||||
|
: numerator - (1ll << (31 - TableBits));
|
||||||
|
// The fixed-point table interpolation is monotone within each quadrant;
|
||||||
|
// cosine interval bounds therefore still attain extrema at their endpoints
|
||||||
|
// or at the exact quarter-turn extrema checked below.
|
||||||
|
return first + rounded / (1ll << (32 - TableBits));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::uint32_t SurfaceProfileTurnAtCoordinate(std::int64_t coordinate,
|
||||||
|
std::int64_t wavelength, std::uint32_t phaseTurnsQ32)
|
||||||
|
{
|
||||||
|
const std::uint64_t coordinateTurns = static_cast<std::uint64_t>(
|
||||||
|
FloorMod(coordinate, wavelength)) * static_cast<std::uint64_t>(Q32)
|
||||||
|
/ static_cast<std::uint64_t>(wavelength);
|
||||||
|
return phaseTurnsQ32 + static_cast<std::uint32_t>(coordinateTurns);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CosineRangeQ30
|
||||||
|
{
|
||||||
|
std::int64_t minimum = -Q30;
|
||||||
|
std::int64_t maximum = Q30;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline CosineRangeQ30 SurfaceProfileCosineRangeQ30(std::int64_t minimumCoordinate,
|
||||||
|
std::int64_t maximumCoordinate, std::int64_t wavelength,
|
||||||
|
std::uint32_t phaseTurnsQ32)
|
||||||
|
{
|
||||||
|
if (wavelength <= 0 || minimumCoordinate > maximumCoordinate
|
||||||
|
|| maximumCoordinate - minimumCoordinate >= wavelength)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
CosineRangeQ30 range;
|
||||||
|
const std::int64_t cosineAtMinimum = CosineQ30(
|
||||||
|
SurfaceProfileTurnAtCoordinate(minimumCoordinate, wavelength, phaseTurnsQ32));
|
||||||
|
const std::int64_t cosineAtMaximum = CosineQ30(
|
||||||
|
SurfaceProfileTurnAtCoordinate(maximumCoordinate, wavelength, phaseTurnsQ32));
|
||||||
|
range.minimum = std::min(cosineAtMinimum, cosineAtMaximum);
|
||||||
|
range.maximum = std::max(cosineAtMinimum, cosineAtMaximum);
|
||||||
|
|
||||||
|
// Coordinate turns are Q0.32. Test the exact analytic extrema in a common
|
||||||
|
// rational numerator domain; one Q0.32 unit of endpoint slack accounts for
|
||||||
|
// the fixed phase conversion used by point().
|
||||||
|
const std::uint64_t periodNumerator = static_cast<std::uint64_t>(wavelength)
|
||||||
|
* static_cast<std::uint64_t>(Q32);
|
||||||
|
const std::uint64_t startNumerator = (
|
||||||
|
static_cast<std::uint64_t>(FloorMod(minimumCoordinate, wavelength))
|
||||||
|
* static_cast<std::uint64_t>(Q32)
|
||||||
|
+ static_cast<std::uint64_t>(phaseTurnsQ32)
|
||||||
|
* static_cast<std::uint64_t>(wavelength)) % periodNumerator;
|
||||||
|
const std::uint64_t spanNumerator = static_cast<std::uint64_t>(
|
||||||
|
maximumCoordinate - minimumCoordinate) * static_cast<std::uint64_t>(Q32);
|
||||||
|
const auto includesExtremum = [&](std::uint64_t target)
|
||||||
|
{
|
||||||
|
const std::uint64_t forward = target >= startNumerator
|
||||||
|
? target - startNumerator : periodNumerator - startNumerator + target;
|
||||||
|
const std::uint64_t backward = periodNumerator - forward;
|
||||||
|
return forward <= spanNumerator + wavelength
|
||||||
|
|| backward <= wavelength;
|
||||||
|
};
|
||||||
|
if (includesExtremum(0)) range.maximum = Q30;
|
||||||
|
if (includesExtremum(periodNumerator / 2)) range.minimum = -Q30;
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::int64_t MulQ30(std::int64_t value, std::int64_t cosineQ30)
|
||||||
|
{ return (value * cosineQ30) / Q30; }
|
||||||
|
|
||||||
|
struct SurfaceProfileColumnBounds
|
||||||
|
{
|
||||||
|
std::int64_t groundMinimum = 0;
|
||||||
|
std::int64_t groundMaximum = 0;
|
||||||
|
std::int64_t capMinimum = 0;
|
||||||
|
std::int64_t capMaximum = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline SurfaceProfileColumnBounds ProfileColumnBounds(
|
||||||
|
const WorldAssemblySurfaceProfileV1& profile,
|
||||||
|
const WorldAssemblyStratumPlanV1& plan,
|
||||||
|
std::int64_t minimumX, std::int64_t minimumY,
|
||||||
|
std::int64_t maximumX, std::int64_t maximumY)
|
||||||
|
{
|
||||||
|
SurfaceProfileColumnBounds bounds;
|
||||||
|
if (profile.kind == WorldAssemblySurfaceProfileKindV1::flatField)
|
||||||
|
{
|
||||||
|
bounds.groundMinimum = profile.h0Subvoxels;
|
||||||
|
bounds.groundMaximum = profile.h0Subvoxels;
|
||||||
|
bounds.capMinimum = plan.capHeightSubvoxels;
|
||||||
|
bounds.capMaximum = plan.capHeightSubvoxels;
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
const CosineRangeQ30 x = SurfaceProfileCosineRangeQ30(minimumX, maximumX,
|
||||||
|
profile.wavelengthXSubvoxels, profile.phaseXTurnsQ32);
|
||||||
|
const CosineRangeQ30 y = SurfaceProfileCosineRangeQ30(minimumY, maximumY,
|
||||||
|
profile.wavelengthYSubvoxels, profile.phaseYTurnsQ32);
|
||||||
|
const std::int64_t groundXMinimum = MulQ30(profile.amplitudeXSubvoxels, x.minimum);
|
||||||
|
const std::int64_t groundXMaximum = MulQ30(profile.amplitudeXSubvoxels, x.maximum);
|
||||||
|
const std::int64_t groundYMinimum = MulQ30(profile.amplitudeYSubvoxels, y.minimum);
|
||||||
|
const std::int64_t groundYMaximum = MulQ30(profile.amplitudeYSubvoxels, y.maximum);
|
||||||
|
bounds.groundMinimum = profile.h0Subvoxels + groundXMinimum + groundYMinimum - 2;
|
||||||
|
bounds.groundMaximum = profile.h0Subvoxels + groundXMaximum + groundYMaximum + 2;
|
||||||
|
if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
||||||
|
{
|
||||||
|
const std::int64_t reliefMinimum = groundXMinimum + groundYMinimum;
|
||||||
|
const std::int64_t reliefMaximum = groundXMaximum + groundYMaximum;
|
||||||
|
bounds.capMinimum = plan.capHeightSubvoxels
|
||||||
|
- MulQ16(reliefMaximum, profile.mirrorLambdaQ16) - 2;
|
||||||
|
bounds.capMaximum = plan.capHeightSubvoxels
|
||||||
|
- MulQ16(reliefMinimum, profile.mirrorLambdaQ16) + 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bounds.capMinimum = plan.capHeightSubvoxels;
|
||||||
|
bounds.capMaximum = plan.capHeightSubvoxels;
|
||||||
|
}
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
struct HeightfieldColumnCacheEntry
|
struct HeightfieldColumnCacheEntry
|
||||||
{
|
{
|
||||||
const WorldAssemblyRecipeV1* recipe = nullptr;
|
const WorldAssemblyRecipeV1* recipe = nullptr;
|
||||||
@@ -1415,6 +1827,61 @@ namespace world_assembly_detail
|
|||||||
HeightfieldPointSample out;
|
HeightfieldPointSample out;
|
||||||
if(recipe.evaluatorVersion>=2)
|
if(recipe.evaluatorVersion>=2)
|
||||||
{
|
{
|
||||||
|
if (recipe.surfaceProfile.version == 1)
|
||||||
|
{
|
||||||
|
const auto& profile = recipe.surfaceProfile;
|
||||||
|
std::int64_t relief = 0;
|
||||||
|
if (profile.kind != WorldAssemblySurfaceProfileKindV1::flatField)
|
||||||
|
{
|
||||||
|
const std::int64_t cosineX = CosineQ30(SurfaceProfileTurnAtCoordinate(
|
||||||
|
x, profile.wavelengthXSubvoxels, profile.phaseXTurnsQ32));
|
||||||
|
const std::int64_t cosineY = CosineQ30(SurfaceProfileTurnAtCoordinate(
|
||||||
|
y, profile.wavelengthYSubvoxels, profile.phaseYTurnsQ32));
|
||||||
|
relief = MulQ30(profile.amplitudeXSubvoxels, cosineX)
|
||||||
|
+ MulQ30(profile.amplitudeYSubvoxels, cosineY);
|
||||||
|
}
|
||||||
|
out.groundHeightSubvoxels = profile.h0Subvoxels + relief;
|
||||||
|
out.capHeightSubvoxels = profile.kind
|
||||||
|
== WorldAssemblySurfaceProfileKindV1::mirroredRelief
|
||||||
|
? plan.capHeightSubvoxels - MulQ16(relief, profile.mirrorLambdaQ16)
|
||||||
|
: plan.capHeightSubvoxels;
|
||||||
|
out.biomeId = recipe.biomes.front().stableId;
|
||||||
|
out.neighborBiomeId = out.biomeId;
|
||||||
|
out.biomeWeightQ16 = static_cast<std::uint32_t>(Q16);
|
||||||
|
const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find(
|
||||||
|
&recipe, &plan, x, y, 0, true);
|
||||||
|
for (const auto& feature : FeatureRange{plan.features, list})
|
||||||
|
{
|
||||||
|
if (feature.type == 1)
|
||||||
|
{
|
||||||
|
const std::uint32_t fade = FeatureFadeQ16(feature, x, y,
|
||||||
|
feature.parametersSubvoxels[1]);
|
||||||
|
if (fade) out.groundHeightSubvoxels += MulQ16(
|
||||||
|
feature.parametersSubvoxels[0], fade);
|
||||||
|
}
|
||||||
|
else if (feature.type == 3)
|
||||||
|
{
|
||||||
|
const std::uint32_t fade = FeatureFadeQ16(feature, x, y,
|
||||||
|
feature.parametersSubvoxels[2]);
|
||||||
|
const std::int64_t wave = std::max<std::int64_t>(
|
||||||
|
1, feature.parametersSubvoxels[1]);
|
||||||
|
if (fade)
|
||||||
|
{
|
||||||
|
const std::uint32_t featureSeed = static_cast<std::uint32_t>(feature.stableId);
|
||||||
|
const std::int64_t nx = NoiseCoordinateQ16(x, wave)
|
||||||
|
+ SeedOffsetQ16(featureSeed, 1700);
|
||||||
|
const std::int64_t ny = NoiseCoordinateQ16(y, wave)
|
||||||
|
+ SeedOffsetQ16(featureSeed, 2900);
|
||||||
|
const std::int32_t noise = FractalNoiseQ16(nx, ny,
|
||||||
|
SeedOffsetQ16(featureSeed, 4100), 3);
|
||||||
|
out.capHeightSubvoxels += MulQ16(MulQ16(noise,
|
||||||
|
feature.parametersSubvoxels[0]), fade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.waterSurfaceSubvoxels = plan.waterSurfaceSubvoxels;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
const BiomeSelectionV2 selection=SelectBiomeV2(recipe,x,y);
|
const BiomeSelectionV2 selection=SelectBiomeV2(recipe,x,y);
|
||||||
const auto& primary=recipe.biomes[selection.primary];
|
const auto& primary=recipe.biomes[selection.primary];
|
||||||
const auto& neighbor=recipe.biomes[selection.neighbor];
|
const auto& neighbor=recipe.biomes[selection.neighbor];
|
||||||
@@ -1742,6 +2209,16 @@ std::optional<WorldAssemblyManifest> WorldAssemblyManifest::FromJson(
|
|||||||
|
|
||||||
bool WorldAssemblyManifest::finalize(std::string& error)
|
bool WorldAssemblyManifest::finalize(std::string& error)
|
||||||
{
|
{
|
||||||
|
ResolveWorldAssemblySurfaceProfilePhasesV1(recipe.surfaceProfile, recipe.seed);
|
||||||
|
for (auto& stratum : plan.strates)
|
||||||
|
{
|
||||||
|
WorldAssemblyRecipeV1& effectiveRecipe = stratum.hasRecipeOverride
|
||||||
|
? stratum.recipeOverride : recipe;
|
||||||
|
ResolveWorldAssemblySurfaceProfilePhasesV1(effectiveRecipe.surfaceProfile,
|
||||||
|
effectiveRecipe.seed);
|
||||||
|
ResolveWorldAssemblySurfaceProfilePhasesV1(stratum.surfaceProfile,
|
||||||
|
effectiveRecipe.seed);
|
||||||
|
}
|
||||||
if (schemaVersion != WorldAssemblySchemaVersion || plan.schemaVersion != WorldAssemblySchemaVersion
|
if (schemaVersion != WorldAssemblySchemaVersion || plan.schemaVersion != WorldAssemblySchemaVersion
|
||||||
|| !world_assembly_detail::ValidateRecipe(recipe, error))
|
|| !world_assembly_detail::ValidateRecipe(recipe, error))
|
||||||
{ if (error.empty()) error = "world assembly manifest or plan version is unsupported"; return false; }
|
{ if (error.empty()) error = "world assembly manifest or plan version is unsupported"; return false; }
|
||||||
@@ -1759,7 +2236,7 @@ bool WorldAssemblyManifest::finalize(std::string& error)
|
|||||||
{ error = "SurfaceWorld stratum recipe seed does not match the world assembly season seed"; return false; }
|
{ error = "SurfaceWorld stratum recipe seed does not match the world assembly season seed"; return false; }
|
||||||
stratum.recipeOverride.contentHash = world_assembly_detail::HashRecipe(stratum.recipeOverride);
|
stratum.recipeOverride.contentHash = world_assembly_detail::HashRecipe(stratum.recipeOverride);
|
||||||
}
|
}
|
||||||
if (!world_assembly_detail::ValidateStratum(stratum, recipe.layoutChunkWidthSubvoxels, error)) return false;
|
if (!world_assembly_detail::ValidateStratum(stratum, recipe, error)) return false;
|
||||||
if (recipe.evaluatorVersion >= 4)
|
if (recipe.evaluatorVersion >= 4)
|
||||||
{
|
{
|
||||||
if (stratum.routeConstraints.version != 1 || stratum.route.size() < 2)
|
if (stratum.routeConstraints.version != 1 || stratum.route.size() < 2)
|
||||||
@@ -1882,6 +2359,9 @@ std::string WorldAssemblyManifest::ToJson() const
|
|||||||
if (stratum.hasRecipeOverride)
|
if (stratum.hasRecipeOverride)
|
||||||
out << ",\"recipe_override\":"
|
out << ",\"recipe_override\":"
|
||||||
<< world_assembly_detail::RecipeJson(stratum.recipeOverride);
|
<< world_assembly_detail::RecipeJson(stratum.recipeOverride);
|
||||||
|
if (stratum.surfaceProfile.version != 0)
|
||||||
|
out << ",\"surface_profile\":"
|
||||||
|
<< world_assembly_detail::SurfaceProfileJson(stratum.surfaceProfile);
|
||||||
out
|
out
|
||||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash)
|
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash)
|
||||||
<< "\",\"features\":[";
|
<< "\",\"features\":[";
|
||||||
@@ -1957,6 +2437,8 @@ bool Heightfield2D::isValid() const noexcept
|
|||||||
|| recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount
|
|| recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount
|
||||||
|| recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0
|
|| recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0
|
||||||
|| recipe_->biomeWarpSubvoxels < 0 || recipe_->biomeBorderBlendSubvoxels < 0
|
|| recipe_->biomeWarpSubvoxels < 0 || recipe_->biomeBorderBlendSubvoxels < 0
|
||||||
|
|| recipe_->surfaceProfile != plan_->surfaceProfile
|
||||||
|
|| recipe_->surfaceProfile.version > 1
|
||||||
|| plan_->strateIndex < 0 || plan_->topChunkZ < plan_->bottomChunkZ
|
|| plan_->strateIndex < 0 || plan_->topChunkZ < plan_->bottomChunkZ
|
||||||
|| plan_->capWavelengthSubvoxels <= 0 || plan_->capUndulationSubvoxels < 0)
|
|| plan_->capWavelengthSubvoxels <= 0 || plan_->capUndulationSubvoxels < 0)
|
||||||
return false;
|
return false;
|
||||||
@@ -1973,6 +2455,32 @@ bool Heightfield2D::isValid() const noexcept
|
|||||||
std::array<std::int64_t,2> Heightfield2D::capHeightBounds() const noexcept
|
std::array<std::int64_t,2> Heightfield2D::capHeightBounds() const noexcept
|
||||||
{
|
{
|
||||||
if(!isValid())return {0,0};
|
if(!isValid())return {0,0};
|
||||||
|
if (recipe_->surfaceProfile.version == 1)
|
||||||
|
{
|
||||||
|
const auto& profile = recipe_->surfaceProfile;
|
||||||
|
std::int64_t swing = 0;
|
||||||
|
if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
||||||
|
{
|
||||||
|
const std::int64_t relief = profile.amplitudeXSubvoxels
|
||||||
|
+ profile.amplitudeYSubvoxels;
|
||||||
|
swing = (relief * profile.mirrorLambdaQ16 + world_assembly_detail::Q16 - 1)
|
||||||
|
/ world_assembly_detail::Q16 + 2;
|
||||||
|
}
|
||||||
|
std::int64_t featureAmplitude = 0;
|
||||||
|
for (const auto& feature : plan_->features)
|
||||||
|
if (feature.type == 3)
|
||||||
|
featureAmplitude += std::abs(feature.parametersSubvoxels[0]);
|
||||||
|
std::int64_t minimum = plan_->capHeightSubvoxels - swing - featureAmplitude;
|
||||||
|
std::int64_t maximum = plan_->capHeightSubvoxels + swing + featureAmplitude;
|
||||||
|
if (recipe_->evaluatorVersion >= 5)
|
||||||
|
{
|
||||||
|
const std::int64_t upperBoundary = (static_cast<std::int64_t>(plan_->topChunkZ) + 1)
|
||||||
|
* recipe_->layoutChunkWidthSubvoxels;
|
||||||
|
minimum = std::min(minimum, upperBoundary);
|
||||||
|
maximum = std::min(maximum, upperBoundary);
|
||||||
|
}
|
||||||
|
return {minimum, maximum};
|
||||||
|
}
|
||||||
long double Minimum=std::numeric_limits<long double>::infinity();
|
long double Minimum=std::numeric_limits<long double>::infinity();
|
||||||
long double Maximum=-std::numeric_limits<long double>::infinity();
|
long double Maximum=-std::numeric_limits<long double>::infinity();
|
||||||
const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome)
|
const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome)
|
||||||
@@ -2145,6 +2653,9 @@ void Heightfield2D::setThreadColumnCaptureBounds(std::int64_t minimumXSubvoxels,
|
|||||||
void Heightfield2D::setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept
|
void Heightfield2D::setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept
|
||||||
{ world_assembly_detail::HeightfieldColumnCacheLegacyDirectMapped=enabled; }
|
{ world_assembly_detail::HeightfieldColumnCacheLegacyDirectMapped=enabled; }
|
||||||
|
|
||||||
|
bool Heightfield2D::threadColumnCacheLegacyDirectMapped() noexcept
|
||||||
|
{ return world_assembly_detail::HeightfieldColumnCacheLegacyDirectMapped; }
|
||||||
|
|
||||||
void Heightfield2D::setThreadColumnOctaveDrop(std::uint32_t octaveDrop) noexcept
|
void Heightfield2D::setThreadColumnOctaveDrop(std::uint32_t octaveDrop) noexcept
|
||||||
{ world_assembly_detail::HeightfieldColumnOctaveDrop=octaveDrop; }
|
{ world_assembly_detail::HeightfieldColumnOctaveDrop=octaveDrop; }
|
||||||
|
|
||||||
@@ -2178,6 +2689,15 @@ HeightfieldBoxClass Heightfield2D::classifyLatticeBox(std::int64_t minimumX,
|
|||||||
&&feature.minimumSubvoxels[2]<=maximumZ&&feature.maximumSubvoxels[2]>=minimumZ)
|
&&feature.minimumSubvoxels[2]<=maximumZ&&feature.maximumSubvoxels[2]>=minimumZ)
|
||||||
return HeightfieldBoxClass::mixed;
|
return HeightfieldBoxClass::mixed;
|
||||||
}
|
}
|
||||||
|
if (recipe_->surfaceProfile.version == 1)
|
||||||
|
{
|
||||||
|
const FieldInterval proof = interval(minimumX, minimumY, minimumZ,
|
||||||
|
maximumX, maximumY, maximumZ);
|
||||||
|
if (!proof.known) return HeightfieldBoxClass::unknown;
|
||||||
|
if (proof.minimumDensitySubvoxels > 0) return HeightfieldBoxClass::allAir;
|
||||||
|
if (proof.maximumDensitySubvoxels < 0) return HeightfieldBoxClass::allSolid;
|
||||||
|
return HeightfieldBoxClass::mixed;
|
||||||
|
}
|
||||||
const std::int64_t stride=static_cast<std::int64_t>(stepVoxels)
|
const std::int64_t stride=static_cast<std::int64_t>(stepVoxels)
|
||||||
*WorldAssemblySubvoxelsPerVoxel;
|
*WorldAssemblySubvoxelsPerVoxel;
|
||||||
if((maximumX-minimumX)%stride!=0||(maximumY-minimumY)%stride!=0
|
if((maximumX-minimumX)%stride!=0||(maximumY-minimumY)%stride!=0
|
||||||
@@ -2220,7 +2740,29 @@ FieldInterval Heightfield2D::interval(std::int64_t minX, std::int64_t minY,
|
|||||||
std::int64_t groundMax = std::numeric_limits<std::int64_t>::min();
|
std::int64_t groundMax = std::numeric_limits<std::int64_t>::min();
|
||||||
std::int64_t capMin=plan_->capHeightSubvoxels-plan_->capUndulationSubvoxels;
|
std::int64_t capMin=plan_->capHeightSubvoxels-plan_->capUndulationSubvoxels;
|
||||||
std::int64_t capMax=plan_->capHeightSubvoxels+plan_->capUndulationSubvoxels;
|
std::int64_t capMax=plan_->capHeightSubvoxels+plan_->capUndulationSubvoxels;
|
||||||
if(recipe_->evaluatorVersion>=2)
|
if (recipe_->surfaceProfile.version == 1)
|
||||||
|
{
|
||||||
|
const auto profileBounds = world_assembly_detail::ProfileColumnBounds(
|
||||||
|
recipe_->surfaceProfile, *plan_, minX, minY, maxX, maxY);
|
||||||
|
groundMin = profileBounds.groundMinimum;
|
||||||
|
groundMax = profileBounds.groundMaximum;
|
||||||
|
capMin = profileBounds.capMinimum;
|
||||||
|
capMax = profileBounds.capMaximum;
|
||||||
|
std::int64_t groundFeatureMagnitude = 0;
|
||||||
|
std::int64_t capFeatureMagnitude = 0;
|
||||||
|
for (const auto& feature : plan_->features)
|
||||||
|
{
|
||||||
|
if (feature.type == 1)
|
||||||
|
groundFeatureMagnitude += std::abs(feature.parametersSubvoxels[0]);
|
||||||
|
if (feature.type == 3)
|
||||||
|
capFeatureMagnitude += std::abs(feature.parametersSubvoxels[0]);
|
||||||
|
}
|
||||||
|
groundMin -= groundFeatureMagnitude;
|
||||||
|
groundMax += groundFeatureMagnitude;
|
||||||
|
capMin -= capFeatureMagnitude;
|
||||||
|
capMax += capFeatureMagnitude;
|
||||||
|
}
|
||||||
|
else if(recipe_->evaluatorVersion>=2)
|
||||||
{
|
{
|
||||||
for(const auto& biome:recipe_->biomes)
|
for(const auto& biome:recipe_->biomes)
|
||||||
{
|
{
|
||||||
@@ -2313,6 +2855,24 @@ FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept
|
|||||||
{
|
{
|
||||||
FieldDetailEnvelope out;
|
FieldDetailEnvelope out;
|
||||||
if (!isValid()) return out;
|
if (!isValid()) return out;
|
||||||
|
if (recipe_->surfaceProfile.version == 1)
|
||||||
|
{
|
||||||
|
const auto& profile = recipe_->surfaceProfile;
|
||||||
|
out.maximumAbsoluteDisplacementSubvoxels = profile.amplitudeXSubvoxels
|
||||||
|
+ profile.amplitudeYSubvoxels;
|
||||||
|
if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
||||||
|
out.maximumAbsoluteDisplacementSubvoxels +=
|
||||||
|
(out.maximumAbsoluteDisplacementSubvoxels * profile.mirrorLambdaQ16
|
||||||
|
+ world_assembly_detail::Q16 - 1) / world_assembly_detail::Q16;
|
||||||
|
for (const auto& feature : plan_->features)
|
||||||
|
if (feature.type == 1 || feature.type == 3)
|
||||||
|
out.maximumAbsoluteDisplacementSubvoxels +=
|
||||||
|
std::abs(feature.parametersSubvoxels[0]);
|
||||||
|
if (profile.kind != WorldAssemblySurfaceProfileKindV1::flatField)
|
||||||
|
out.minimumFeatureSizeSubvoxels = std::max<std::int64_t>(1,
|
||||||
|
std::min(profile.wavelengthXSubvoxels, profile.wavelengthYSubvoxels) / 2);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
std::int64_t maximum = plan_->capUndulationSubvoxels*5/2
|
std::int64_t maximum = plan_->capUndulationSubvoxels*5/2
|
||||||
+plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels;
|
+plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels;
|
||||||
std::int64_t minimumFeature = std::numeric_limits<std::int64_t>::max();
|
std::int64_t minimumFeature = std::numeric_limits<std::int64_t>::max();
|
||||||
@@ -2448,6 +3008,7 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block
|
|||||||
&result.capBoxes,false,"cap");
|
&result.capBoxes,false,"cap");
|
||||||
}
|
}
|
||||||
bool hasCliffProfile=false;
|
bool hasCliffProfile=false;
|
||||||
|
if(recipe->surfaceProfile.version==0)
|
||||||
for(const auto& biome:recipe->biomes)hasCliffProfile|=biome.cliffStrengthQ16>0;
|
for(const auto& biome:recipe->biomes)hasCliffProfile|=biome.cliffStrengthQ16>0;
|
||||||
if(hasCliffProfile)
|
if(hasCliffProfile)
|
||||||
{
|
{
|
||||||
@@ -2488,7 +3049,8 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block
|
|||||||
// Controle negatif : cette plage unilaterale exclut un echantillon connu; l'audit doit rejeter cette borne mensongere.
|
// Controle negatif : cette plage unilaterale exclut un echantillon connu; l'audit doit rejeter cette borne mensongere.
|
||||||
const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2};
|
const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2};
|
||||||
result.negativeControlDetected = !world_assembly_detail::InInterval(deliberatelyWrong, controlPoint);
|
result.negativeControlDetected = !world_assembly_detail::InInterval(deliberatelyWrong, controlPoint);
|
||||||
const bool borderCoverage=recipe->biomes.size()<2||recipe->biomeBorderBlendSubvoxels==0
|
const bool borderCoverage=recipe->surfaceProfile.version!=0
|
||||||
|
||recipe->biomes.size()<2||recipe->biomeBorderBlendSubvoxels==0
|
||||||
||result.biomeBorderBoxes>0;
|
||result.biomeBorderBoxes>0;
|
||||||
const bool cliffCoverage=!hasCliffProfile||result.cliffBoxes>0;
|
const bool cliffCoverage=!hasCliffProfile||result.cliffBoxes>0;
|
||||||
const bool exceptionCoverage=std::none_of(plan->features.begin(),plan->features.end(),
|
const bool exceptionCoverage=std::none_of(plan->features.begin(),plan->features.end(),
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
Thank you, that was a strong first pass. Your scope warning about L76 was right and important. I need to correct my
|
||||||
|
brief: "about 75% of field time is the passage composer" was **wrong**.
|
||||||
|
- L76's term profiler covered only about 6.0M legacy samples for FI (5.6M for TN).
|
||||||
|
- L73 counts about 52M density calls per FI or TN whole view: 512 LOD0 tiles × 44,121, 448 LOD1 × 43,322, and so
|
||||||
|
on.
|
||||||
|
- The measured passage/support cost is about 6.5 of FI's ~40 field worker-seconds (~15%), not 75%. Roughly
|
||||||
|
30 worker-seconds of field time is not attributed to any term we've measured.
|
||||||
|
|
||||||
|
Your gain arithmetic for recommendation 1 inherited that error. Please redo it.
|
||||||
|
|
||||||
|
## Deeper pass (you now have more reasoning budget)
|
||||||
|
|
||||||
|
Examine **the whole generation chain**, end to end, for one cold whole view on the legacy FI/TN path. For each
|
||||||
|
stage, say what it does, how often it runs, what it costs per call (from code structure and the measured numbers),
|
||||||
|
and whether its work is necessary:
|
||||||
|
- world/plan resolution;
|
||||||
|
- tile key selection;
|
||||||
|
- `ClassifyBoxDetailed`;
|
||||||
|
- the dispatch into `AnalyticDensityAt` (and whatever runs for the ~46M calls the term profiler did not see);
|
||||||
|
- the composer;
|
||||||
|
- grid fill;
|
||||||
|
- MC;
|
||||||
|
- normals;
|
||||||
|
- skirts;
|
||||||
|
- ground/cap refinement;
|
||||||
|
- payload;
|
||||||
|
- Unreal publication.
|
||||||
|
|
||||||
|
The central question: **where do the unattributed ~30 worker-seconds most plausibly go?** For example:
|
||||||
|
- a different evaluation path the profiler doesn't instrument;
|
||||||
|
- per-call overhead outside the timed terms (dispatch, stratum lookup, virtual calls, allocation, cache misses);
|
||||||
|
- border/halo samples evaluated twice;
|
||||||
|
- normals sampling the field again;
|
||||||
|
- duplicated work between neighbouring tiles or LODs.
|
||||||
|
|
||||||
|
Give file:line for each. Then name the cheapest measurement that would confirm your top explanation, so the next
|
||||||
|
implementation round can verify it first.
|
||||||
|
|
||||||
|
Then **revise your ranked recommendations** in the light of the whole chain. Keep what still holds and drop what
|
||||||
|
doesn't. Add anything the chain review reveals.
|
||||||
|
|
||||||
|
Same rules as before:
|
||||||
|
- read-only;
|
||||||
|
- no edits anywhere, including temp files;
|
||||||
|
- web allowed;
|
||||||
|
- labels [CODE]/[MEASURED]/[PRIOR ART]/[INFERENCE].
|
||||||
|
- At most ~3,500 words.
|
||||||
|
- **Lead with the stage-by-stage cost map**, then the revised ranking.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
Thank you for both earlier passes. Your rec #1 (localize passages) turned out to be right, and here is what we've
|
||||||
|
learned since. This is a focused third pass: **passage evaluation design.** Read and reason only; no edits
|
||||||
|
anywhere, including temp files. Web allowed. Labels as before.
|
||||||
|
|
||||||
|
## What we know now (all measured, ablations verified to remove the work)
|
||||||
|
|
||||||
|
- **Owner four-strate world, cold LOD0–2-ready at W12:** FI 2.27 s, TN 2.25 s. Passages off: FI 0.81 s,
|
||||||
|
TN 0.85 s. Boundary bands cost only 0.1–0.2 s. Passage composition costs **~1.4–1.6 s** of near readiness, and
|
||||||
|
it alone separates FI/TN from the ~1 s goal. SW 1.02 s and VS 1.13 s are close already.
|
||||||
|
- **Inside the passage phase** (sampled, L83): carve distance ~40%, wall relief ~21–24% (3D value noise, 3 octaves
|
||||||
|
at 43/29/19 voxels), support restoration/columns ~14%, mouth shaping ~4%, other ~11%. Top symbols:
|
||||||
|
`ApplyResolvedStratePassageCarves`, `VF_FieldCachedValueNoise`, `VisitNearestPassageSpatialNode`,
|
||||||
|
`PassagePotentialWithinNode`, the carve lambda.
|
||||||
|
- **Exact trims done** (L81–L85):
|
||||||
|
- a density-aware outer reach bound (carve = `max(source, -roughSDF/Q)`, reach = `-d·Q + 0.24 m`);
|
||||||
|
- an exact relief skip when it can't change the max;
|
||||||
|
- per-tile BVH node masks and contiguous ranges;
|
||||||
|
- heavy-tile splitting;
|
||||||
|
- heavy-first order.
|
||||||
|
|
||||||
|
Each gave 5–12%. Readiness is throughput-bound (workers 81–85% busy).
|
||||||
|
- **Two representation shortcuts failed:**
|
||||||
|
- L86: a lazy exact per-sample result cache. 24% hit rate, ~700 MB, +47% slower.
|
||||||
|
- L88: a tile-local 4-voxel trilinear cache of passage deltas, exact near the surface. Coarse-node
|
||||||
|
evaluations still paid full route cost, and carve-segment visits rose from 34.5M to 56.2M (FI), making it
|
||||||
|
+46% slower.
|
||||||
|
|
||||||
|
**Lesson:** each route evaluation is expensive in itself, and reorganising *where* we evaluate doesn't help
|
||||||
|
unless the evaluation gets cheaper.
|
||||||
|
- **Composer semantics that must keep their meaning:**
|
||||||
|
- the carve is `max(source, -roughSDF/Q)`;
|
||||||
|
- support restoration, applied after later overlapping bores;
|
||||||
|
- segment-local capsule clearance;
|
||||||
|
- mouth shaping and fades;
|
||||||
|
- the walkable tread floor;
|
||||||
|
- Catmull–Rom joins on short segments;
|
||||||
|
- wall/floor relief of ±0.21/0.20 m.
|
||||||
|
|
||||||
|
Reports: `Standalone/Research/WorldAssemblerM3/Linux81`–`Linux88/REPORT.md`; code in `ShapeWorldInternal.h`
|
||||||
|
(`ApplyResolvedStratePassageCarves` and its helpers).
|
||||||
|
|
||||||
|
## The owner's rules (unchanged)
|
||||||
|
|
||||||
|
- One seed gives the same world on every machine. Byte equality with older versions isn't required; a perf pass
|
||||||
|
may change the world slightly.
|
||||||
|
- Passages stay walkable arrival → departure (support, capsule, grade, ripple audits), and look natural.
|
||||||
|
- No visible quality regression.
|
||||||
|
- Target: **LOD0–2 ready in ~1 s** after cold arrival. The whole view is a bonus; continuous low-priority far
|
||||||
|
generation afterwards.
|
||||||
|
|
||||||
|
## Questions
|
||||||
|
|
||||||
|
1. **What makes one route evaluation expensive** (read the code path end to end)? Per sample: how many segments are
|
||||||
|
visited, what does each visit compute (closest point on a Catmull–Rom or linear segment, frames, radius
|
||||||
|
interpolation, relief noise, support capsule tests, later-bore checks)? Which parts are redundant per sample, per
|
||||||
|
segment, or per tile?
|
||||||
|
2. **Design a passage representation where per-sample cost is near-constant and small,** and the work is not merely
|
||||||
|
moved around. Candidates to evaluate (don't limit yourself):
|
||||||
|
- analytic primitives: polyline/capsule chains with precomputed per-segment frames and an arc-length table;
|
||||||
|
- relief defined in 2D (arc length, angle) with a cheap periodic or table noise;
|
||||||
|
- a precomputed narrow-band distance grid per passage, built once per world at a fixed resolution with
|
||||||
|
deterministic integer construction, plus its build cost;
|
||||||
|
- separating "is this sample within reach" (cheap) from "exact carve value" (expensive);
|
||||||
|
- vectorising across a z-run of samples sharing the same segment candidates.
|
||||||
|
|
||||||
|
For each: expected per-sample cost, build cost, memory, how exact (displacement vs today), determinism risk, and
|
||||||
|
effort.
|
||||||
|
3. **Rank by expected gain toward ~1 s,** with arithmetic from the numbers above. For the top choice, describe the
|
||||||
|
smallest prototype and the cheapest falsifier, so the implementer can prove it before building it fully.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
At most ~3,000 words, **ranked recommendation first**, file:line evidence, labels [CODE]/[MEASURED]/[PRIOR ART]/
|
||||||
|
[INFERENCE].
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# VoxelForge — design review: how do we get a cold whole view from ~2.5–4.6 s to ~1 s?
|
||||||
|
|
||||||
|
You're reviewing a voxel terrain generator's performance architecture. **Read and reason only.** Don't edit,
|
||||||
|
build, run tests, commit or stash. You may read any file in this repository and search the web.
|
||||||
|
|
||||||
|
We've spent about 15 rounds finding and fixing hot spots, and the gains are shrinking. We want fresh eyes on the
|
||||||
|
*approach*, not another micro-optimisation. Question our framing where it's wrong.
|
||||||
|
|
||||||
|
## The system (summary, so you don't have to rediscover it)
|
||||||
|
|
||||||
|
**VoxelForge** is a UE5 plugin for underground density-field terrain. Generation lives in a standalone C++
|
||||||
|
library, "Core" (`Standalone/Core/`), that Unreal calls through an adapter
|
||||||
|
(`Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`).
|
||||||
|
- **The field.** Density is a *function*, not stored data: negative = solid. Core voxels are 25 cm. Tiles hold
|
||||||
|
32³ voxels.
|
||||||
|
- **Clipmap.** LOD0–8; each LOD doubles the voxel step. A "whole view" is about 2,250–2,850 tiles across all LODs
|
||||||
|
around one camera.
|
||||||
|
- **Meshing.** Marching cubes per tile (about 35³ samples per LOD0/1 tile with borders), plus normals and skirts.
|
||||||
|
- **The world is a vertical stack of "strates".** Each is ceiling → content → bedrock, with its own generator:
|
||||||
|
- SurfaceWorld (SW): heightfield, native path;
|
||||||
|
- FloatingIslands (FI), TunnelNetwork (TN) and VerticalShafts (VS): the "legacy compiled-analytic" path.
|
||||||
|
- **The owner's test world** stacks four of them: SW 512 m, FI 240 m, TN 128 m, VS 128 m tall.
|
||||||
|
- **Passages.** Walkable, winding, natural-looking tunnels near (0,0) connect each strate to the next. They're
|
||||||
|
carved by a shared "composer" that also adds support treads, boundary bands and mouth shaping.
|
||||||
|
- **Multiplayer.** No geometry is replicated. Every peer regenerates from the seed, the layout and a diff of
|
||||||
|
player edits. That's why peer determinism is mandatory.
|
||||||
|
|
||||||
|
## The goal and constraints
|
||||||
|
|
||||||
|
- **Goal:** about 1 s cold whole view at any strate arrival on the owner's PC (Ryzen 5800X3D, 8 cores/16 threads,
|
||||||
|
7900 XTX). Streaming must stay smooth in VR (Quest 3) and at high travel speed.
|
||||||
|
- **Must hold:**
|
||||||
|
- peer determinism (same inputs → same world on every machine);
|
||||||
|
- no visible quality regression;
|
||||||
|
- passages stay walkable from arrival mouth to departure mouth (capsule, support and grade audits exist);
|
||||||
|
- no game-thread hitches.
|
||||||
|
- **Not required:** byte-identical output with earlier versions. The owner accepts that perf passes change the
|
||||||
|
world's shape slightly, as long as every peer gets the same result and quality holds. (We have tended to treat
|
||||||
|
hash equality as a gate anyway; it's a convenience, not a rule.)
|
||||||
|
- **On the shelf, not a speed excuse:** a disk mesh cache is an accepted feature, but perf is judged cold.
|
||||||
|
|
||||||
|
## Measurements (owner world, measured on an i9-12900H with 8 workers, headless unless stated)
|
||||||
|
|
||||||
|
**Whole view now** (it was 2.6 / 7.2 / 6.0 / 2.7 s a week ago):
|
||||||
|
|
||||||
|
| Strate | Whole view |
|
||||||
|
|---|---:|
|
||||||
|
| SW | 2.4 s |
|
||||||
|
| FI | 4.6 s |
|
||||||
|
| TN | 4.3 s |
|
||||||
|
| VS | 2.5 s |
|
||||||
|
|
||||||
|
**In Unreal (SW arrival):**
|
||||||
|
- Core work equals headless;
|
||||||
|
- initial set ready at 3.74 s vs headless 2.41 s, the extra being scheduling;
|
||||||
|
- walkable ring 0.4–1.8 s.
|
||||||
|
|
||||||
|
**Where the time goes:**
|
||||||
|
- **L73:** field evaluation inside grid fill is about 40 of FI's 46 worker-seconds (TN 33 of 39). LOD0/1 tiles
|
||||||
|
evaluate about 44k density samples each.
|
||||||
|
- **L74/L75:** sound whole-tile skip proofs now certify about 22–26% of tile visits.
|
||||||
|
- Ceiling: 46–82% of LOD0–2 tiles are truly uniform, and 90–97% of samples lie in uniform 8³ blocks.
|
||||||
|
- Dominant remaining veto: tiles crossing a strate boundary.
|
||||||
|
- Legacy sub-tile (block) proofs were unsound once (missed passage wall/floor relief), then sound but slower
|
||||||
|
than they saved.
|
||||||
|
- **L76, per legacy sample:**
|
||||||
|
- passage/support composer ≈ **1.05 µs**;
|
||||||
|
- primitive scan ≈ 0.19 µs (0.25 primitive distance evaluations per sample, binned);
|
||||||
|
- boundary band ≈ 0.06 µs;
|
||||||
|
- noise ≈ 0.01 µs.
|
||||||
|
|
||||||
|
**About 75% of field time is the passage composer, even far from passages.**
|
||||||
|
- Per-tile passage culling was tried and changed 612 density samples / 24 mesh tiles. It was rejected only for
|
||||||
|
breaking hash equality, which is a convenience, not a rule; nobody checked whether that change was harmless.
|
||||||
|
|
||||||
|
**Tried and rejected:**
|
||||||
|
- the native FI generator (fails passage walk audits on this plan);
|
||||||
|
- deeper Unreal queues (done: 4.78 → 3.74 s).
|
||||||
|
|
||||||
|
**Trap we fell into once:** a diagnostic hash mode silently disabled the skips and doubled the timings. Measure the
|
||||||
|
production path.
|
||||||
|
|
||||||
|
## What to reason about (open; don't limit yourself to these)
|
||||||
|
|
||||||
|
1. **Is "evaluate a density function at ~44k points per tile, then march cubes" the right shape** for this world?
|
||||||
|
Consider, for example:
|
||||||
|
- sparse/hierarchical evaluation (octree or brick refinement driven by interval or Lipschitz bounds, as in
|
||||||
|
libfive/Fidget/MPR-style implicit rendering);
|
||||||
|
- reusing samples across nested clipmap LODs;
|
||||||
|
- evaluating smooth large-scale terms coarsely and interpolating;
|
||||||
|
- narrow-band evaluation near the surface only.
|
||||||
|
2. **The passages are a few winding tubes.** Why does every sample pay 1 µs for them? What representation would
|
||||||
|
make "far from any passage" free and "near a passage" cheap? For example:
|
||||||
|
- bake each passage once at plan time into a sparse distance/brick structure;
|
||||||
|
- a spatial index of segments with a conservative distance early-out;
|
||||||
|
- a per-tile passage candidate list built once.
|
||||||
|
|
||||||
|
What would a correct per-tile culling need so that it doesn't change the result? And if it changes the result,
|
||||||
|
how should we judge whether that change is acceptable?
|
||||||
|
3. **Which work is needed for "ready in 1 s"?** Maybe the target should be redefined: walkable ring first,
|
||||||
|
progressive LOD refinement, far LODs from cheaper representations or impostors, fewer tiles. What do comparable
|
||||||
|
engines do (Voxel Plugin, Teardown, Dreams, No Man's Sky, Minecraft-style streaming, Nanite-era voxel work)?
|
||||||
|
4. **Hardware.** 8 CPU cores plus a 7900 XTX that's mostly idle during load. An earlier GPU port was closed, and
|
||||||
|
determinism across GPU vendors is a concern. Is there a deterministic way to use it, e.g. integer/fixed-point
|
||||||
|
evaluation or a CPU-authoritative plus GPU-display split? Is SIMD (AVX2) evaluation of the legacy field
|
||||||
|
realistic?
|
||||||
|
5. **Anything in our process or measurement** that hides the real bottleneck.
|
||||||
|
|
||||||
|
## How to work efficiently
|
||||||
|
|
||||||
|
- **Trust the numbers above; don't re-measure or re-derive them.** Spend your effort on thinking.
|
||||||
|
- **Read code only to check whether a specific idea fits.** Entry points:
|
||||||
|
- `CODEMAP.md` (navigation) and `ARCHITECTURE.md` §8.10 (performance invariants);
|
||||||
|
- `Standalone/Core/src/ShapeWorldInternal.h`: composer, `AnalyticDensityAt`, `ClassifyBoxDetailed`, passage
|
||||||
|
terms;
|
||||||
|
- `Standalone/Core/src/VoxelForgeCore.cpp`: tile generation and skips;
|
||||||
|
- reports in `Standalone/Research/WorldAssemblerM3/`: `Linux73`–`Linux76`, `Research36`, and R38–R40 notes
|
||||||
|
inside `Linux73/` and `Linux76/`.
|
||||||
|
- **Use the web** for prior art and techniques, and cite them.
|
||||||
|
- **Go broad first** (many approaches, one or two lines each), then deep on the best three.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
At most ~2,500 words, plain language, **ranked recommendations first**. For each of the top three:
|
||||||
|
- what changes, and where in this codebase (file and function);
|
||||||
|
- expected gain, with arithmetic from the numbers above;
|
||||||
|
- what it risks: determinism, quality, walkability;
|
||||||
|
- effort;
|
||||||
|
- **a cheap experiment that would prove it wrong** before anyone builds it fully.
|
||||||
|
|
||||||
|
Then the shorter list of other ideas, then any questions only the owner can answer (e.g. what "ready" must mean).
|
||||||
|
Mark claims as [CODE] (read in this repo), [MEASURED] (from the numbers above), [PRIOR ART] (cited) or
|
||||||
|
[INFERENCE].
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Astra 1, pass 2 — whole-chain review (gpt-6-astra, effort medium, read-only), 2026-10-27
|
||||||
|
|
||||||
|
Claude verified: PackedDensityAt (ShapeWorldInternal.h ~5909) applies ApplyResolvedStrateComposition outside the L76 term scope; FScopedDensityQueryProfile times whole invocations crossing a 256-point boundary (batch-biased).
|
||||||
|
|
||||||
|
**Stage-by-stage cost map**
|
||||||
|
|
||||||
|
[MEASURED] The L73 stage timings below are historical, before the later skips. They describe where that run spent time; they cannot be subtracted from L76 term timings to produce a current residual. I also inspected the saved L76 results without executing generation.
|
||||||
|
|
||||||
|
[CODE] A major architectural detail: the “compiled-analytic” world **already contains precomputed packed signs and quantized surface-distance proxies**. Many lattice queries never enter `AnalyticDensityAt`.
|
||||||
|
|
||||||
|
| Stage | Work and frequency | Cost and necessity |
|
||||||
|
|---|---|---|
|
||||||
|
| **World/plan resolution** | [CODE] Headless loads the owner export, resolves/validates its layout, constructs a snapshot, then constructs `World`. Once per cold run. UE’s serialization entry is [`VoxelForgeCoreAdapter.cpp:2062`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:2062); headless startup begins at [`StreamBenchmarkHeadless.cpp:1659`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkHeadless.cpp:1659). | [MEASURED] Saved L76 FI owner-data loading takes about **25 ms**. No complete isolated resolution cost is supplied. [INFERENCE] Necessary, but not a plausible home for worker time measured inside tile-field queries. |
|
||||||
|
| **Shape compilation** | [CODE] `World::compile` builds primitives/bins, packed rock intervals, candidate bands, detailed distance proxies, sign-edge repairs, passage validation and far parents. Once for the base shape; additional pages when needed. [`VoxelForgeCore.cpp:1394`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1394), [`ShapeWorldInternal.h:2724`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2724). | [MEASURED] Saved L76 FI: **313 ms wall**, including approximately 48 ms shapes, 64 ms intervals/signs, 62 ms surface band and 119 ms far parents. [INFERENCE] Important to a one-second cold budget, but separate from the missing tile-field attribution. It already amortizes substantial base-field work. |
|
||||||
|
| **Tile-key selection** | [CODE] `desiredTiles` builds clipmap keys, vertical bounds and additional far surface columns. Once initially, then on streaming-set changes; about 2,250 tiles for FI/TN. [`VoxelForgeCore.cpp:1904`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1904). | [INFERENCE] Thousands of key/set operations, not millions of field evaluations. No measured isolated cost. Necessary selection; exact tile count remains a design choice. |
|
||||||
|
| **Whole-tile classification** | [CODE] Each assembly tile calls `ClassifyBoxDetailed`, including a one-step halo. It delegates to `ClassifyBoundsDetailed`, with conditional slicing and conservative vetoes. [`VoxelForgeCore.cpp:2377`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2377), [`ShapeWorldInternal.h:6385`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6385). | [MEASURED] L73: only **4/3 ms total FI/TN**, roughly **2/1 μs per tile**, but ineffective legacy skips then. These are not current classifier costs. [INFERENCE] Necessary only when avoided work pays for it; retain current profitable proofs. |
|
||||||
|
| **Density dispatch and base evaluation** | [CODE] Scalar lattice queries probe native/legacy-surface paths, then choose packed lookup or analytic evaluation. Batches have additional fast paths. [`ShapeWorldInternal.h:3475`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3475), [`:3602`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3602), [`:5909`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5909). | [MEASURED] L73’s approximately 52M calls and 39.9/33.2 field worker-seconds imply roughly **0.77/0.64 μs per counted point**, averaged over heterogeneous paths. [INFERENCE] Fetching/evaluating the base value is necessary; repeated dispatch is not inherently necessary. |
|
||||||
|
| **Composer** | [CODE] Packed and scalar analytic paths both call `ApplyResolvedStrateComposition`: cavern graph → boundary → passage/support → stamps → compatibility floor caps. [`ShapeWorldInternal.h:1993`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1993). Frequency depends on the actual route, not just the strate label. | [MEASURED] L76 measures approximately **6.5/5.5 worker-seconds** only within its eligible analytic scopes. [CODE] Its passage/support bucket also includes `ApplyResolvedCavernFloorCaps` at [`:2053`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2053). [INFERENCE] Composition is necessary where influential; universal invocation is not. |
|
||||||
|
| **Grid fill** | [CODE] One cached lattice per generated tile: typically **35³ = 42,875** points at LOD0/1, including halo. Scalar, X8 or Z-run traversal fills it. Storage is thread-local and reused. [`Standalone mesher:430`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:430), [`:957`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:957), [`:1296`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1296). | [MEASURED] L73: **36.14/29.37 s**, approximately **16.1/13.2 ms per visited tile**, including evaluation. [INFERENCE] Dense fill is avoidable in proven-uniform regions. This is the primary workload, not merely array writes. |
|
||||||
|
| **Marching cubes** | [CODE] Classifies cells using cached corners, computes geometry only for surface cells, interpolates edges and deduplicates vertices. LOD0 midpoint correction can issue extra unclamped queries. [`Mesher:1879`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1879), [`:1943`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1943). | [MEASURED] L73 MC aggregate: **40.58/32.63 s**, including grid work. Subtracting grid gives about **4.44/3.26 s**, not another 40 seconds. [INFERENCE] Necessary extraction, but replacing MC is not the first lever. |
|
||||||
|
| **Normals** | [CODE] Central differences normally read the existing grid. At Step 1, proxy-band fallback values trigger `DensityAtUnclamped`; optional gradient memoization avoids repeated corner gradients. [`Mesher:1654`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1654). | [MEASURED] L73: **0.306/0.246 s**, about **0.14/0.11 ms per visited tile**. [INFERENCE] Some repeated field queries exist, but six new field evaluations per vertex is not the general path. Normals cannot plausibly explain 30 seconds under these measurements. |
|
||||||
|
| **Skirts** | [CODE] Boundary-edge detection, extrusion and density probes prevent unsuitable skirt geometry. Work scales with boundary geometry, not tile volume. [`Mesher:2111`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:2111). | [MEASURED] L73: **1.079/1.091 s**, approximately **0.48/0.49 ms per visited tile**. [INFERENCE] Necessary for the current seam solution; bounded opportunity, not the missing dominant cost. |
|
||||||
|
| **Ground/cap refinement and far shells** | [CODE] Far stratum/connector repairs and native ground/cap refinement run under eligibility gates, with refinement at LOD≥4 and no overlapping edits. [`VoxelForgeCore.cpp:2792`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2792), [`:2875`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2875). | [MEASURED] L73’s combined “Other,” including these and assembly, is **2.86/2.78 s**. [INFERENCE] Required by the current far representation, but partly corrective work that a better far representation could avoid. Not an explanation for missing grid time. |
|
||||||
|
| **Payload** | [CODE] Core assembles material/vertex/index outputs; the headless wrapper converts vertices and moves indices/masks. Per output vertex/index, once per tile. [`VoxelForgeCore.cpp:2969`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2969), [`StreamingWorld.h:934`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamingWorld.h:934). | [MEASURED] The separately timed headless conversion is **12 ms total**, about **5 μs per tile**. [INFERENCE] This does not include all Core assembly or Unreal upload/collision costs. It is not a useful primary target. |
|
||||||
|
| **Unreal publication** | [CODE] Worker generation → prepared streams → result queue → budgeted game-thread component/upload/section work → asynchronous collision completion. [`VoxelWorld.cpp:13303`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13303), [`:9400`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:9400), [`:14010`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:14010), [`:14279`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:14279). | [MEASURED] SW has **1.33 s additional readiness latency**. [INFERENCE] That is not 1.33 s of CPU publication work. Per-tile apply/cook costs are not established here. Essential to the target, but cannot explain headless field time. |
|
||||||
|
|
||||||
|
**Where the “unattributed 30 seconds” most plausibly went**
|
||||||
|
|
||||||
|
[CODE] **First: packed-field composition is outside L76’s measurement scope.** The relevant chain is:
|
||||||
|
|
||||||
|
`DensityAtLattice` → `PackedDensityAt` → `ApplyResolvedStrateComposition`.
|
||||||
|
|
||||||
|
`PackedDensityAt` reads `RockWords`, `CandidateWords` and possibly `SurfaceDistanceQ`, then applies composition. It never enters the `FScopedLegacyFieldTermScope` created later inside `AnalyticDensityAt` at [`ShapeWorldInternal.h:5967`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5967).
|
||||||
|
|
||||||
|
[CODE] Term timers immediately return when that scope is inactive: [`ShapeWorldInternal.h:2275`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2275). Thus an expensive passage query can be counted by the outer field profiler but entirely absent from L76’s passage timing.
|
||||||
|
|
||||||
|
[CODE] `DensityDispatchPathAt` returns “compiled-analytic/FloatingIslands” from the bound generator kind without distinguishing packed from analytic execution: [`ShapeWorldInternal.h:3455`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3455). That label is insufficient for performance attribution.
|
||||||
|
|
||||||
|
[CODE] There are further differences:
|
||||||
|
|
||||||
|
- X8 can return packed proxy values directly without invoking the scalar composer; other conditions make it fall back to scalar calls. [`ShapeWorldInternal.h:3675`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3675)
|
||||||
|
- FI LOD0 preferentially uses scalar vertical traversal. [`Mesher:1288`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1288)
|
||||||
|
- Z-run dispatch can fall back to scalar lattice queries, including packed queries. Its analytic term scope is reached only after those fallbacks. [`ShapeWorldInternal.h:11460`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:11460)
|
||||||
|
- Native/surface branches and non-FI/TN samples are excluded from the legacy-term population.
|
||||||
|
|
||||||
|
[INFERENCE] Therefore, the correction establishes that **6.5 seconds is measured passage cost within one subset**, not that total passage cost is only 15%. Packed composition is my leading explanation for substantial uninstrumented work. I cannot assign it 30 seconds from source alone.
|
||||||
|
|
||||||
|
[MEASURED] **Second: the residual mixes different generations of the workload.** Existing [L76 saved results](/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l76/term-profile-before/summary.json) contain:
|
||||||
|
|
||||||
|
| Same saved L76 run | FI | TN |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Density point calls | 26.99M | 25.13M |
|
||||||
|
| Eligible legacy points | 6.01M | 5.56M |
|
||||||
|
| Reported field estimate | 18.19 worker-s | 15.49 worker-s |
|
||||||
|
| Tile-worker total | 33.80 worker-s | 28.56 worker-s |
|
||||||
|
| Whole-view wall | 4.621 s | 3.977 s |
|
||||||
|
|
||||||
|
[INFERENCE] The approximately 46M unseen calls belong to subtracting L76’s eligible count from L73’s pre-skip population. They are not an established current population. Likewise, `40−measured_terms` is not a coherent current residual.
|
||||||
|
|
||||||
|
[CODE] **Third: the aggregate field estimator can be biased by batching.** `FScopedDensityQueryProfile` samples when the point sequence crosses a multiple of 256, timing the entire invocation and recording its point count. The reducer pools sampled time/points and scales by all points. See [`ShapeWorldInternal.h:2355`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2355) and [`StreamBenchmarkPipeline.h:5611`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkPipeline.h:5611).
|
||||||
|
|
||||||
|
[INFERENCE] A batch of eight has approximately eight times a scalar invocation’s chance of being selected, and contributes eight sampled points when selected. Pooling both without inclusion-probability correction overrepresents batch points. If packed batches are cheap and scalar composition is expensive, the estimate can substantially understate field time. Periodic sampling also risks correlation with spatial traversal.
|
||||||
|
|
||||||
|
[MEASURED] This deserves attention: L76 FI LOD0 records **12.70 s grid fill but only 5.96 s estimated field evaluation**. Conversely, LOD3 estimates **3.04 s field time against 2.58 s total tile-worker time**. Those are diagnostic inconsistencies, not proof of where the discrepancy lies. [Saved log](/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l76/term-profile-before/strate1-w8.log:12880)
|
||||||
|
|
||||||
|
[INFERENCE] **Lower-ranked explanations:**
|
||||||
|
|
||||||
|
- **Dispatch/lookups:** real repeated work. Stratum lookup uses binary search and coordinate conversion, sometimes repeatedly per sample: [`ShapeWorldInternal.h:437`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:437). But attributing 30 seconds to this alone means roughly **0.65 μs per purported 46M calls**. That needs evidence; hidden composer execution is more concrete.
|
||||||
|
- **Allocation/cache misses:** possible contributors, but grid allocation is reused and packed access is direct indexing. There is no evidence of one heap allocation per ordinary density point.
|
||||||
|
- **Halos/neighbor duplication:** significant but bounded. `35³−33³ = 6,938`, or **16.2%** of a dense tile’s samples, is the gradient halo. Including shared faces, an ideal large same-LOD region could reduce `35³` toward `32³` unique samples per tile: approximately **23.6%** savings before edges and synchronization. This multiplies existing evaluation cost; it does not create a separate unmeasured stage.
|
||||||
|
- **Normals/midpoints/skirts:** issue additional calls, but their measured stage budgets exclude them as the dominant explanation. Cross-LOD duplication remains unquantified.
|
||||||
|
|
||||||
|
**Cheapest decisive measurement**
|
||||||
|
|
||||||
|
[INFERENCE] First take a sampling call-stack profile of the clean production run, focused on grid fill. It requires no new per-sample timers and should immediately distinguish packed composition from dispatch or memory stalls.
|
||||||
|
|
||||||
|
[INFERENCE] The next instrumentation change should be a **mutually exclusive dispatch census**, not another individual-term profiler. Count and sample time for packed-scalar, packed-X8, analytic-scalar, analytic-batch, native and legacy-surface routes, split by LOD and caller stage. Measure composition inside packed-scalar explicitly.
|
||||||
|
|
||||||
|
Use equal-probability invocation sampling with correct expansion, or stratify by route **and batch length**. Avoid nested double-counting; compare the resulting total against directly timed grid fill. Separate passage carving from compatibility floor caps.
|
||||||
|
|
||||||
|
[INFERENCE] This falsifies my leading explanation if packed-scalar composition occupies little time. Only then prioritize dispatch, cache misses or other overhead.
|
||||||
|
|
||||||
|
**Revised ranked recommendations**
|
||||||
|
|
||||||
|
**1. Establish complete attribution, then specialize the dominant lattice route.**
|
||||||
|
|
||||||
|
[INFERENCE] This replaces my unconditional “passages first” ranking. The first implementation round should produce a closed cost account and identify which route dominates LOD0/1.
|
||||||
|
|
||||||
|
If packed composition dominates, give each tile/brick a stable local composition program and an identity fast path. Extend `FScopedPassageTileQueries`, `PackedDensityAt` and `ApplyResolvedStrateComposition`. Hoist Z-only dispatch decisions into plane/run contexts. Before copying the X8 shortcut into scalar traversal, establish why their composition behavior differs and which semantics are required.
|
||||||
|
|
||||||
|
[INFERENCE] Effort: small for attribution, medium for specialization. Risks: incorrect support envelopes, changed composition order, scalar/batch disagreement. Preserve peer determinism and audit clearance, support and grade; old-version hashes remain an exactness diagnostic.
|
||||||
|
|
||||||
|
[MEASURED → INFERENCE] **Corrected passage arithmetic:** using your historical budget strictly as an illustration:
|
||||||
|
|
||||||
|
- FI, removing 80–90% of **6.5/46** worker-seconds: `4.6 × (1−0.8…0.9 × 6.5/46)` = **4.08–4.02 s**.
|
||||||
|
- TN, removing 80–90% of **5.5/39**: approximately **3.81–3.75 s**.
|
||||||
|
|
||||||
|
These replace my erroneous approximately two-second estimates. They are not forecasts because the operands span rounds. Using the contemporaneous saved L76 totals instead gives approximately **3.91–3.83 s FI**, **3.37–3.29 s TN**, under proportional scaling.
|
||||||
|
|
||||||
|
[INFERENCE] Total passage savings could be larger if the missing packed-route work is substantial. Measure it before assigning a percentage.
|
||||||
|
|
||||||
|
**2. Remove repeated composed sampling; retain MC.**
|
||||||
|
|
||||||
|
[INFERENCE] Keep the retained-proof/local-program recommendation, but build on the existing packed representation. Priorities:
|
||||||
|
|
||||||
|
- Retain successful slice/brick proofs rather than requiring a uniform whole-tile verdict.
|
||||||
|
- Reuse final composed lattice samples across neighboring tiles where inputs and semantics match.
|
||||||
|
- Evaluate exact surface endpoints and normal stencils; leave certified interiors implicit.
|
||||||
|
|
||||||
|
The relevant changes remain `ClassifyBoundsDetailed`, `World::generateTile` and the standalone mesher’s grid fill. Sample-cache keys must include evaluator version, edits and relevant resolution/context—not coordinates alone.
|
||||||
|
|
||||||
|
[INFERENCE] An alternative worth testing is applying composition once to a shared near-region lattice. Count its cold construction cost: moving the same work earlier is not a speedup. It wins through deduplication, localization or cheaper batching.
|
||||||
|
|
||||||
|
[INFERENCE] Effort: medium-to-large. Risks: stale edit dependencies, seams and false uniform proofs. Cheap falsifier: one representative region spanning adjacent tiles and a strate boundary; compare total construction plus meshing cost against independent tiles.
|
||||||
|
|
||||||
|
[INFERENCE] Withdraw the earlier combined one-second projection. At unchanged throughput, FI’s current 4.6 seconds requires approximately **78% critical-path reduction**. Border sharing alone cannot supply that; broad sampling elimination still has to prove its economics.
|
||||||
|
|
||||||
|
**3. Preserve a separate arrival deadline and reduce end-to-end latency.**
|
||||||
|
|
||||||
|
[INFERENCE] Keep dependency-aware scheduling through collision publication, rather than merely increasing queue depth. The existing near-priority and hot-swap mechanisms already provide the foundation.
|
||||||
|
|
||||||
|
Shape compilation’s approximately 0.31 seconds matters once generation approaches one second. Far repair/refinement becomes material too. Optimize these after the dominant grid route is understood; do not hide their cost outside the cold timer.
|
||||||
|
|
||||||
|
[MEASURED → INFERENCE] SW’s scheduling gap remains a possible **0–1.33-second** latency opportunity, not a guaranteed CPU saving. Scheduling cannot remove required whole-view work.
|
||||||
|
|
||||||
|
[INFERENCE] Effort: medium. Risks: starving far coverage or publishing visuals before safe collision. Cheap falsifier: an arrival-only admission experiment plus prerequisite/publication tracing. If its uncontended critical path exceeds one second, scheduling alone cannot meet arrival readiness.
|
||||||
|
|
||||||
|
[INFERENCE] GPU porting, SIMD, native-generator replacement and a new mesher stay below these three. The newly visible distinction is between **cheap cached base density and potentially expensive repeated composition**, not simply “52M analytic evaluations.”
|
||||||
|
|
||||||
|
Read-only inspection completed; no edits, builds, tests, commits or stashes.
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# Astra 1, pass 3 — passage evaluation design (gpt-6-astra, medium, read-only), 2026-10-28
|
||||||
|
|
||||||
|
**[INFERENCE] Ranked recommendation: replace recursive route queries with a compiled spatial passage evaluator—not another cache of evaluated densities.**
|
||||||
|
|
||||||
|
1. **Compile short lists of prepared carve, tread and clearance primitives into spatial bricks.** Evaluate those lists directly, retaining ordered support restoration and explicit mouth exceptions.
|
||||||
|
2. **Add a compact relief representation**, preferably sampled from today’s relief on the passage surface. Treat this as part of recommendation 1’s performance budget, not a sufficient standalone fix.
|
||||||
|
3. **Consider a sparse distance grid only if built directly from prepared primitives**, without calling today’s composer at every grid node. Its cold construction cost makes it a less attractive first prototype.
|
||||||
|
|
||||||
|
[MEASURED → INFERENCE] The target requires a substantial replacement:
|
||||||
|
|
||||||
|
\[
|
||||||
|
T_{\text{FI}}\approx0.81+1.46r+B,\qquad
|
||||||
|
T_{\text{TN}}\approx0.85+1.40r+B
|
||||||
|
\]
|
||||||
|
|
||||||
|
Here \(r\) is the fraction of passage cost remaining and \(B\) is added cold critical-path construction time. This is an ablation-based planning model, not a guaranteed linear prediction.
|
||||||
|
|
||||||
|
| Passage cost remaining | FI, before added construction | TN |
|
||||||
|
|---|---:|---:|
|
||||||
|
| 30% | 1.25 s | 1.27 s |
|
||||||
|
| 15% | 1.03 s | 1.06 s |
|
||||||
|
| 10% | 0.96 s | 0.99 s |
|
||||||
|
| 5% | 0.88 s | 0.92 s |
|
||||||
|
|
||||||
|
[INFERENCE] **Tenfold passage acceleration barely reaches one second before construction.** With 30 ms additional construction, TN needs approximately **11.7×**. Another 5–12% trim cannot close this gap.
|
||||||
|
|
||||||
|
**What one evaluation actually does**
|
||||||
|
|
||||||
|
[CODE] The entry point is [`ApplyResolvedStratePassageCarves:1115`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1115). Its cost is a sequence of different geometric questions:
|
||||||
|
|
||||||
|
1. **Reach screening.** Check tile flags, incoming density, passage Z range and floor-profile ranges. Solid samples can invoke `PassageMayAffectDensity` before entering the main passage loop. Inside that loop, another reach test may occur.
|
||||||
|
2. **Column/support preparation.** Scan eligible void crossings and possibly evaluate column distance.
|
||||||
|
3. **Nearest carve feature.** Search four ranges: descending body, upper endpoint, lower-mouth neighborhood and departure tail.
|
||||||
|
4. **Relief and mouth shaping.** Compute wall orientation, route-relative distances, entrance projection, relief and fades.
|
||||||
|
5. **Tread selection.** Find the floor-owning segment separately from the nearest carve segment; evaluate up to three neighboring support segments.
|
||||||
|
6. **Clearance and composition.** Check the passage’s player capsules, later bores and other passages’ capsules; apply, cancel or defer support. Finally restore mouth support and deferred support.
|
||||||
|
|
||||||
|
[CODE] **A carve segment visit is not an iterative closest-point solve on Catmull–Rom.** It computes endpoint differences, squared length, a clamped projection onto the straight chord, then optionally evaluates a Catmull–Rom position at that same parameter. Radius remains interpolated from the endpoints. Distance requires a deterministic square root, followed by stable tie-breaking. [`ShapeWorldInternal.h:1497`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1497)
|
||||||
|
|
||||||
|
[INFERENCE] Consequently, replacing this with a mathematically accurate spline-distance solver would likely increase cost and change the field. Compiling the existing formula—or approximating it deliberately—is the appropriate comparison.
|
||||||
|
|
||||||
|
[CODE] **Traversal itself is substantial.** Each nearest-node bound computes distance to an AABB using a square root, subtracts maximum radius, checks range membership and visits children. Potential-within queries perform a separate recursive search. Tile masks and contiguous ranges already remove some overhead; they do not eliminate per-sample traversal. [`:5590`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5590), [`:5676`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5676)
|
||||||
|
|
||||||
|
[MEASURED → INFERENCE] L88’s approximately 34.5M/33.1M carve visits divided by approximately 14.06M/12.43M passage iterations gives about **2.45/2.66 carve visits per iteration**, assuming those census populations align. This is not visits per density query or per successful route evaluation: iterations include rejected passages, and the visit count excludes internal BVH nodes and separate capsule searches. **The problem is not necessarily hundreds of evaluated segments; it is expensive repeated search around relatively few winners.**
|
||||||
|
|
||||||
|
[CODE] **Support is a different geometry query.** It uses horizontal projection, extended tread segments, distinct smoothing rules, native-density-dependent mouth offsets and floor-depth gates. Its floor-profile search deliberately avoids choosing a neighboring helix coil merely because that coil supplies the nearest wall. [`:1833`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1833), [`:2127`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2127)
|
||||||
|
|
||||||
|
[CODE] Capsule checks project horizontally and test a vertical capsule with 0.34 m radius and 0.54 m axial half-length. Later-bore checks already have precomputed overlap candidates, but can fall back to BVH searches. Deferred support is restored after the passage loop. [`:5708`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5708), [`:5810`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5810), [`:2163`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2163)
|
||||||
|
|
||||||
|
[CODE] **Relief still costs work on cache hits.** Each of three octaves computes lattice coordinates, a cache hash, fades and seven interpolations. A miss additionally generates eight corner values. The cache has 64 direct-mapped entries. Wall relief uses wavelengths 43/29/19 and weights 0.50/0.35/0.15. [`VoxelForgeFieldShared.h:260`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeFieldShared.h:260), [`:394`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeFieldShared.h:394)
|
||||||
|
|
||||||
|
[CODE] Mouth work additionally evaluates noise at projected route positions. Some calculations happen before determining whether the sample belongs to the mouth’s small affected region. [`ShapeWorldInternal.h:1683`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1683)
|
||||||
|
|
||||||
|
[INFERENCE] The reusable information is:
|
||||||
|
|
||||||
|
- **Per segment:** chord vectors, inverse squared lengths, lengths, radius slopes, cubic coefficients, support flags, overlap extension and prefix/suffix arc lengths.
|
||||||
|
- **Per spatial brick:** possible carve winners, floor owners, clearance capsules, column features and ordered passage dependencies.
|
||||||
|
- **Per sample:** projections, distances, source-dependent support blending and final min/max operations.
|
||||||
|
- **Across a Z-run:** candidate lists and XY projections; not generally the winning segment or final support decision.
|
||||||
|
|
||||||
|
[CODE] `BuildPassageComposerConstants` currently prepares passage-level metadata, not this complete segment representation. [`:5147`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5147)
|
||||||
|
|
||||||
|
**1. Compile a spatial passage program**
|
||||||
|
|
||||||
|
[INFERENCE] Build immutable sparse bricks containing contiguous references to prepared features. Use separate lists for **carve, tread, clearance and mouth/column operations**. A single nearest-segment identifier cannot represent their different ownership rules.
|
||||||
|
|
||||||
|
For each brick:
|
||||||
|
|
||||||
|
- Eliminate segments that cannot win its carve query, using conservative lower/upper distance bounds.
|
||||||
|
- Retain every potentially relevant overlapping coil.
|
||||||
|
- Store floor candidates independently.
|
||||||
|
- Store later-bore and other-capsule dependencies in stable passage order.
|
||||||
|
- Subdivide crowded bricks or use an explicit slow fallback; never silently cap candidates.
|
||||||
|
|
||||||
|
[INFERENCE] The common query becomes one brick lookup, typically a few direct projections/distances, one relief lookup and a small ordered composition sequence. No reach-BVH traversal followed by nearest-BVH traversal followed by clearance-BVH traversal.
|
||||||
|
|
||||||
|
**Important limitation:** “two to four candidates” is a design target to falsify, not something the current route guarantees. Nearby coils and short, densely authored segments may produce much larger lists.
|
||||||
|
|
||||||
|
[INFERENCE] Prepare the existing chord/Catmull formulas first. This avoids multiplying segment count through premature tessellation. Subsequently merge or approximate pieces only where a measured error budget permits. Precomputed coefficients and reciprocals may change floating-point rounding; version the evaluator and enforce the same arithmetic across peers.
|
||||||
|
|
||||||
|
[PRIOR ART] If route-coordinate relief needs frames, compute stable frames once. Rotation-minimizing frames are established for swept surfaces; ordinary Frenet frames can become discontinuous at inflections. **Keep the walkable floor gravity-referenced**, regardless of the wall-texture frame. [Wang et al., rotation-minimizing frames](https://www.microsoft.com/en-us/research/publication/computation-rotation-minimizing-frames/)
|
||||||
|
|
||||||
|
[INFERENCE] **Do not discard the density-dependent reach rule.** A narrow geometric envelope alone is insufficient when `max(source, -roughSDF/Q)` can change deeply negative source values outside the visible tube. Outside a brick’s detailed region, return identity only with a valid source-aware proof. Otherwise use a wider prepared candidate set or an explicit fallback. Count that fallback in the speed budget.
|
||||||
|
|
||||||
|
[INFERENCE] Cost model:
|
||||||
|
|
||||||
|
- **Query:** target roughly **0.1–0.3 μs** for an ordinary active query with 2–4 prepared candidates, excluding rare complex exceptions. This is an engineering target, not measured performance.
|
||||||
|
- **Build:** \(O(S+R)\) segment preparation/reference emission, plus candidate-certification work; \(S\) segments and \(R\) brick references. Target **under 30 ms added cold critical path** initially.
|
||||||
|
- **Memory:** approximately 128–256 bytes per prepared segment, plus roughly `16 + 4K` bytes per simple brick list, before multiple semantic lists and directory overhead. Thousands of segments should require megabytes, not L86’s hundreds of megabytes.
|
||||||
|
- **Exactness:** prepared original formulas can preserve geometry closely; segment simplification has no automatic displacement guarantee. Keep mouth/support equations source-dependent.
|
||||||
|
- **Effort:** medium-to-large; the candidate compiler and semantic separation are the substantial work.
|
||||||
|
|
||||||
|
[INFERENCE] Leaving the entire old support phase untouched is unlikely to suffice: **14% of the 1.4-second passage contribution is approximately 0.20 seconds**, already exceeding TN’s available 0.15-second margin. Preserve support’s meaning, but accelerate its feature selection too.
|
||||||
|
|
||||||
|
**2. Replace repeated 3D relief evaluation with a small surface field**
|
||||||
|
|
||||||
|
[INFERENCE] Use coordinates along the route and around its wall, with periodic angular indexing and continuous frames. A table can combine all three octaves into one lookup. Avoid evaluating general-purpose `atan2` unnecessarily; a specified octant-based angular mapping is an option, but its distortion must be checked.
|
||||||
|
|
||||||
|
Prefer initially **sampling today’s 3D relief at the reference wall surface** rather than inventing unrelated 2D noise. This retains much of its spatial appearance. Extend the value radially only over a controlled band; preserve the floor-alignment weights and separate mouth-floor treatment.
|
||||||
|
|
||||||
|
[INFERENCE] Costs and risks:
|
||||||
|
|
||||||
|
- **Query:** target **20–80 ns** for coordinate mapping plus a warm four-value lookup; cold memory or frame interpolation can exceed this.
|
||||||
|
- **Build/memory:** illustrative 1 km of route, 0.25 m longitudinal spacing, 64 angular samples and int16 values: **256k samples, approximately 0.5 MB**. Build requires those relief evaluations once, not full composer evaluations.
|
||||||
|
- **Exactness:** independently regenerated noise can differ by **0.42 m in wall offset** between opposite extrema despite retaining ±0.21 m amplitude. Equal amplitude does not establish equal quality. Surface sampling reduces this risk but does not prove a displacement bound.
|
||||||
|
- **Determinism:** fixed integer coordinates, specified interpolation/rounding, periodic endpoints and stable frame construction.
|
||||||
|
- **Effort:** medium; seam handling, frame continuity and floor/mouth validation dominate.
|
||||||
|
|
||||||
|
[MEASURED → INFERENCE] Even removing **all** the measured 21–24% relief share saves only roughly **0.3 seconds**. An 80% relief reduction alone predicts roughly **2.0 seconds**, not one second. It belongs in the combined representation.
|
||||||
|
|
||||||
|
[INFERENCE] Cheapest falsifier: evaluate table interpolation error and normal/ripple changes on one curved wall, one mouth and one self-overlapping coil. Reject a table that needs near-original per-sample 3D noise to hide seams or preserve appearance.
|
||||||
|
|
||||||
|
**3. Sparse narrow-band distance grids**
|
||||||
|
|
||||||
|
[INFERENCE] A grid gives genuinely near-constant queries, but only if its construction avoids L88’s failure: **do not fill it by invoking the existing full route evaluator per node**.
|
||||||
|
|
||||||
|
Rasterize prepared local primitives into owned bricks, reduce candidates deterministically, and construct the band from those primitives. Store separate carve and support/clearance information where needed. A baked final delta cannot represent all source-density-dependent mouth restoration.
|
||||||
|
|
||||||
|
[PRIOR ART] OpenVDB’s narrow-band representation stores active distances near the surface and constant signed values elsewhere. That is useful representation prior art, but its clamped exterior is not automatically compatible with your composer. [OpenVDB representation](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/doc/doc.txt)
|
||||||
|
|
||||||
|
[INFERENCE] For route length \(L\), radius \(r\), band half-width \(w\) and spacing \(h\), band sample count is approximately:
|
||||||
|
|
||||||
|
\[
|
||||||
|
N \approx 4\pi r w L/h^3.
|
||||||
|
\]
|
||||||
|
|
||||||
|
Illustratively, \(L=1{,}000\) m, \(r=3\) m, \(w=0.75\) m:
|
||||||
|
|
||||||
|
- **25 cm spacing:** approximately 1.8M samples, **3.6 MB per int16 channel**.
|
||||||
|
- **12.5 cm spacing:** approximately 14.5M samples, **29 MB per channel**.
|
||||||
|
|
||||||
|
Overlapping passages, mouth volumes, brick padding and multiple channels add memory.
|
||||||
|
|
||||||
|
[INFERENCE] **Query:** roughly **0.05–0.25 μs per interpolated channel** as a target, with locality decisive. **Build:** millions of local distance operations; even 0.1–0.3 μs per sample means 0.18–0.54 CPU-seconds for the coarser example, before indexing, extra channels or allocation. Parallel wall time must be measured; it is not free.
|
||||||
|
|
||||||
|
[INFERENCE] Grid spacing is not a displacement bound. At 25 cm, narrow tread/clearance transitions need analytic treatment or finer data. Integer storage does not ensure deterministic construction: integer distance/projection arithmetic, tie-breaking, reduction order and interpolation must also be specified.
|
||||||
|
|
||||||
|
[INFERENCE] Effort: large. This is attractive only if direct construction plus all arrival queries beats the compiled analytic representation. A tiny benchmark of grid lookups alone proves nothing.
|
||||||
|
|
||||||
|
**Other candidates**
|
||||||
|
|
||||||
|
- **[INFERENCE] Cheap reach versus exact value:** retain the distinction, but answer reach from brick metadata or squared primitive bounds. The current distinction already exists; repeating its recursive BVH search is the expense.
|
||||||
|
- **[INFERENCE] SIMD Z-runs:** useful after candidate compilation. XY projection is reusable for tread/capsule queries, while 3D carve projection varies affinely with Z before clamping. Expect amortized lookup and arithmetic benefits, not eightfold speedup. Memory adds only scratch arrays; construction is unchanged. Deterministic lane arithmetic and stable winner selection are required. Medium effort.
|
||||||
|
- **[INFERENCE] Pure capsule-chain replacement:** cheap per primitive, but tessellation can increase candidate count and introduce curvature/radius artifacts. Preserve short-join shape with bounded approximation; do not equate a visually smooth centerline with a walkable tread.
|
||||||
|
|
||||||
|
**Smallest prototype and decisive falsifier**
|
||||||
|
|
||||||
|
[INFERENCE] Implement one prepared passage evaluator for the **FI/TN junction passage**, including an overlapping coil and tread—not a straight isolated tube.
|
||||||
|
|
||||||
|
1. Compile segment constants and brick candidate lists; retain existing geometry formulas and relief.
|
||||||
|
2. Run the complete carve/support/clearance sequence from those lists.
|
||||||
|
3. Measure construction, query time, candidate-count distribution and fallback time separately.
|
||||||
|
4. Add the relief table only after direct evaluation beats the old traversal.
|
||||||
|
|
||||||
|
[INFERENCE] The cheapest first falsifier is the **candidate compiler census**: if commonly sampled bricks still need many segments or fallback retains more than approximately 10% of original passage time, the proposed common path cannot meet the target even if nearly free.
|
||||||
|
|
||||||
|
[INFERENCE] Then replay representative samples with their incoming densities and original spatial order. Include misses, mouths, support and overlapping bores; exclude benchmark-only allocations. A promising kernel must finally survive cold arrival timing including construction, peer comparisons, capsule/support/grade/ripple audits and visual inspection.
|
||||||
|
|
||||||
|
[INFERENCE] Budget acceptance around **90–95% total passage-cost removal**, not an attractive isolated kernel speedup. The measured ceiling makes approximately one second plausible, but only if the new representation replaces the search-heavy semantic pipeline broadly enough.
|
||||||
|
|
||||||
|
Read-only review; no files changed or generation/tests run.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Astra 1 — design review (gpt-6-astra, effort low, read-only), 2026-10-27
|
||||||
|
|
||||||
|
Brief: see BRIEF.md. Note from Claude: the brief's "~75% of field time is the passage composer" was WRONG — L76's term profiler covered ~6.0M of ~52M density calls per view (FI); measured passage cost is ~6.5 of ~40 worker-s (~15%). Astra flagged the scope mismatch itself. Its gain arithmetic in rec. 1 inherits the wrong share.
|
||||||
|
|
||||||
|
**[INFERENCE] Ranked recommendations:**
|
||||||
|
|
||||||
|
1. **Compile passages into spatially local operations.** Make most samples bypass the composer entirely; simplify the remaining work.
|
||||||
|
2. **Replace repeated block classification with a retained hierarchy of proofs and local field programs.** Keep marching cubes; stop densely evaluating regions that cannot produce geometry.
|
||||||
|
3. **Give arrival readiness its own deadline-driven pipeline.** Preserve whole-view completion as a separate benchmark; scheduling improvements alone cannot meet it.
|
||||||
|
|
||||||
|
**[INFERENCE]** These are complementary. Passage localization is the strongest first investment, but does not plausibly deliver one second alone. A wholesale mesher replacement, GPU rewrite, or native-generator migration is premature.
|
||||||
|
|
||||||
|
### 1. Make passage influence local by construction
|
||||||
|
|
||||||
|
**[CODE]** There is already a spatial index: `BuildPassageFeatureTileList` queries passage feature bounds. `FScopedPassageTileQueries` turns route candidates into per-passage flags, while retaining segment lists for mouth support. Nevertheless, `ApplyResolvedStratePassageCarves` still iterates all passages and scans crossings. After finding a route distance, it calculates relief and mouth-related terms. See [ShapeWorldInternal.h](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:989).
|
||||||
|
|
||||||
|
**[INFERENCE] What changes:** compile a small, immutable passage program for each spatial region:
|
||||||
|
|
||||||
|
- Empty region: no passage invocation.
|
||||||
|
- Route region: ordered candidate segments, precomputed geometry and distances along the route.
|
||||||
|
- Mouth/support region: explicit support, transition-fade and later-bore dependencies.
|
||||||
|
- Overlapping features: retain the original composition order.
|
||||||
|
|
||||||
|
Use `BuildPassageComposerConstants`, `BuildPassageFeatureTileList`, `FScopedPassageTileQueries`, and `ApplyResolvedStratePassageCarves`; connect the result through `World::generateTile` in [VoxelForgeCore.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1997). This is stronger than filtering one loop: the region program should remove entire categories of work.
|
||||||
|
|
||||||
|
**[INFERENCE] Exact culling requires an identity proof**, not merely “the tube does not intersect this tile.” For every omitted operation, prove that it leaves the incoming density unchanged over the query domain. Include:
|
||||||
|
|
||||||
|
- Spline extrema, variable radius, wall/floor relief and smooth-blend reach.
|
||||||
|
- Mouth shaping, support treads, columns, boundary fade and dependencies on other passages.
|
||||||
|
- All sample positions used by normals, borders and skirts.
|
||||||
|
- Deterministic ordering and tie-breaking among retained features.
|
||||||
|
|
||||||
|
A feature can leave density signs unchanged while altering marching-cubes interpolation or normals. Conversely, changing far-away density magnitudes may be harmless. These are different correctness claims.
|
||||||
|
|
||||||
|
**[CODE]** L75 already corrected bounds for passage relief and spline extrema. The composer also conditionally restores support and defers support composition. Therefore, baking one distance to the nearest tube would not reproduce its semantics. [L75 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux75/REPORT.md:22)
|
||||||
|
|
||||||
|
**[INFERENCE]** If exact influence is awkward or effectively nonlocal, introduce a versioned, compact-support passage operator: identical in the protected walkable corridor and mouth regions, smoothly becoming identity outside an explicit envelope. That is a legitimate world-generation change under your constraints.
|
||||||
|
|
||||||
|
**[MEASURED → INFERENCE] Expected gain:** passage work is approximately `0.75 × 40 = 30` of FI’s 46 worker-seconds, and `0.75 × 33 = 24.75` of TN’s 39. Removing 80–90% gives proportional wall-time scenarios:
|
||||||
|
|
||||||
|
| View | 80% passage reduction | 90% passage reduction |
|
||||||
|
|---|---:|---:|
|
||||||
|
| FI | `4.6 × (46−24)/46 = 2.20 s` | `4.6 × (46−27)/46 = 1.90 s` |
|
||||||
|
| TN | `4.3 × (39−19.8)/39 = 2.12 s` | `4.3 × (39−22.275)/39 = 1.84 s` |
|
||||||
|
|
||||||
|
These assume critical-path time follows work reduction; they are not predictions for the Ryzen. Even eliminating passage cost entirely leaves approximately **1.6 seconds** in both views.
|
||||||
|
|
||||||
|
**[INFERENCE] Risks and effort:** medium effort for regional programs; larger for a new passage representation. Stable candidate ordering and versioned arithmetic protect determinism. Underestimated envelopes threaten clearance and support; overly generous envelopes merely lose speed. Run capsule, grade and support audits across entire passages, including landings and seams.
|
||||||
|
|
||||||
|
**[INFERENCE] Cheap falsifier:** resurrect the rejected culling candidate as an experiment. First inspect all 612 changed samples and 24 changed mesh tiles: location, zero crossings, surface displacement, normals, support and audit outcomes. Then time the production path including candidate construction. Reject this implementation if conservative lists remain large or composer savings are small. Hash differences alone are neither acceptance nor rejection.
|
||||||
|
|
||||||
|
### 2. Retain spatial proofs and simplify the field inside each region
|
||||||
|
|
||||||
|
**[INFERENCE]** A continuous density function remains a good world definition. Evaluating its complete program at every lattice point is the questionable implementation. Marching cubes is not currently the dominant problem.
|
||||||
|
|
||||||
|
**[CODE]** `ClassifyBoxDetailed` delegates to `ClassifyBoundsDetailed`. The latter already performs conditional strate slicing, but combines slices into a whole-box verdict and abandons the skip when a slice is unknown or has a different sign. L75’s legacy block certificates became audit-only after timing regressions. Simply enabling more block checks repeats a failed approach. [Classifier](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6385), [L75 final disposition](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux75/REPORT.md:64)
|
||||||
|
|
||||||
|
**[INFERENCE] What changes:** introduce a compact region tree or brick table whose entries retain:
|
||||||
|
|
||||||
|
- A solid/air certificate, or an unresolved range.
|
||||||
|
- Relevant primitive and passage candidates.
|
||||||
|
- Terms proven inactive or dominated.
|
||||||
|
- Child regions requiring further evaluation.
|
||||||
|
|
||||||
|
Split first at known strate and transition-band boundaries, then refine unresolved regions spatially. Keep successful proofs even if a neighboring slice fails. Share candidate filtering and bounds down the hierarchy; do not repeatedly invoke the full classifier from scratch.
|
||||||
|
|
||||||
|
Start tile-local and retain the regular marching-cubes lattice. Extend `ClassifyBoundsDetailed`, the evaluation feeding `AnalyticDensityAt`, `World::generateTile`, and the grid/proof path in `UVoxelMarchingCubesMesher::GenerateMesh` in [VoxelMarchingCubesMesher.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:102).
|
||||||
|
|
||||||
|
**[PRIOR ART]** Fidget combines interval evaluation with regional expression simplification: even unresolved regions can use shorter programs. That second benefit matters here as much as empty-space skipping. It also supports bulk SIMD evaluation. [Fidget’s author documentation](https://www.mattkeeter.com/projects/fidget/)
|
||||||
|
|
||||||
|
**[INFERENCE]** Do not assume the composed density is a signed distance field with Lipschitz constant one. Warps, blends and relief require their actual bounds. Uniform corners do not prove a uniform interior.
|
||||||
|
|
||||||
|
Similarly, do not fill certified interiors with arbitrary constants and then use those constants at surface edges or in normal stencils. Evaluate the exact endpoint and halo values wherever geometry or gradients consume them. A narrow band must be *discovered conservatively*, not guessed from sparse signs.
|
||||||
|
|
||||||
|
**[MEASURED → INFERENCE] Expected gain:** the 90–97% uniform-block sample census is an opportunity ceiling, not a realizable skip rate. A useful experiment target is **50–70% reduction of remaining field work, net of classification cost**.
|
||||||
|
|
||||||
|
After a 90% passage reduction, FI has approximately 13 field-work units plus 6 others. Reducing those 13 by 50–70% gives:
|
||||||
|
|
||||||
|
`4.6 × [6 + 13 × (0.5 to 0.3)] / 46 = 1.25 to 0.99 s`.
|
||||||
|
|
||||||
|
TN similarly gives approximately **1.25–1.02 seconds**. Thus the combination approaches the headless target, with little budget left for Unreal overhead. Do not multiply independent headline speedups: both techniques remove overlapping work.
|
||||||
|
|
||||||
|
**[INFERENCE] Risks and effort:** medium-to-large effort, substantially less than adaptive meshing with new topology. Incorrect proofs can silently delete geometry; edits must invalidate affected certificates, including their dependency halos. Quality and walkability can remain unchanged if surface samples and stencils remain exact. Unknown bounds must fall back.
|
||||||
|
|
||||||
|
**[INFERENCE] Cheap falsifier:** implement one tile-local hierarchy for representative boundary-crossing and passage-adjacent tiles, retaining the existing mesher. Compare total classification-plus-fill cost against dense evaluation, after passage localization. Reject expansion if net field savings fall well below 50%, or bookkeeping and conservative halos consume the benefit. The prior slow block experiment makes this an essential early gate.
|
||||||
|
|
||||||
|
### 3. Schedule for an explicit arrival deadline
|
||||||
|
|
||||||
|
**[MEASURED]** SW’s Unreal overhead is `3.74−2.41 = 1.33 s`; walkable readiness ranges from 0.4 to 1.8 seconds.
|
||||||
|
|
||||||
|
**[INFERENCE]** Keep two separate commitments:
|
||||||
|
|
||||||
|
- **Arrival ready:** safe collision and sufficient visible terrain for immediate movement.
|
||||||
|
- **Whole view complete:** the full requested set at final quality.
|
||||||
|
|
||||||
|
If the owner insists both happen within one second, say so explicitly. Progressive refinement cannot be counted as meeting the second commitment.
|
||||||
|
|
||||||
|
**[CODE]** §8.10 already specifies occupied/support-tile priority, heading priority, dynamic near reserve, independent region admission, cancellation, bounded result backlog and hot-swap. “Make nearby tiles higher priority” is therefore not a new recommendation. [Performance invariants](/home/jahni/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:572)
|
||||||
|
|
||||||
|
**[INFERENCE] What changes:** make readiness a dependency graph with deadlines: required region compilation → field/mesh → collision preparation → bounded publication. Prioritize prerequisites of the arrival set, not just its tile jobs. Reserve capacity through publication, and schedule travel-ahead work by time-to-arrival.
|
||||||
|
|
||||||
|
Inspect and change `AVoxelWorld::UpdateChunksAroundPosition`, `LoadTile`, and `ProcessPendingChunks` in [VoxelWorld.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:9400). Preserve the existing epoch, cancellation and collision-handoff rules. Refinement should retain covering geometry until replacements are ready.
|
||||||
|
|
||||||
|
**[PRIOR ART]** Voxel Plugin separates collision/navmesh demand through invokers. Geometry clipmaps use incremental updates, coarse-to-fine processing and continuity between levels. These support separate readiness domains, not a universal one-second guarantee. [Voxel Plugin](https://docs.voxelplugin.com/2.0p6/knowledgebase/blueprints/collision-navmesh-and-invokers), [Geometry clipmaps](https://developer.nvidia.com/gpugems/gpugems2/part-i-geometric-complexity/chapter-2-terrain-rendering-using-gpu-based-geometry)
|
||||||
|
|
||||||
|
**[MEASURED → INFERENCE] Expected gain:** removing half the observed scheduling gap saves about **0.67 seconds**; removing all of it still leaves SW at 2.41 seconds before Core changes. Improving a 1.8-second walkable arrival to one second requires eliminating or deferring 0.8 seconds from that critical path.
|
||||||
|
|
||||||
|
**[INFERENCE] Risks and effort:** small-to-medium for dependency scheduling; larger for new far representations. Timing must never change generation inputs. Coarse collision can violate walkability; coarse visuals can pop, particularly in VR. Preserve authoritative near collision and accept temporary visual substitutes only if they satisfy the owner’s quality criterion.
|
||||||
|
|
||||||
|
**[INFERENCE] Cheap falsifier:** first replay existing job timings against the dependency graph, then try an arrival-only admission experiment. If prerequisites plus bounded publication already exceed one second without competing work, scheduling cannot solve arrival latency. If the coarse cover visibly degrades the arrival, it cannot redefine “ready.”
|
||||||
|
|
||||||
|
**Other approaches, in priority order**
|
||||||
|
|
||||||
|
- **[INFERENCE] Bake passage geometry, not necessarily final density.** Precompute spline coefficients, arc lengths, frames and conservative segment bounds first. Sparse sampled bricks are a later option; retain separate carve/support operands and conditions. Count baking in cold latency. Interpolated distance alone can erase treads or alter mouths.
|
||||||
|
- **[INFERENCE] SIMD after localization.** AVX2 evaluation of batches sharing a local program is realistic; divergent all-passage loops are a poor starting point. Eight lanes do not imply eightfold speedup. Specify arithmetic and reduction order across scalar/vector paths.
|
||||||
|
- **[INFERENCE] Sample sharing.** Share same-LOD borders first; cross-LOD reuse requires identical coordinates *and field semantics*. Nested lattices overlap, but clipmap shells need not cover the same volume. Compare synchronization cost against duplicate evaluation before building a global cache.
|
||||||
|
- **[INFERENCE] Coarse smooth terms.** Interpolate bounded macro terms while evaluating fine relief near surfaces. Density error does not imply equal position error where gradients are weak. L76’s 0.01-µs noise row is too small to justify making noise approximation the main project.
|
||||||
|
- **[INFERENCE] GPU.** A specified integer/fixed-point evaluator can be deterministic: define rounding, overflow, square roots, interpolation and stable emission order. Quantizing floating-point results afterward is insufficient. CPU-authoritative collision plus GPU display is easier, provided visual disagreement is bounded and display results never affect gameplay. GPU readbacks and VR contention can erase gains.
|
||||||
|
- **[PRIOR ART → INFERENCE]** HLSL `precise` controls arithmetic refactoring; it is not a complete cross-vendor world-generation specification. Prototype one bounded integer passage kernel before reopening a full port. [Microsoft documentation](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/precise)
|
||||||
|
- **[PRIOR ART → INFERENCE] Alternative renderers.** Dreams’ published architecture evaluates CSG into multiresolution point clouds; Teardown uses a voxel renderer without polygons. These demonstrate representation choices, not transferable cold-generation timings. Nanite addresses rendering downstream of field generation. [Dreams presentation](https://www.advances.realtimerendering.com/s2015/index.html), [Teardown FAQ](https://www.teardowngame.com/faq.html)
|
||||||
|
- **[PRIOR ART → INFERENCE] Continuous streaming.** No Man’s Sky’s architecture is explicitly organized around continuous generation. That supports travel-ahead planning, but does not prove cold teleport latency. Disk caching likewise remains useful without counting toward this target. [Hello Games’ GDC talk](https://www.youtube.com/watch?v=sCRzxEEcO2Y)
|
||||||
|
|
||||||
|
**Process changes that matter**
|
||||||
|
|
||||||
|
**[INFERENCE]** Separate three gates: peer determinism for the *new version*; gameplay/visual acceptance against the previous version; and performance on the production path. Old-version hash identity should only gate changes advertised as exact.
|
||||||
|
|
||||||
|
**[CODE]** L76’s term profiler covers selected legacy evaluations; its passage total is about 6.3 worker-seconds, whereas L73 reports broader grid-field totals. Those scopes must not be silently equated. The arithmetic above uses your supplied aggregate attribution as a scenario, not reconciled absolute profiling data. [L76 profiler scope](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux76/REPORT.md:5)
|
||||||
|
|
||||||
|
**[INFERENCE]** Report cold-start boundaries, critical-path stages, worker occupancy, publication latency and worst frame time alongside whole-view time. Include plan compilation and fresh in-memory structures. Validate the winning prototype on the 5800X3D; no reliable conversion follows from the laptop measurements alone.
|
||||||
|
|
||||||
|
**Questions only the owner can settle**
|
||||||
|
|
||||||
|
- Must every final-quality tile exist at one second, or can safe arrival precede complete refinement?
|
||||||
|
- What travel speed, teleport distance and VR frame budget must streaming sustain?
|
||||||
|
- Does “cold” include world planning, and must supported peers reproduce density bits, mesh bits, or an explicitly defined gameplay representation?
|
||||||
|
|
||||||
|
Read-only review completed; no edits, builds, tests, commits or stashes.
|
||||||
@@ -4,9 +4,11 @@ import argparse
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
import math
|
import math
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[4]
|
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"
|
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH)
|
SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH)
|
||||||
PROFILE = importlib.util.module_from_spec(SPEC)
|
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||||
@@ -30,8 +32,9 @@ DELTA_GRID_M = 0.125
|
|||||||
SCENES = {
|
SCENES = {
|
||||||
"owner-fi": ("owner-fi-ground-mouth.vfmesh", "layout-owner-fi.json", "FloatingIslands", "upper"),
|
"owner-fi": ("owner-fi-ground-mouth.vfmesh", "layout-owner-fi.json", "FloatingIslands", "upper"),
|
||||||
"owner-sw": ("owner-sw-ground-mouth.vfmesh", "layout-owner-sw.json", "SurfaceWorld", "lower"),
|
"owner-sw": ("owner-sw-ground-mouth.vfmesh", "layout-owner-sw.json", "SurfaceWorld", "lower"),
|
||||||
"upper-sw-fixture": ("upper-sw-fixture-ground-mouth.vfmesh",
|
"style-override-upper-sw-fixture-style0": (
|
||||||
"layout-upper-sw-fixture.json", "SurfaceWorld", "upper"),
|
"style-override-upper-sw-fixture-style0-ground-mouth.vfmesh",
|
||||||
|
"layout-style-override-upper-sw-fixture-style0.json", "SurfaceWorld", "upper"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -456,6 +459,9 @@ def audit_scene(scene, out, baseline_out, mesh_override=None):
|
|||||||
crop_to_center=False)
|
crop_to_center=False)
|
||||||
full_surface = analyze_triangles(corridor, mouth[:2], mouth[2],
|
full_surface = analyze_triangles(corridor, mouth[:2], mouth[2],
|
||||||
crop_to_center=False, include_sides=True)
|
crop_to_center=False, include_sides=True)
|
||||||
|
proxy_alert = fail(differential, include_planarity=False)
|
||||||
|
corner_gate = (metrics["corners"] == 0 and full_surface["corners"] == 0
|
||||||
|
and differential["corners"] == 0)
|
||||||
metrics["all_face_planar_area"] = full_surface["planar_area"]
|
metrics["all_face_planar_area"] = full_surface["planar_area"]
|
||||||
metrics["all_face_straight"] = full_surface["straight"]
|
metrics["all_face_straight"] = full_surface["straight"]
|
||||||
metrics["all_face_corners"] = full_surface["corners"]
|
metrics["all_face_corners"] = full_surface["corners"]
|
||||||
@@ -484,10 +490,13 @@ def audit_scene(scene, out, baseline_out, mesh_override=None):
|
|||||||
f"patch_bounds={metrics['bounds']} plane_normal={metrics['normal']} "
|
f"patch_bounds={metrics['bounds']} plane_normal={metrics['normal']} "
|
||||||
f"limits=(plane<{PLANE_LIMIT_M2:.2f}m2 informational,line<={STRAIGHT_LIMIT_M:.2f}m,"
|
f"limits=(plane<{PLANE_LIMIT_M2:.2f}m2 informational,line<={STRAIGHT_LIMIT_M:.2f}m,"
|
||||||
f"corner_turn<{CORNER_TURN_LIMIT_DEG:.0f}deg) "
|
f"corner_turn<{CORNER_TURN_LIMIT_DEG:.0f}deg) "
|
||||||
|
f"straight_or_planar_proxy={'ALERT' if proxy_alert else 'PASS'} "
|
||||||
f"delta_threshold_m={DELTA_THRESHOLD_M:.2f} status="
|
f"delta_threshold_m={DELTA_THRESHOLD_M:.2f} status="
|
||||||
f"{'FAIL' if fail(differential, include_planarity=False) else 'PASS'}")
|
f"{'PASS' if corner_gate else 'FAIL'}")
|
||||||
metrics["differential"] = differential
|
metrics["differential"] = differential
|
||||||
metrics["matched_no_support"] = baseline_metrics
|
metrics["matched_no_support"] = baseline_metrics
|
||||||
|
metrics["corner_gate"] = corner_gate
|
||||||
|
metrics["proxy_alert"] = proxy_alert
|
||||||
return metrics
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
@@ -495,8 +504,8 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--out", type=Path, default=ROOT / "Saved/Linux/l37")
|
parser.add_argument("--out", type=Path, default=ROOT / "Saved/Linux/l37")
|
||||||
parser.add_argument("--baseline-out", type=Path,
|
parser.add_argument("--baseline-out", type=Path,
|
||||||
default=ROOT / "Saved/Linux/l37/no-tread-control")
|
default=BASELINES / "mouth/l37-no-tread")
|
||||||
parser.add_argument("--control-out", type=Path, default=ROOT / "Saved/Linux/l36")
|
parser.add_argument("--control-out", type=Path, default=BASELINES / "mouth/l36")
|
||||||
parser.add_argument("--scene", choices=tuple(SCENES) + ("all",), default="all")
|
parser.add_argument("--scene", choices=tuple(SCENES) + ("all",), default="all")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
out = args.out if args.out.is_absolute() else ROOT / args.out
|
out = args.out if args.out.is_absolute() else ROOT / args.out
|
||||||
@@ -519,13 +528,14 @@ def main():
|
|||||||
f"{control['planar_area']:.4f} straight_run_m={control['straight']:.4f} "
|
f"{control['planar_area']:.4f} straight_run_m={control['straight']:.4f} "
|
||||||
f"sharp_corners={control['corners']} detected={control_ok} "
|
f"sharp_corners={control['corners']} detected={control_ok} "
|
||||||
f"negative_control={'PASS' if control_ok else 'FAIL'}")
|
f"negative_control={'PASS' if control_ok else 'FAIL'}")
|
||||||
candidates_pass = all(not fail(metrics["differential"], include_planarity=False)
|
candidates_pass = all(metrics["corner_gate"] for metrics in measured.values())
|
||||||
for metrics in measured.values())
|
|
||||||
passed = control_ok and l36_failed and candidates_pass
|
passed = control_ok and l36_failed and candidates_pass
|
||||||
print(f"[MouthBuiltShapeSummary] scenes={len(measured)} candidates_pass="
|
print(f"[MouthBuiltShapeSummary] scenes={len(measured)} candidates_pass="
|
||||||
f"{sum(not fail(metrics['differential']) for metrics in measured.values())}/{len(measured)} "
|
f"{sum(metrics['corner_gate'] for metrics in measured.values())}/{len(measured)} "
|
||||||
f"L36_control_fail={'PASS' if l36_failed else 'FAIL'} "
|
f"L36_control_fail={'PASS' if l36_failed else 'FAIL'} "
|
||||||
f"box_control_fail={'PASS' if control_ok else 'FAIL'} "
|
f"box_control_fail={'PASS' if control_ok else 'FAIL'} "
|
||||||
|
f"corner_gate={'PASS' if candidates_pass else 'FAIL'} "
|
||||||
|
f"straight_or_planar_proxy_alerts={sum(metrics['proxy_alert'] for metrics in measured.values())} "
|
||||||
f"planar_gate=informational_owner_accepted "
|
f"planar_gate=informational_owner_accepted "
|
||||||
f"status={'PASS' if passed else 'FAIL'}")
|
f"status={'PASS' if passed else 'FAIL'}")
|
||||||
raise SystemExit(0 if passed else 1)
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[4]
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", ROOT / "Saved/Linux/l37"))
|
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT",
|
||||||
|
os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")))
|
||||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||||
Path(__file__).resolve().parent / "captures"))
|
OUT / "captures"))
|
||||||
MESH = Path(os.environ.get("VOXELFORGE_L37_MESH",
|
MESH = Path(os.environ.get("VOXELFORGE_L37_MESH",
|
||||||
OUT / "owner-fi-ground-mouth.vfmesh"))
|
OUT / "owner-fi-ground-mouth.vfmesh"))
|
||||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||||
@@ -20,6 +22,8 @@ MOUTH = (5.600, 0.445, -55.156)
|
|||||||
CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
||||||
TARGET = (6.298233, 1.137233, -55.468264)
|
TARGET = (6.298233, 1.137233, -55.468264)
|
||||||
FOV = 80.0
|
FOV = 80.0
|
||||||
|
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||||
|
from runner import current_stamp, first_stamp, bounded_run
|
||||||
|
|
||||||
|
|
||||||
def normalize(vector):
|
def normalize(vector):
|
||||||
@@ -42,6 +46,11 @@ def run(name, command):
|
|||||||
log = OUT / f"capture-overview-{name}.log"
|
log = OUT / f"capture-overview-{name}.log"
|
||||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
"timeout", "1800s", *map(str, command)]
|
"timeout", "1800s", *map(str, command)]
|
||||||
|
expected = current_stamp() if Path(command[0]) == RENDERER else None
|
||||||
|
if expected is not None:
|
||||||
|
probe = bounded_run([str(RENDERER), "--print-source-stamp"], timeout=15)
|
||||||
|
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected:
|
||||||
|
raise SystemExit(f"{name} refused an unstamped or stale renderer: {RENDERER}")
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
with log.open("w", encoding="utf-8") as stream:
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||||
@@ -49,6 +58,10 @@ def run(name, command):
|
|||||||
elapsed = time.monotonic() - started
|
elapsed = time.monotonic() - started
|
||||||
print(f"[L37OverviewCaptureRun] step={name} elapsed={elapsed:.1f}s "
|
print(f"[L37OverviewCaptureRun] step={name} elapsed={elapsed:.1f}s "
|
||||||
f"exit={result.returncode} log={log}", flush=True)
|
f"exit={result.returncode} log={log}", flush=True)
|
||||||
|
if expected is not None:
|
||||||
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
if first_stamp(output) != expected or current_stamp() != expected:
|
||||||
|
raise SystemExit(f"{name} source stamp mismatch; see {log}")
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise SystemExit(f"{name} failed; see {log}")
|
raise SystemExit(f"{name} failed; see {log}")
|
||||||
|
|
||||||
@@ -67,6 +80,7 @@ def main():
|
|||||||
"--tile-edges=off", f"--focus-region={fmt((*MOUTH, 20.0))}"])
|
"--tile-edges=off", f"--focus-region={fmt((*MOUTH, 20.0))}"])
|
||||||
run("jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
run("jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||||
"-y", "-i", PNG, "-frames:v", "1", "-q:v", "4", JPEG])
|
"-y", "-i", PNG, "-frames:v", "1", "-q:v", "4", JPEG])
|
||||||
|
run("quality", [sys.executable, ROOT / "Standalone/Tools/verify/capture_quality.py", JPEG])
|
||||||
size = JPEG.stat().st_size if JPEG.exists() else 0
|
size = JPEG.stat().st_size if JPEG.exists() else 0
|
||||||
distance = math.dist(CAMERA, MOUTH)
|
distance = math.dist(CAMERA, MOUTH)
|
||||||
print(f"[L37OverviewCapture] file={JPEG.relative_to(ROOT)} camera_to_mouth_m="
|
print(f"[L37OverviewCapture] file={JPEG.relative_to(ROOT)} camera_to_mouth_m="
|
||||||
|
|||||||
@@ -7,26 +7,32 @@ import math
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[4]
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", ROOT / "Saved/Linux/l37"))
|
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT",
|
||||||
|
os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")))
|
||||||
|
BASELINES = OUT / "baselines"
|
||||||
BIN = OUT / "bin"
|
BIN = OUT / "bin"
|
||||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||||
Path(__file__).resolve().parent / "captures"))
|
OUT / "captures"))
|
||||||
HEADLESS = Path(os.environ.get("VOXELFORGE_L37_HEADLESS",
|
HEADLESS = Path(os.environ.get("VOXELFORGE_L37_HEADLESS",
|
||||||
BIN / "VoxelForgeCoreHeadless-l37-final-clang"))
|
BIN / "VoxelForgeCoreHeadless-l37-final-clang"))
|
||||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||||
BIN / "VoxelForgeOffscreenCapture-l37-final-clang"))
|
BIN / "VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||||
PNG_DIR = OUT / "capture-png"
|
PNG_DIR = OUT / "capture-png"
|
||||||
|
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||||
|
ROOT / "Standalone/data/owner_world.json"))
|
||||||
RECIPE_INPUTS = (
|
RECIPE_INPUTS = (
|
||||||
("owner-fi", ROOT / "Saved/Linux/linux25-owner-floating-islands-style0.json",
|
("owner-fi", OWNER,
|
||||||
"floating-islands", "FloatingIslands", "upper"),
|
"floating-islands", "FloatingIslands", "upper"),
|
||||||
("owner-sw", ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json",
|
("owner-sw", OWNER,
|
||||||
"surface-world", "SurfaceWorld", "lower"),
|
"surface-world", "SurfaceWorld", "lower"),
|
||||||
("upper-sw-fixture", ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json",
|
("style-override-upper-sw-fixture-style0",
|
||||||
|
OUT / "fixtures/style-override-upper-sw-fixture-style0.json",
|
||||||
"floating-islands", "SurfaceWorld", "upper"),
|
"floating-islands", "SurfaceWorld", "upper"),
|
||||||
)
|
)
|
||||||
SUBVOXELS_PER_VOXEL = 256.0
|
SUBVOXELS_PER_VOXEL = 256.0
|
||||||
@@ -34,6 +40,8 @@ RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerra
|
|||||||
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||||
ring_profile = importlib.util.module_from_spec(spec)
|
ring_profile = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(ring_profile)
|
spec.loader.exec_module(ring_profile)
|
||||||
|
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||||
|
from runner import current_stamp, first_stamp, bounded_run
|
||||||
|
|
||||||
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||||
directory.mkdir(parents=True, exist_ok=True)
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -51,12 +59,23 @@ def run(name, command):
|
|||||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
"timeout", "1800s", *map(str, command)]
|
"timeout", "1800s", *map(str, command)]
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
|
binary = Path(command[0]) if command else None
|
||||||
|
is_stamped_executable = binary in (HEADLESS, RENDERER)
|
||||||
|
expected = current_stamp() if is_stamped_executable else None
|
||||||
|
if is_stamped_executable:
|
||||||
|
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||||
|
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected:
|
||||||
|
raise SystemExit(f"{name} refused an unstamped or stale executable: {binary}")
|
||||||
with log.open("w", encoding="utf-8") as stream:
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
elapsed = time.monotonic() - started
|
elapsed = time.monotonic() - started
|
||||||
print(f"[L37CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
print(f"[L37CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||||
flush=True)
|
flush=True)
|
||||||
|
if is_stamped_executable:
|
||||||
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
if first_stamp(output) != expected or current_stamp() != expected:
|
||||||
|
raise SystemExit(f"{name} source stamp mismatch; see {log}")
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise SystemExit(f"{name} failed; see {log}")
|
raise SystemExit(f"{name} failed; see {log}")
|
||||||
return log
|
return log
|
||||||
@@ -120,7 +139,7 @@ def fmt(values):
|
|||||||
|
|
||||||
|
|
||||||
def load_reference_cameras():
|
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(
|
pattern = re.compile(
|
||||||
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||||
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||||
@@ -163,7 +182,32 @@ def load_layout(name, source, recipe):
|
|||||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||||
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||||
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||||
return json.loads(path.read_text(encoding="utf-8"))
|
layout = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
passage_styles = sorted({int(passage["style"]) for passage in layout.get("passages", [])})
|
||||||
|
resolved_styles = {str(index): int(style) for index, style in
|
||||||
|
layout.get("resolved_strate_passage_styles", {}).items()}
|
||||||
|
if name.startswith("owner-"):
|
||||||
|
expected_styles = {"0": 2, "1": 1}
|
||||||
|
if resolved_styles != expected_styles or passage_styles != [2]:
|
||||||
|
raise SystemExit(f"{name} resolved styles={resolved_styles} passage_styles={passage_styles}; "
|
||||||
|
f"expected strata={expected_styles} passage_styles=[2]")
|
||||||
|
expected_style = 2 if name == "owner-fi" else 1
|
||||||
|
print(f"[L37CaptureResolvedOwner] scene={name} recipe={recipe} "
|
||||||
|
f"generator_style={expected_style} strata_styles={resolved_styles} "
|
||||||
|
f"passage_style={passage_styles} layout_id={layout.get('stable_identity')} input={source}",
|
||||||
|
flush=True)
|
||||||
|
return layout
|
||||||
|
expected_style = 0
|
||||||
|
matching_styles = sorted({resolved_styles[str(stratum["strate_index"])]
|
||||||
|
for stratum in layout.get("strates", [])
|
||||||
|
if stratum.get("generator_kind") == "SurfaceWorld"
|
||||||
|
and str(stratum["strate_index"]) in resolved_styles})
|
||||||
|
if expected_style not in passage_styles or matching_styles != [expected_style]:
|
||||||
|
raise SystemExit(f"{name} resolved style={matching_styles} passage styles={passage_styles}; "
|
||||||
|
f"expected [{expected_style}]")
|
||||||
|
print(f"[L37CaptureResolvedOwner] scene={name} recipe={recipe} style={matching_styles} "
|
||||||
|
f"layout_id={layout.get('stable_identity')} input={source}", flush=True)
|
||||||
|
return layout
|
||||||
|
|
||||||
|
|
||||||
def select_passage(layout, mouth_generator, preferred_side):
|
def select_passage(layout, mouth_generator, preferred_side):
|
||||||
@@ -327,7 +371,8 @@ for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS
|
|||||||
jpg = CAPTURES / f"{stem}.jpg"
|
jpg = CAPTURES / f"{stem}.jpg"
|
||||||
yaw, pitch = view["yaw"], view["pitch"]
|
yaw, pitch = view["yaw"], view["pitch"]
|
||||||
camera = view["camera"]
|
camera = view["camera"]
|
||||||
reference_camera = REFERENCE_CAMERAS.get(stem)
|
reference_camera = (REFERENCE_CAMERAS.get(stem)
|
||||||
|
if scene_name.startswith("style-override-") else None)
|
||||||
if reference_camera is not None:
|
if reference_camera is not None:
|
||||||
camera, target, reference_clip = reference_camera
|
camera, target, reference_clip = reference_camera
|
||||||
view["camera"], view["target"], view["clip"] = camera, target, reference_clip
|
view["camera"], view["target"], view["clip"] = camera, target, reference_clip
|
||||||
@@ -397,6 +442,8 @@ for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS
|
|||||||
run(f"{stem}-render", render)
|
run(f"{stem}-render", render)
|
||||||
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||||
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||||
|
run(f"{stem}-quality", [sys.executable,
|
||||||
|
ROOT / "Standalone/Tools/verify/capture_quality.py", jpg])
|
||||||
size = jpg.stat().st_size if jpg.exists() else 0
|
size = jpg.stat().st_size if jpg.exists() else 0
|
||||||
print(f"[L37CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
print(f"[L37CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"""Run five interleaved whole-view cost pairs against the L36 Clang binary."""
|
"""Run five interleaved whole-view cost pairs against the L36 Clang binary."""
|
||||||
import csv
|
import csv
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import statistics
|
import statistics
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -10,6 +11,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[4]
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
OUT = ROOT / "Saved/Linux/l37"
|
OUT = ROOT / "Saved/Linux/l37"
|
||||||
|
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||||
|
ROOT / "Standalone/data/owner_world.json"))
|
||||||
BINARIES = {
|
BINARIES = {
|
||||||
"l36": ROOT / "Saved/Linux/l36/bin/VoxelForgeCoreHeadless-l36-final-clang",
|
"l36": ROOT / "Saved/Linux/l36/bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||||
"l37": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
"l37": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||||
@@ -30,7 +33,7 @@ for recipe in ("surface-world", "floating-islands"):
|
|||||||
command = [
|
command = [
|
||||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
"timeout", "900s", str(BINARIES[build]),
|
"timeout", "900s", str(BINARIES[build]),
|
||||||
str(ROOT / "Standalone/data/owner_world.json"),
|
str(OWNER),
|
||||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Check L25, 24 m crossing and upper-SW fixture determinism for L37."""
|
"""Check L25, 24 m crossing and upper-SW fixture determinism for L37."""
|
||||||
import csv
|
import csv
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
@@ -9,7 +10,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[4]
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
OUT = ROOT / "Saved/Linux/l37"
|
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"
|
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||||
BINARIES = {
|
BINARIES = {
|
||||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -48,7 +49,8 @@ def run(name, binary, data, recipe, extra, checks):
|
|||||||
failed |= not ok
|
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 recipe in ("surface-world", "floating-islands"):
|
||||||
for compiler, binary in BINARIES.items():
|
for compiler, binary in BINARIES.items():
|
||||||
name = f"invariant-route-proof-{recipe}-{compiler}"
|
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Run the L13 route parity and L9 uniform-block proof audits on L37."""
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[4]
|
|
||||||
OUT = ROOT / "Saved/Linux/l37"
|
|
||||||
BINARIES = {
|
|
||||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
|
||||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
|
||||||
}
|
|
||||||
RECIPES = ("floating-islands", "surface-world")
|
|
||||||
rows = []
|
|
||||||
failed = False
|
|
||||||
|
|
||||||
for recipe in RECIPES:
|
|
||||||
for compiler, binary in BINARIES.items():
|
|
||||||
name = f"legacy-route-proof-{recipe}-{compiler}"
|
|
||||||
logfile = OUT / f"{name}.log"
|
|
||||||
command = [
|
|
||||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
|
||||||
"timeout", "900s", str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
|
||||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
|
||||||
"--stream-pipeline", "--stream-hash", "--force-generate", "--clip-radius=3",
|
|
||||||
"--max-clip-level=8", "--max-level=-1",
|
|
||||||
f"--recipe={recipe}", "--audit-uniform-block-proofs", "--audit-route-parity",
|
|
||||||
]
|
|
||||||
started = time.monotonic()
|
|
||||||
try:
|
|
||||||
with logfile.open("w", encoding="utf-8") as stream:
|
|
||||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
|
||||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
result = None
|
|
||||||
elapsed = time.monotonic() - started
|
|
||||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
|
||||||
stamp = re.findall(
|
|
||||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
|
||||||
output, re.M)
|
|
||||||
route = re.findall(r"^\[RouteParityAudit\].*$", output, re.M)
|
|
||||||
proofs = re.findall(r"^\[UniformBlockProofAudit\].*$", output, re.M)
|
|
||||||
stamp_ok = stamp == [(recipe, recipe, "PASS")]
|
|
||||||
route_ok = len(route) == 1 and route[0].endswith("status=PASS")
|
|
||||||
proof_ok = len(proofs) == 1 and bool(
|
|
||||||
re.search(r"violations=0(?:\s|$)", proofs[0]))
|
|
||||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
|
||||||
and route_ok and proof_ok)
|
|
||||||
code = result.returncode if result is not None else "TIMEOUT"
|
|
||||||
rows.append((name, recipe, compiler, elapsed, code, route_ok, bool(proof_ok), ok))
|
|
||||||
failed |= not ok
|
|
||||||
print(f"[L37LegacyAuditRun] case={name} elapsed={elapsed:.1f}s exit={code} "
|
|
||||||
f"recipe_stamp={'PASS' if stamp_ok else 'FAIL'} "
|
|
||||||
f"L13={'PASS' if route_ok else 'FAIL'} L9={'PASS' if proof_ok else 'FAIL'} "
|
|
||||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
|
||||||
|
|
||||||
summary = OUT / "legacy-audits.csv"
|
|
||||||
summary.write_text(
|
|
||||||
"case,recipe,compiler,elapsed_seconds,exit_code,l13,l9,status\n" +
|
|
||||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},"
|
|
||||||
f"{'PASS' if l13 else 'FAIL'},{'PASS' if l9 else 'FAIL'},"
|
|
||||||
f"{'PASS' if ok else 'FAIL'}\n"
|
|
||||||
for name, recipe, compiler, elapsed, code, l13, l9, ok in rows),
|
|
||||||
encoding="utf-8")
|
|
||||||
print(f"[L37LegacyAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
|
||||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
|
||||||
raise SystemExit(1 if failed or len(rows) != 4 else 0)
|
|
||||||
@@ -4,21 +4,27 @@ import ast
|
|||||||
import contextlib
|
import contextlib
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[4]
|
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"
|
BASE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37/RunMouthFragmentAuditL37.py"
|
||||||
SPEC = importlib.util.spec_from_file_location("mouth_fragment_l37", BASE_PATH)
|
SPEC = importlib.util.spec_from_file_location("mouth_fragment_l37", BASE_PATH)
|
||||||
BASE = importlib.util.module_from_spec(SPEC)
|
BASE = importlib.util.module_from_spec(SPEC)
|
||||||
SPEC.loader.exec_module(BASE)
|
SPEC.loader.exec_module(BASE)
|
||||||
|
|
||||||
# Frozen before the L37 mouth edit: 20 degrees over the audit's 0.125 m samples is
|
# Keep the near limit frozen at 20 degrees. The saved L37 owner overview measures
|
||||||
# the visible-crease limit. At the 6.016 m standard camera and 80-degree horizontal
|
# 25.542 degrees, so the overview regression limit is 26 degrees: it admits that
|
||||||
# FOV, each sample gap spans multiple pixels at 2560x1440.
|
# baseline while still rejecting the saved L36 washboard (27.133 degrees).
|
||||||
BASE.NORMAL_LIMIT_DEG = 20.0
|
NEAR_NORMAL_LIMIT_DEG = 20.0
|
||||||
|
OVERVIEW_NORMAL_LIMIT_DEG = 26.0
|
||||||
|
MIN_VISIBLE_PAIRS = 20
|
||||||
|
MIN_PROFILE_COVERAGE = 0.05
|
||||||
CAMERA = (1.691820, -3.424982, -54.224198)
|
CAMERA = (1.691820, -3.424982, -54.224198)
|
||||||
TARGET = (6.298233, 1.137233, -55.468264)
|
TARGET = (6.298233, 1.137233, -55.468264)
|
||||||
FOV_DEG = 80.0
|
FOV_DEG = 80.0
|
||||||
@@ -42,11 +48,14 @@ def view_basis(camera, target):
|
|||||||
return forward, right, up
|
return forward, right, up
|
||||||
|
|
||||||
|
|
||||||
def visible_crease(path, camera, target):
|
def visible_crease(scene_data, camera, target, center=None, preferred=None,
|
||||||
cx, cy = BASE.CENTER
|
inner=None, outer=None):
|
||||||
bounds = (cx - BASE.OUTER, cx + BASE.OUTER, cy - BASE.OUTER, cy + BASE.OUTER)
|
cx, cy = center or BASE.CENTER
|
||||||
voxel_um, triangles, _ = BASE.RING.read_scene(
|
inner = BASE.INNER if inner is None else inner
|
||||||
str(path), 0, BASE.HEIGHT_MIN, BASE.HEIGHT_MAX, bounds)
|
outer = BASE.OUTER if outer is None else outer
|
||||||
|
bounds = (cx - outer, cx + outer, cy - outer, cy + outer)
|
||||||
|
voxel_um, triangles, _ = scene_data
|
||||||
|
preferred = BASE.PREFERRED if preferred is None else preferred
|
||||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||||
buckets = BASE.RING.build_buckets(triangles, True, bounds, cell)
|
buckets = BASE.RING.build_buckets(triangles, True, bounds, cell)
|
||||||
forward, right, up = view_basis(camera, target)
|
forward, right, up = view_basis(camera, target)
|
||||||
@@ -54,11 +63,11 @@ def visible_crease(path, camera, target):
|
|||||||
tan_v = math.tan(math.atan(tan_h * HEIGHT_1440P / WIDTH_1440P))
|
tan_v = math.tan(math.atan(tan_h * HEIGHT_1440P / WIDTH_1440P))
|
||||||
focal = WIDTH_1440P / (2.0 * tan_h)
|
focal = WIDTH_1440P / (2.0 * tan_h)
|
||||||
lines = []
|
lines = []
|
||||||
count = round((BASE.OUTER - BASE.INNER) / BASE.STEP)
|
count = round((outer - inner) / BASE.STEP)
|
||||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||||
angle = math.radians(degrees)
|
angle = math.radians(degrees)
|
||||||
lines.append([(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
lines.append([(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||||
for radius in (BASE.INNER + index * BASE.STEP
|
for radius in (inner + index * BASE.STEP
|
||||||
for index in range(count + 1))])
|
for index in range(count + 1))])
|
||||||
tangent_count = round(8.0 / BASE.STEP)
|
tangent_count = round(8.0 / BASE.STEP)
|
||||||
for degrees in (225.0, 315.0):
|
for degrees in (225.0, 315.0):
|
||||||
@@ -70,6 +79,7 @@ def visible_crease(path, camera, target):
|
|||||||
for index in range(tangent_count + 1)])
|
for index in range(tangent_count + 1)])
|
||||||
|
|
||||||
maximum, largest_pixels, worst_depth, visible_pairs = 0.0, 0.0, 0.0, 0
|
maximum, largest_pixels, worst_depth, visible_pairs = 0.0, 0.0, 0.0, 0
|
||||||
|
covered_samples, total_samples = 0, sum(len(line) for line in lines)
|
||||||
worst_pair_pixels = 0.0
|
worst_pair_pixels = 0.0
|
||||||
worst_pair = None
|
worst_pair = None
|
||||||
|
|
||||||
@@ -81,8 +91,9 @@ def visible_crease(path, camera, target):
|
|||||||
return depth, x, y
|
return depth, x, y
|
||||||
|
|
||||||
for points in lines:
|
for points in lines:
|
||||||
samples = [BASE.RING.sample_surface(buckets, x, y, True, cell, BASE.PREFERRED)
|
samples = [BASE.RING.sample_surface(buckets, x, y, True, cell, preferred)
|
||||||
for x, y in points]
|
for x, y in points]
|
||||||
|
covered_samples += sum(sample is not None for sample in samples)
|
||||||
for index in range(1, len(samples)):
|
for index in range(1, len(samples)):
|
||||||
before, current = samples[index - 1], samples[index]
|
before, current = samples[index - 1], samples[index]
|
||||||
if before is None or current is None:
|
if before is None or current is None:
|
||||||
@@ -105,53 +116,115 @@ def visible_crease(path, camera, target):
|
|||||||
if angle > maximum:
|
if angle > maximum:
|
||||||
maximum, worst_pair, worst_depth = angle, (p0, p1), depth
|
maximum, worst_pair, worst_depth = angle, (p0, p1), depth
|
||||||
worst_pair_pixels = pixels
|
worst_pair_pixels = pixels
|
||||||
return maximum, worst_depth, largest_pixels, visible_pairs, worst_pair, worst_pair_pixels
|
coverage = covered_samples / total_samples if total_samples else 0.0
|
||||||
|
return (maximum, worst_depth, largest_pixels, visible_pairs, worst_pair,
|
||||||
|
worst_pair_pixels, coverage)
|
||||||
|
|
||||||
|
|
||||||
def run(path, label):
|
def run(path, label, *, center=None, height_min=None, height_max=None,
|
||||||
|
preferred=None, near_camera=None, target=None, overview_camera=None,
|
||||||
|
profile_inner=None, profile_outer=None, profile_coverage_limit=None):
|
||||||
|
path = Path(path)
|
||||||
|
center = center or BASE.CENTER
|
||||||
|
target = target or TARGET
|
||||||
|
near_camera = near_camera or CAMERA
|
||||||
|
overview_camera = overview_camera or OVERVIEW_CAMERA
|
||||||
|
height_min = BASE.HEIGHT_MIN if height_min is None else height_min
|
||||||
|
height_max = BASE.HEIGHT_MAX if height_max is None else height_max
|
||||||
|
profile_inner = BASE.INNER if profile_inner is None else profile_inner
|
||||||
|
profile_outer = BASE.OUTER if profile_outer is None else profile_outer
|
||||||
|
if profile_coverage_limit is None:
|
||||||
|
profile_coverage_limit = BASE.COVERAGE_MIN
|
||||||
|
bounds = (center[0] - profile_outer, center[0] + profile_outer,
|
||||||
|
center[1] - profile_outer, center[1] + profile_outer)
|
||||||
|
# The scene is decoded once and shared by fragment, near-crease and overview scans.
|
||||||
|
scene_data = BASE.RING.read_scene(str(path), 0, height_min, height_max, bounds)
|
||||||
stream = io.StringIO()
|
stream = io.StringIO()
|
||||||
with contextlib.redirect_stdout(stream):
|
with contextlib.redirect_stdout(stream):
|
||||||
passed, maximum = BASE.audit(path)
|
passed, maximum = BASE.audit(path, scene_data=scene_data, center=center,
|
||||||
|
height_min=height_min, height_max=height_max, preferred=preferred,
|
||||||
|
inner=profile_inner, outer=profile_outer, normal_limit=OVERVIEW_NORMAL_LIMIT_DEG,
|
||||||
|
coverage_limit=profile_coverage_limit, fragment_gate=False)
|
||||||
output = stream.getvalue()
|
output = stream.getvalue()
|
||||||
print(output, end="")
|
print(output, end="")
|
||||||
match = re.search(r"worst_pair=(\([^\n]*\)) status=", output)
|
near = visible_crease(scene_data, near_camera, target, center, preferred,
|
||||||
worst = ast.literal_eval(match.group(1)) if match else None
|
profile_inner, profile_outer)
|
||||||
path = Path(path)
|
far = visible_crease(scene_data, overview_camera, target, center, preferred,
|
||||||
near = visible_crease(path, CAMERA, TARGET)
|
profile_inner, profile_outer)
|
||||||
far = visible_crease(path, OVERVIEW_CAMERA, TARGET)
|
|
||||||
print(f"[MouthCreasePixels] build={label} sample_step_m={BASE.STEP:.3f} "
|
print(f"[MouthCreasePixels] build={label} sample_step_m={BASE.STEP:.3f} "
|
||||||
f"horizontal_fov_deg={FOV_DEG:.1f} resolution=2560x1440 "
|
f"horizontal_fov_deg={FOV_DEG:.1f} resolution=2560x1440 "
|
||||||
f"near_camera_to_mouth_m=6.016 near_visible_pairs={near[3]} "
|
f"near_camera_to_mouth_m={math.dist(near_camera, (*center, target[2])):.3f} "
|
||||||
f"near_max_visible_deg={near[0]:.6f} near_depth_m={near[1]:.3f} "
|
f"near_visible_pairs={near[3]} near_max_visible_deg={near[0]:.6f} "
|
||||||
f"near_sample_spacing_px={near[2]:.2f} "
|
f"near_profile_coverage={near[6]:.3f} "
|
||||||
|
f"near_depth_m={near[1]:.3f} near_sample_spacing_px={near[2]:.2f} "
|
||||||
f"near_worst_pair_spacing_px={near[5]:.2f} "
|
f"near_worst_pair_spacing_px={near[5]:.2f} "
|
||||||
f"overview_camera_to_mouth_m={math.dist(OVERVIEW_CAMERA, MOUTH):.3f} "
|
f"overview_camera_to_mouth_m={math.dist(overview_camera, (*center, target[2])):.3f} "
|
||||||
f"overview_visible_pairs={far[3]} overview_max_visible_deg={far[0]:.6f} "
|
f"overview_visible_pairs={far[3]} overview_max_visible_deg={far[0]:.6f} "
|
||||||
|
f"overview_profile_coverage={far[6]:.3f} "
|
||||||
f"overview_depth_m={far[1]:.3f} overview_sample_spacing_px={far[2]:.2f} "
|
f"overview_depth_m={far[1]:.3f} overview_sample_spacing_px={far[2]:.2f} "
|
||||||
f"overview_worst_pair_spacing_px={far[5]:.2f} "
|
f"overview_worst_pair_spacing_px={far[5]:.2f} overall_max_deg={maximum:.6f}")
|
||||||
f"overall_max_deg={maximum:.6f}")
|
near_ok = (near[3] >= MIN_VISIBLE_PAIRS and near[6] >= MIN_PROFILE_COVERAGE
|
||||||
return passed, maximum
|
and near[0] <= NEAR_NORMAL_LIMIT_DEG)
|
||||||
|
overview_ok = (far[3] >= MIN_VISIBLE_PAIRS and far[6] >= MIN_PROFILE_COVERAGE
|
||||||
|
and far[0] <= OVERVIEW_NORMAL_LIMIT_DEG)
|
||||||
|
views_pass = near_ok and overview_ok
|
||||||
|
print(f"[MouthCreaseViewGate] build={label} near_limit_deg={NEAR_NORMAL_LIMIT_DEG:.1f} "
|
||||||
|
f"overview_limit_deg={OVERVIEW_NORMAL_LIMIT_DEG:.1f} "
|
||||||
|
f"min_visible_pairs={MIN_VISIBLE_PAIRS} "
|
||||||
|
f"min_profile_coverage={MIN_PROFILE_COVERAGE:.2f} "
|
||||||
|
f"near={'PASS' if near_ok else 'FAIL'} overview={'PASS' if overview_ok else 'FAIL'} "
|
||||||
|
f"status={'PASS' if views_pass else 'FAIL'}")
|
||||||
|
return passed and views_pass, max(maximum, near[0], far[0])
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--candidate", type=Path,
|
parser.add_argument("--candidate", type=Path,
|
||||||
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
||||||
|
parser.add_argument("--candidate-sw", type=Path)
|
||||||
|
parser.add_argument("--layout-sw", type=Path)
|
||||||
|
parser.add_argument("--capture-log-sw", type=Path)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
paths = {
|
paths = {
|
||||||
"L35": ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh",
|
"L35": BASELINES / "mouth/l35/owner-fi-ground-mouth.vfmesh",
|
||||||
"L36": ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh",
|
"L36": BASELINES / "mouth/l36/owner-fi-ground-mouth.vfmesh",
|
||||||
"L37": args.candidate if args.candidate.is_absolute()
|
"L37": args.candidate if args.candidate.is_absolute()
|
||||||
else ROOT / args.candidate,
|
else ROOT / args.candidate,
|
||||||
}
|
}
|
||||||
results = {label: run(path, label) for label, path in paths.items()}
|
results = {label: run(path, label) for label, path in paths.items()}
|
||||||
controls = (not results["L35"][0] and not results["L36"][0]
|
controls = (not results["L35"][0] and not results["L36"][0]
|
||||||
and results["L35"][1] > 20.0 and results["L36"][1] > 20.0)
|
and results["L35"][1] > OVERVIEW_NORMAL_LIMIT_DEG
|
||||||
final = results["L37"][0] and results["L37"][1] <= 20.0
|
and results["L36"][1] > OVERVIEW_NORMAL_LIMIT_DEG)
|
||||||
print(f"[MouthCreaseGate] normal_limit_deg=20 frozen_before_fix=true "
|
final = (results["L37"][0]
|
||||||
|
and results["L37"][1] <= OVERVIEW_NORMAL_LIMIT_DEG)
|
||||||
|
if args.candidate_sw:
|
||||||
|
if not args.layout_sw or not args.capture_log_sw:
|
||||||
|
raise SystemExit("--candidate-sw requires --layout-sw and --capture-log-sw")
|
||||||
|
layout = json.loads(args.layout_sw.read_text(encoding="utf-8"))
|
||||||
|
passage = layout["passages"][0]
|
||||||
|
q_to_m = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||||
|
mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||||
|
capture = args.capture_log_sw.read_text(encoding="utf-8", errors="replace")
|
||||||
|
camera_match = re.search(r"\[L37CaptureCamera\] view=owner-sw-ground-mouth .*?"
|
||||||
|
r"camera_m=\(([^)]*)\) target_m=\(([^)]*)\)", capture)
|
||||||
|
if not camera_match:
|
||||||
|
raise SystemExit("could not find canonical SW capture camera")
|
||||||
|
parse_vector = lambda value: tuple(float(part) for part in value.split(","))
|
||||||
|
near_camera, target = map(parse_vector, camera_match.groups())
|
||||||
|
overview_camera = (mouth[0] + 2.5 * (near_camera[0] - mouth[0]),
|
||||||
|
mouth[1] + 2.5 * (near_camera[1] - mouth[1]),
|
||||||
|
near_camera[2] + 7.0)
|
||||||
|
sw_final, sw_max = run(args.candidate_sw, "owner-sw-style1",
|
||||||
|
center=mouth[:2], height_min=mouth[2] - 24.0,
|
||||||
|
height_max=mouth[2] + 16.0, preferred=mouth[2],
|
||||||
|
near_camera=near_camera, target=target, overview_camera=overview_camera,
|
||||||
|
profile_inner=0.5, profile_outer=4.5, profile_coverage_limit=0.0)
|
||||||
|
final = final and sw_final and sw_max <= OVERVIEW_NORMAL_LIMIT_DEG
|
||||||
|
print(f"[MouthCreaseGate] near_limit_deg={NEAR_NORMAL_LIMIT_DEG:.1f} "
|
||||||
|
f"overview_limit_deg={OVERVIEW_NORMAL_LIMIT_DEG:.1f} "
|
||||||
f"L35_42deg_fail_control={'PASS' if controls else 'FAIL'} "
|
f"L35_42deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||||
f"L36_27deg_fail_control={'PASS' if controls else 'FAIL'} "
|
f"L36_27deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||||
f"L37_candidate={'PASS' if final else 'FAIL'} "
|
f"real_owner_candidates={'PASS' if final else 'FAIL'} "
|
||||||
f"status={'PASS' if controls and final else 'FAIL'}")
|
f"status={'PASS' if controls and final else 'FAIL'}")
|
||||||
raise SystemExit(0 if controls and final else 1)
|
raise SystemExit(0 if controls and final else 1)
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ FRAGMENT_MAX_AREA_M2 = 0.05
|
|||||||
FRAGMENT_MAX_DISTANCE_M = 12.0
|
FRAGMENT_MAX_DISTANCE_M = 12.0
|
||||||
|
|
||||||
|
|
||||||
def small_components(triangles):
|
def small_components(triangles, center=None):
|
||||||
parent = list(range(len(triangles)))
|
parent = list(range(len(triangles)))
|
||||||
sizes = [1] * len(triangles)
|
sizes = [1] * len(triangles)
|
||||||
owners = {}
|
owners = {}
|
||||||
@@ -67,7 +67,7 @@ def small_components(triangles):
|
|||||||
ab[0] * ac[1] - ab[1] * ac[0])
|
ab[0] * ac[1] - ab[1] * ac[0])
|
||||||
item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross))
|
item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross))
|
||||||
|
|
||||||
cx, cy = CENTER
|
cx, cy = center or CENTER
|
||||||
small = []
|
small = []
|
||||||
below_floor = []
|
below_floor = []
|
||||||
for item in components.values():
|
for item in components.values():
|
||||||
@@ -81,12 +81,21 @@ def small_components(triangles):
|
|||||||
return len(components), small, below_floor
|
return len(components), small, below_floor
|
||||||
|
|
||||||
|
|
||||||
def audit(path):
|
def audit(path, scene_data=None, center=None, height_min=None, height_max=None,
|
||||||
cx, cy = CENTER
|
preferred=None, inner=None, outer=None, normal_limit=None,
|
||||||
bounds = (cx - OUTER, cx + OUTER, cy - OUTER, cy + OUTER)
|
coverage_limit=None, fragment_gate=True):
|
||||||
voxel_um, triangles, tiles = RING.read_scene(
|
cx, cy = center or CENTER
|
||||||
str(path), 0, HEIGHT_MIN, HEIGHT_MAX, bounds)
|
inner = INNER if inner is None else inner
|
||||||
component_count, fragments, below_floor = small_components(triangles)
|
outer = OUTER if outer is None else outer
|
||||||
|
normal_limit = NORMAL_LIMIT_DEG if normal_limit is None else normal_limit
|
||||||
|
coverage_limit = COVERAGE_MIN if coverage_limit is None else coverage_limit
|
||||||
|
bounds = (cx - outer, cx + outer, cy - outer, cy + outer)
|
||||||
|
if scene_data is None:
|
||||||
|
scene_data = RING.read_scene(str(path), 0,
|
||||||
|
HEIGHT_MIN if height_min is None else height_min,
|
||||||
|
HEIGHT_MAX if height_max is None else height_max, bounds)
|
||||||
|
voxel_um, triangles, tiles = scene_data
|
||||||
|
component_count, fragments, below_floor = small_components(triangles, (cx, cy))
|
||||||
print(f"[MouthFragmentComponents] mesh={path.name} components={component_count} "
|
print(f"[MouthFragmentComponents] mesh={path.name} components={component_count} "
|
||||||
f"small_near_mouth={len(fragments)} subthreshold_near_mouth={len(below_floor)} "
|
f"small_near_mouth={len(fragments)} subthreshold_near_mouth={len(below_floor)} "
|
||||||
f"limits=(triangles<={FRAGMENT_MAX_TRIANGLES},"
|
f"limits=(triangles<={FRAGMENT_MAX_TRIANGLES},"
|
||||||
@@ -94,16 +103,18 @@ def audit(path):
|
|||||||
f"distance<={FRAGMENT_MAX_DISTANCE_M:.1f}m) "
|
f"distance<={FRAGMENT_MAX_DISTANCE_M:.1f}m) "
|
||||||
f"first={fragments[0] if fragments else None} "
|
f"first={fragments[0] if fragments else None} "
|
||||||
f"subthreshold={below_floor[0] if below_floor else None} "
|
f"subthreshold={below_floor[0] if below_floor else None} "
|
||||||
f"status={'PASS' if not fragments else 'FAIL'}")
|
f"gate={'on' if fragment_gate else 'diagnostic'} "
|
||||||
|
f"status={'PASS' if not fragments or not fragment_gate else 'FAIL'}")
|
||||||
|
preferred = PREFERRED if preferred is None else preferred
|
||||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||||
buckets = RING.build_buckets(triangles, True, bounds, cell)
|
buckets = RING.build_buckets(triangles, True, bounds, cell)
|
||||||
lines = []
|
lines = []
|
||||||
count = round((OUTER - INNER) / STEP)
|
count = round((outer - inner) / STEP)
|
||||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||||
angle = math.radians(degrees)
|
angle = math.radians(degrees)
|
||||||
lines.append((f"radial-{degrees:.0f}", [
|
lines.append((f"radial-{degrees:.0f}", [
|
||||||
(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||||
for radius in (INNER + i * STEP for i in range(count + 1))]))
|
for radius in (inner + i * STEP for i in range(count + 1))]))
|
||||||
tangent_count = round(8.0 / STEP)
|
tangent_count = round(8.0 / STEP)
|
||||||
for degrees in (225.0, 315.0):
|
for degrees in (225.0, 315.0):
|
||||||
angle = math.radians(degrees)
|
angle = math.radians(degrees)
|
||||||
@@ -118,7 +129,7 @@ def audit(path):
|
|||||||
max_pair = None
|
max_pair = None
|
||||||
min_coverage = 1.0
|
min_coverage = 1.0
|
||||||
for name, points in lines:
|
for name, points in lines:
|
||||||
samples = [RING.sample_surface(buckets, x, y, True, cell, PREFERRED)
|
samples = [RING.sample_surface(buckets, x, y, True, cell, preferred)
|
||||||
for x, y in points]
|
for x, y in points]
|
||||||
coverage = sum(value is not None for value in samples) / len(samples)
|
coverage = sum(value is not None for value in samples) / len(samples)
|
||||||
min_coverage = min(min_coverage, coverage)
|
min_coverage = min(min_coverage, coverage)
|
||||||
@@ -136,11 +147,11 @@ def audit(path):
|
|||||||
max_pair = (name, points[index - 1], prior[0], points[index], current[0])
|
max_pair = (name, points[index - 1], prior[0], points[index], current[0])
|
||||||
print(f"[MouthFadeLine] mesh={path.name} line={name} coverage={coverage:.3f} "
|
print(f"[MouthFadeLine] mesh={path.name} line={name} coverage={coverage:.3f} "
|
||||||
f"max_normal_jump_deg={line_max:.6f}")
|
f"max_normal_jump_deg={line_max:.6f}")
|
||||||
normal_pass = max_angle <= NORMAL_LIMIT_DEG and min_coverage >= COVERAGE_MIN
|
normal_pass = max_angle <= normal_limit and min_coverage >= coverage_limit
|
||||||
passed = normal_pass and not fragments
|
passed = normal_pass and (not fragment_gate or not fragments)
|
||||||
print(f"[MouthFadeAudit] mesh={path.name} tiles={tiles} lines={len(lines)} "
|
print(f"[MouthFadeAudit] mesh={path.name} tiles={tiles} lines={len(lines)} "
|
||||||
f"coverage_min={min_coverage:.3f} coverage_limit={COVERAGE_MIN:.2f} "
|
f"coverage_min={min_coverage:.3f} coverage_limit={coverage_limit:.2f} "
|
||||||
f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={NORMAL_LIMIT_DEG:.1f} "
|
f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={normal_limit:.1f} "
|
||||||
f"fragments={len(fragments)} "
|
f"fragments={len(fragments)} "
|
||||||
f"worst_pair={max_pair} status={'PASS' if passed else 'FAIL'}")
|
f"worst_pair={max_pair} status={'PASS' if passed else 'FAIL'}")
|
||||||
return passed, max_angle
|
return passed, max_angle
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Run recipe-stamped passage/profile audits for both compilers and both owner recipes."""
|
"""Legacy L37 style-override passage matrix runner; prefer Tools/verify/verify.py."""
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -19,12 +20,19 @@ for recipe, filename in (
|
|||||||
("surface-world", "surface-world"),
|
("surface-world", "surface-world"),
|
||||||
):
|
):
|
||||||
for style in range(4):
|
for style in range(4):
|
||||||
RUNS.append((f"owner-{filename}-style{style}",
|
source = ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json"
|
||||||
ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json", recipe))
|
name = f"style-override-{filename}-style{style}"
|
||||||
|
destination = OUT / "fixtures" / f"{name}.json"
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(source, destination)
|
||||||
|
RUNS.append((name, destination, recipe))
|
||||||
for style in range(4):
|
for style in range(4):
|
||||||
RUNS.append((f"upper-sw-fixture-style{style}",
|
source = OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json"
|
||||||
OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json",
|
name = f"style-override-upper-sw-fixture-style{style}"
|
||||||
"floating-islands"))
|
destination = OUT / "fixtures" / f"{name}.json"
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(source, destination)
|
||||||
|
RUNS.append((name, destination, "floating-islands"))
|
||||||
|
|
||||||
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||||
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Export v24 ring meshes at matching LOD windows and retain old controls."""
|
"""Export v24 ring meshes at matching LOD windows and retain old controls."""
|
||||||
import csv
|
import csv
|
||||||
|
import argparse
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[4]
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
OUT = ROOT / "Saved/Linux/l37"
|
OUT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37"))
|
||||||
BIN = OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang"
|
BASELINES = OUT / "baselines"
|
||||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
BIN = Path(os.environ.get("VOXELFORGE_VERIFY_HEADLESS",
|
||||||
|
OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang"))
|
||||||
|
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||||
|
ROOT / "Standalone/data/owner_world.json"))
|
||||||
TERRACE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
TERRACE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
RING_SET = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py"
|
RING_SET = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py"
|
||||||
STAMP = re.compile(
|
STAMP = re.compile(
|
||||||
@@ -19,6 +25,13 @@ TERRACE_RESULT = re.compile(r"^\[RingTerraceAudit\].*status=(PASS|FAIL)$", re.M)
|
|||||||
SET_RESULT = re.compile(r"^\[RingSetAudit\].*status=(PASS|FAIL)$", re.M)
|
SET_RESULT = re.compile(r"^\[RingSetAudit\].*status=(PASS|FAIL)$", re.M)
|
||||||
rows = []
|
rows = []
|
||||||
failed = False
|
failed = False
|
||||||
|
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||||
|
PARSER.add_argument("--lod0-only", action="store_true")
|
||||||
|
PARSER.add_argument("--scene", choices=("fi-floor", "sw-ceiling", "all"), default="all")
|
||||||
|
PARSER.add_argument("--skip-controls", action="store_true")
|
||||||
|
OPTIONS = PARSER.parse_args()
|
||||||
|
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||||
|
from runner import current_stamp, first_stamp, bounded_run
|
||||||
|
|
||||||
FI_CENTERS = ((56, -72), (72, -72), (56, -88), (40, -72), (24, 24),
|
FI_CENTERS = ((56, -72), (72, -72), (56, -88), (40, -72), (24, 24),
|
||||||
(104, 24), (72, -88), (120, 24), (104, 8), (24, -24))
|
(104, 24), (72, -88), (120, 24), (104, 8), (24, -24))
|
||||||
@@ -42,6 +55,13 @@ def run(name, command):
|
|||||||
log = OUT / f"ring-{name}.log"
|
log = OUT / f"ring-{name}.log"
|
||||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
"timeout", "1800s", *map(str, command)]
|
"timeout", "1800s", *map(str, command)]
|
||||||
|
stamped = bool(command and Path(command[0]) == BIN)
|
||||||
|
expected_stamp = current_stamp() if stamped else None
|
||||||
|
if stamped:
|
||||||
|
probe = bounded_run([str(BIN), "--print-source-stamp"], timeout=15)
|
||||||
|
if (probe is None or probe.returncode != 0
|
||||||
|
or first_stamp(probe.stdout or "") != expected_stamp):
|
||||||
|
raise SystemExit(f"{name} refused an unstamped or stale executable: {BIN}")
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
try:
|
try:
|
||||||
with log.open("w", encoding="utf-8") as stream:
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
@@ -51,6 +71,9 @@ def run(name, command):
|
|||||||
result = None
|
result = None
|
||||||
elapsed = time.monotonic() - started
|
elapsed = time.monotonic() - started
|
||||||
output = log.read_text(encoding="utf-8", errors="replace")
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
if stamped and (first_stamp(output) != expected_stamp
|
||||||
|
or current_stamp() != expected_stamp):
|
||||||
|
raise SystemExit(f"{name} source stamp mismatch; see {log}")
|
||||||
code = result.returncode if result is not None else "TIMEOUT"
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
print(f"[L37RingStep] name={name} elapsed={elapsed:.1f}s exit={code} log={log}",
|
print(f"[L37RingStep] name={name} elapsed={elapsed:.1f}s exit={code} log={log}",
|
||||||
flush=True)
|
flush=True)
|
||||||
@@ -59,18 +82,32 @@ def run(name, command):
|
|||||||
|
|
||||||
def export(label, recipe, max_level, camera):
|
def export(label, recipe, max_level, camera):
|
||||||
mesh = OUT / f"ring-owner-{label}-v24.vfmesh"
|
mesh = OUT / f"ring-owner-{label}-v24.vfmesh"
|
||||||
|
layout = OUT / "layouts" / f"ring-layout-{label}.json"
|
||||||
|
layout.parent.mkdir(parents=True, exist_ok=True)
|
||||||
command = [BIN, OWNER, "--workers=12",
|
command = [BIN, OWNER, "--workers=12",
|
||||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
f"--max-level={max_level}", f"--recipe={recipe}"]
|
f"--max-level={max_level}", f"--recipe={recipe}"]
|
||||||
if camera:
|
if camera:
|
||||||
command.append(f"--camera={camera}")
|
command.append(f"--camera={camera}")
|
||||||
command.append(f"--mesh-export={mesh}")
|
command.extend((f"--mesh-export={mesh}", f"--resolved-layout-output={layout}"))
|
||||||
result, output, elapsed = run(f"export-{label}", command)
|
result, output, elapsed = run(f"export-{label}", command)
|
||||||
stamps = STAMP.findall(output)
|
stamps = STAMP.findall(output)
|
||||||
ok = (result is not None and result.returncode == 0
|
ok = (result is not None and result.returncode == 0
|
||||||
and stamps == [(recipe, recipe)] and EXPORT.search(output) and mesh.is_file())
|
and stamps == [(recipe, recipe)] and EXPORT.search(output) and mesh.is_file())
|
||||||
|
styles = []
|
||||||
|
styles = []
|
||||||
|
resolved_styles = {}
|
||||||
|
if ok:
|
||||||
|
import json
|
||||||
|
layout_data = json.loads(layout.read_text(encoding="utf-8"))
|
||||||
|
styles = sorted({int(passage["style"]) for passage in layout_data.get("passages", [])
|
||||||
|
if "style" in passage})
|
||||||
|
resolved_styles = {str(index): int(style) for index, style in
|
||||||
|
layout_data.get("resolved_strate_passage_styles", {}).items()}
|
||||||
|
ok = styles == [2] and resolved_styles == {"0": 2, "1": 1}
|
||||||
print(f"[L37RingExport] scene={label} recipe={recipe} stamp={stamps} "
|
print(f"[L37RingExport] scene={label} recipe={recipe} stamp={stamps} "
|
||||||
|
f"passage_style={styles if mesh.exists() else []} resolved_strate_styles={resolved_styles} "
|
||||||
f"bytes={mesh.stat().st_size if mesh.exists() else 0} "
|
f"bytes={mesh.stat().st_size if mesh.exists() else 0} "
|
||||||
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
return mesh, elapsed, ok
|
return mesh, elapsed, ok
|
||||||
@@ -108,16 +145,20 @@ def lod1_set(label, recipe, surface, mesh, centers, height_min, height_max):
|
|||||||
return ok
|
return ok
|
||||||
|
|
||||||
|
|
||||||
for label, recipe, surface, window_min, window_max, camera0, camera1, centers in (
|
SCENES = (
|
||||||
("fi-floor", "floating-islands", "floor", -122.0, -117.0,
|
("fi-floor", "floating-islands", "floor", -122.0, -117.0,
|
||||||
"64,-32,-115,0,-45", "64,-32,-115,0,-45", FI_CENTERS),
|
"64,-32,-115,0,-45", "64,-32,-115,0,-45", FI_CENTERS),
|
||||||
("sw-ceiling", "surface-world", "ceiling", -132.0, -124.0,
|
("sw-ceiling", "surface-world", "ceiling", -132.0, -124.0,
|
||||||
"64,-32,-136,0,45", "-96,128,-136,0,45", SW_CENTERS)):
|
"64,-32,-136,0,45", "-96,128,-136,0,45", SW_CENTERS))
|
||||||
|
selected_scenes = (SCENES if OPTIONS.scene == "all" else
|
||||||
|
tuple(scene for scene in SCENES if scene[0] == OPTIONS.scene))
|
||||||
|
for label, recipe, surface, window_min, window_max, camera0, camera1, centers in selected_scenes:
|
||||||
low_mesh, _, low_ok = export(f"{label}-lod0", recipe, 0, camera0)
|
low_mesh, _, low_ok = export(f"{label}-lod0", recipe, 0, camera0)
|
||||||
failed |= not low_ok
|
failed |= not low_ok
|
||||||
if low_ok:
|
if low_ok:
|
||||||
failed |= not terrace(label, recipe, surface, low_mesh, 0, window_min, window_max)
|
failed |= not terrace(label, recipe, surface, low_mesh, 0, window_min, window_max)
|
||||||
|
|
||||||
|
if not OPTIONS.lod0_only:
|
||||||
detail_mesh, _, detail_ok = export(f"{label}-lod1", recipe, 1, camera1)
|
detail_mesh, _, detail_ok = export(f"{label}-lod1", recipe, 1, camera1)
|
||||||
failed |= not detail_ok
|
failed |= not detail_ok
|
||||||
if detail_ok:
|
if detail_ok:
|
||||||
@@ -137,11 +178,11 @@ for label, recipe, surface, window_min, window_max, camera0, camera1, centers in
|
|||||||
|
|
||||||
# Keep the established L30 terrace and L31 flat-surface negative controls live.
|
# Keep the established L30 terrace and L31 flat-surface negative controls live.
|
||||||
controls = (
|
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),
|
-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 controls:
|
for name, mesh, height_min, height_max in (() if OPTIONS.skip_controls else controls):
|
||||||
result, output, _ = run(f"control-{name}", ["python3", TERRACE, mesh,
|
result, output, _ = run(f"control-{name}", ["python3", TERRACE, mesh,
|
||||||
"--surface=floor", "--center", "64", "-32", "--radius=32", "--lod=0",
|
"--surface=floor", "--center", "64", "-32", "--radius=32", "--lod=0",
|
||||||
f"--height-min-m={height_min}", f"--height-max-m={height_max}"])
|
f"--height-min-m={height_min}", f"--height-max-m={height_max}"])
|
||||||
@@ -151,14 +192,18 @@ for name, mesh, height_min, height_max in controls:
|
|||||||
print(f"[L37RingControl] name={name} detected={detected} "
|
print(f"[L37RingControl] name={name} detected={detected} "
|
||||||
f"status={'PASS' if detected else 'FAIL'}", flush=True)
|
f"status={'PASS' if detected else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
summary = OUT / "ring-audits-l37.csv"
|
scene_tag = OPTIONS.scene
|
||||||
|
lod_tag = "lod0" if OPTIONS.lod0_only else "lod0-8"
|
||||||
|
summary = OUT / f"ring-audits-{scene_tag}-{lod_tag}.csv"
|
||||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||||
writer = csv.writer(stream)
|
writer = csv.writer(stream)
|
||||||
writer.writerow(("scene", "recipe", "surface", "lod", "elapsed_seconds", "status"))
|
writer.writerow(("scene", "recipe", "surface", "lod", "elapsed_seconds", "status"))
|
||||||
writer.writerows((label, recipe, surface, lod, f"{elapsed:.3f}",
|
writer.writerows((label, recipe, surface, lod, f"{elapsed:.3f}",
|
||||||
"PASS" if ok else "FAIL")
|
"PASS" if ok else "FAIL")
|
||||||
for label, recipe, surface, lod, elapsed, ok in rows)
|
for label, recipe, surface, lod, elapsed, ok in rows)
|
||||||
|
expected_runs = (1 if OPTIONS.lod0_only else 9) * len(selected_scenes)
|
||||||
|
expected_controls = 0 if OPTIONS.skip_controls else len(controls)
|
||||||
print(f"[L37RingSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
print(f"[L37RingSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
f"fail={sum(not row[-1] for row in rows)} controls=2 "
|
f"fail={sum(not row[-1] for row in rows)} controls={expected_controls} "
|
||||||
f"status={'FAIL' if failed else 'PASS'} summary={summary}", flush=True)
|
f"status={'FAIL' if failed else 'PASS'} summary={summary}", flush=True)
|
||||||
raise SystemExit(1 if failed or len(rows) != 18 else 0)
|
raise SystemExit(1 if failed or len(rows) != expected_runs else 0)
|
||||||
Loaded 100 of 476 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user