Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa67bd31f5 | ||
|
|
b21a19d561 |
No files matched your search
+5
-524
@@ -73,7 +73,7 @@ is the only density path; a degenerate (zero-height) strate has none and is open
|
||||
| Maze | `FMazeGenerationParams` | `BuildMazeStack` | origin-directed spanning-tree corridors on a 3D lattice, with capped loop edges and a thread-local per-cell capsule cache |
|
||||
| SurfaceWorld | `FSurfaceGenerationParams` | `BuildSurfaceStack` | heightfield terrain: domain-warped continents+ridged mtns+detail, a low-freq **relief map** (`M`) that scales mountains/elevation for plains↔highland variety, **F20 heightfield terrain ops** (`Surface|Ops` — all default off ⇒ byte-identical): **Cliff** (slope-gated steepening — where the analytic structural slope > `CliffSlopeThreshold`, push the height away from the local mean by `CliffSharpness` ⇒ gentle slopes become sheer walls/canyon faces that hug real steep ground, gentle areas untouched; 4 structural resamples only when enabled), **Terrace** (relief-gated plateau quantize + `TerraceHardness` soft-round↔crisp-mesa), **LayerLines** (sedimentary sine shelves, slope-expressed) — pure per-column height remaps in the single oracle `ComputeSurfaceTerrainZ` (`SampleSurfaceStructuralZ` = pre-op raw height, re-sampled at an XY offset for Cliff's slope), biome-selected + border-blended for free via each biome's `SurfaceParams` + the height output-lerp; plus **phase-2 Overhang** (the first VOLUMETRIC op — real jutting shelves like a cliff lip): in `FOverhangShelfMod`, for AIR voxels in the window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height and unioned in. Low in the window the shift is ~0 (borrows nearby low rock ⇒ stays air over the void); high up it reaches the far cliff (solid) ⇒ a shelf attached to the cliff, tapering out over the void with air underneath. Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill dir `(DirX,DirY)` are resolved once in `ComputeSurfaceColumn` — the gradient sampled at the REACH scale (`OverhangReach`) so a point out over the void can "see" the cliff to know which way is uphill — and cached on `FSurfaceColumn`. It's genuine 3D (per-voxel structural re-eval), so it's gated hard to steep overhang columns; the union only ADDS rock (never removes), capped at `TerrainZ+OverhangHeight`, so `ClassifyTile` forces Mixed only in `(TerrainZ, TerrainZ+OverhangMargin]` (upward-only; margin = max `OverhangHeight`) — a shelf never holes a trivially-skipped tile, and only the thin cliff-edge band of air tiles is woken (NOT far-field like phase-3 spikes/holes). Overhang undersides classify as ground rock by the F17 rule (down-facing but below `TerrainZ`). Known v1 limit: applies at all LODs (Step-agnostic) — may alias far; gate to fine tiles later. Beaches at water line, high sky-cap ceiling. The cap is shapeable terrain in its own right (`ComputeSurfaceCeiling`, `Surface|Sky` params: `CeilingUndulation` broad inverted hills/valleys, `CeilingRidgeStrength` hanging ridgelines, `CeilingRoughness`+freq fine bumps, `CeilingWarp*`) — defaults (strengths 0, freq 0.04) = old flat-ish cap. (`Surface|Macro` params = the cheap precursor to biomes; `ReliefStrength=0` ⇒ old uniform terrain.) **Sky-cap vs ground is a PER-TRIANGLE surface class (F17), not a per-tile verdict**: the mesher classifies each unique vertex on the **worker** — only down-facing verts (`N.Z<-0.1`) pay a memoized `GetSurfaceHeightAt` column query; nearer `CeilSurf` ⇒ sky-cap, nearer `TerrainZ` ⇒ terrain overhang stays ground (a future SurfaceWorld cave roof — down-facing but below `TerrainZ` — also lands ground by the same rule; non-surface strates always ground). Triangles take the majority class of their 3 verts and are packed as **two contiguous index runs** (ground then cap, `FVoxelMeshData::NumCeilingTriangles`; skirts inherit their source triangle's run) → RMC **polygroups 0/1** → one **section per non-empty group** (`ApplyMeshToTile`, material slot = group index): slot 0 = strate `OverrideMaterial`/default (min-corner chunk), slot 1 = `CeilingMaterial` resolved at the tile's **TOP chunk** (mid as gap fallback, then min; fallback: ground material) — a coarse tile is 2^level chunks tall, so its min corner can sit in a lower strate/gap while the cap belongs to the strate above (this was the "far cap = ground material" residue), so the shadowless overhead rock is tinted separately instead of reading flat/bright. Shadow is **per section** via `FRealtimeMeshSectionConfig::bCastsShadow` (NOT the component `SetCastShadow` — RMC's proxy ignores the component flag; this is also why level≥2 far tiles only stopped casting once the section flag was wired): ground casts at level≤1, the cap section never casts, so the rock ceiling never shadows the terrain below it. History: v1 was a game-thread centre height-oracle (misclassified coarse far tiles → terrain material on the cap underside); v2 a whole-tile worker normal VOTE — which painted **mixed coarse tiles** (one far tile spanning terrain AND cap) entirely with the winner's material, and put sky material under terrain overhangs. The per-triangle class fixes both and is the identity channel caves/F8 will reuse. |
|
||||
| VerticalShafts | `FVerticalShaftParams` | `BuildVerticalShaftStack` | full-height shafts + horizontal connectors + partial ledges |
|
||||
| FloatingIslands | `FFloatingIslandParams` | `BuildFloatingIslandStack` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, beneath the shared ceiling at its configured strate upper boundary |
|
||||
| FloatingIslands | `FFloatingIslandParams` | `BuildFloatingIslandStack` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, in an open void |
|
||||
| Underwater | `FStrateGenerationParams` + water | (reuses `BuildTunnelNetworkStack`) | tunnel rock + high water table |
|
||||
|
||||
All stacks share the convention: internal **positive=solid**, and `AppendStructuralPost` ends every
|
||||
@@ -732,15 +732,6 @@ driven by `EditorBrush*` props.
|
||||
its default member initialization was never observable; removing that generated TLS leaf avoids
|
||||
constructor work without changing a height or density. Integer queries still read the cached
|
||||
payload, and fractional queries still use the same direct locals.
|
||||
- **Core shared column cache (L7)**: `Heightfield2D` and FloatingIslands keep their `FLazy`
|
||||
worker-local caches as the first tier. A world-owned, fixed-size four-way table is the second
|
||||
tier, keyed by recipe and plan identity plus content hash, exact XY, and an optional exact
|
||||
sub-domain identity (FloatingIslands uses the island stable ID). Hits read versioned atomic
|
||||
entries without taking a lock; misses take only their set's spin lock, recheck, compute once, and
|
||||
publish. The tables are bounded and die with the world after its tile work has drained. This
|
||||
deliberately adds shared reads on column-cache hits while preserving the no-lock hot-path rule;
|
||||
S14's purely worker-local cache remains the first lookup. Keep large worker-local storage behind
|
||||
`FLazy`, and do not make cache values or keys depend on engine objects.
|
||||
- **Sampler unknown-source audit**: the earlier plugin-side `FSurfaceColumn` constructor PCs no longer
|
||||
occur after the trivial-payload change. The largest remaining LOD0 unknown cluster in the final
|
||||
sampler raw CSV is `UnrealEditor-Core.dll + RVA 0xC080` (runtime PCs around
|
||||
@@ -1431,9 +1422,8 @@ density roots, including a 100 cm datum-offset negative control. See
|
||||
evaluators keep their recorded cap behavior. The clip joins the exact near density root at the owning
|
||||
boundary without changing LOD0–3 density hashes. Future families with planar roofs must declare their
|
||||
cap height and bounds through the family plan/provider contract; the shared tile generator consumes
|
||||
that declaration. FloatingIslands uses the same resolved strate-boundary data flow: its upper seal
|
||||
follows its plan's chunk range and the shared boundary-seal primitive, while SurfaceWorld keeps its
|
||||
existing heightfield cap.
|
||||
that declaration. FloatingIslands declares no planar roof, so open sky above a surface island is the
|
||||
expected view.
|
||||
|
||||
For each requested far SurfaceWorld XY column at LOD4–8, desired-tile selection adds cap-height
|
||||
bands only when they do not overlap an existing or already accepted 3D clipmap tile. The original
|
||||
@@ -1472,9 +1462,8 @@ neighborhood have installed collision (or are explicitly collision-not-required)
|
||||
now** — the active view has ground, horizon and the family-appropriate ceiling, with no exposed
|
||||
coverage hole or unexpected sky; (3) **refined soon** — each target LOD resolves within its stated
|
||||
time or travel distance after entering range. While moving, report the largest frame/time gap in
|
||||
enclosure and maximum distance that unresolved work trails the pawn. FloatingIslands is enclosed by a
|
||||
planar roof at the upper boundary of its configured strate range; a taller strate places that roof
|
||||
farther above its islands.
|
||||
enclosure and maximum distance that unresolved work trails the pawn. FloatingIslands has open sky by
|
||||
design, so its enclosure check excludes a planar roof.
|
||||
|
||||
The shared `AVoxelWorld::UpdateChunksAroundPosition` scheduler serves both the Core snapshot path and
|
||||
Core-off legacy path. It admits the collision-critical 3×3 ring first. The measured S5 order remains
|
||||
@@ -1808,514 +1797,6 @@ page was added: cross-worker repeats alone do not establish a net page-cache win
|
||||
immutable-page lifetime rules would add ownership and memory cost. Round 18 changed no output or
|
||||
recipe version. See [`Standalone/Research/WorldAssemblerM3/Round18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round18/REPORT.md).
|
||||
|
||||
### 8.35 Shared Core column cache — World Assembler L7
|
||||
|
||||
L7 measured 1.34 million repeated formula evaluations on the W12 L1 view before sharing: 83.6%
|
||||
were another worker's XY, 4.0% were same-worker repeats after eviction, and 12.0% were halo/normal
|
||||
repeats. The world-owned shared second tier cut measured formula executions by 60.9% and improved
|
||||
median whole-view-ready time by 19.0%; moving 8 m and generating 144 entering tiles improved median
|
||||
wall time by 36.1%. Peak RSS rose by about 66 MiB. The cache is a bounded exact-value table rather
|
||||
than published pages; no scheduling affinity or page prefetch was needed to clear the measured
|
||||
whole-view threshold.
|
||||
|
||||
The thread-local first tier remains because it avoids shared atomics for local hits. The shared
|
||||
table has no lock on hits; only a miss takes a set-local spin lock. FloatingIslands' cache identity
|
||||
includes its island stable ID because distinct anchors can request the same XY with different
|
||||
column metrics. All W1/W12 L1 hashes, exact queries, and the seed-0 FloatingIslands fixture match
|
||||
the pre-L7 binary with Clang FP contraction off and GCC defaults. See
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux7/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux7/REPORT.md).
|
||||
|
||||
### 8.36 Route-invariant Core density — World Assembler M3 L13
|
||||
|
||||
For an unedited Core world, tile density, mesh, payload, and topology are a function of the world
|
||||
manifest/recipe, seed, and tile key. The reviewed manifest field is authoritative: SurfaceWorld uses
|
||||
its `Heightfield2D`, while FloatingIslands uses its `FloatingIslandFieldV1`. `World::densityAt` and
|
||||
tile generation read the same `FStandaloneShapeWorld` instance. A compiled analytic region page
|
||||
retains bounded spatial bins for page ownership and routing, but `SetSharedDensitySource` binds it to
|
||||
that base shape before publication; density, lattice, detail, classification, and solid queries
|
||||
delegate to the shared evaluator. This keeps exact queries, collision occupancy signs, page tiles,
|
||||
and base tiles on one field. Player edits remain explicit versioned overlays on that world state.
|
||||
|
||||
The route-binding change increments `WorldAssemblyEvaluatorVersion` from 5 to 6. Do not restore a
|
||||
page-local legacy evaluator: it disagrees with the reviewed field. Uniform-block proof audits must
|
||||
fill and check page tiles against the evaluator they actually sample. This is the shared Core path;
|
||||
the Unreal adapter and its asynchronous MPSC/Epoch flow are unchanged. See
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
||||
|
||||
### 8.37 Shared strate boundaries and FloatingIslands lower seal — World Assembler L18
|
||||
|
||||
`FStandaloneShapeWorld::GetResolvedStratumBounds` supplies one physical lower/upper-bound descriptor
|
||||
to layer ranges, far-field sample protection, and far-shell continuity for every strate. SurfaceWorld
|
||||
retains its exact `topChunkZ * 32` Core-voxel mapping and native heightfield cap. Native FloatingIslands
|
||||
takes its resolved bounds from its plan, whose chunk range is converted through the serialized
|
||||
owner/Core voxel ratio. Its version-3 field removes only the lower all-XY seal and calls the same
|
||||
`VF_ApplyUpperBoundarySeal` primitive used by the shared two-sided `VF_ApplyBoundarySeal` for its upper
|
||||
boundary. The ceiling therefore sits at that strate's own upper boundary, with the common
|
||||
`boundarySealSubvoxels` thickness; there is no FloatingIslands-specific height. The recipe range is
|
||||
un-inset to match the Unreal adapter. Versions 1 and 2 retain their serialized two-sided seals.
|
||||
SurfaceWorld's field and output hashes remain unchanged. See
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md).
|
||||
|
||||
### 8.38 Closed shared boundary ceilings and LOD sampling audit — World Assembler L19
|
||||
|
||||
`AppendStratumBoundaryCeilings` fills a candidate cell when the authoritative field is solid just
|
||||
inside that strate's configured upper boundary. It does not require the raw field to cross back to
|
||||
air above the boundary: the ceiling itself closes the strate even when the field continues solid
|
||||
through that plane. This avoids isolated holes where a coarse lattice node has no zero crossing.
|
||||
`FFarMeshColumnIndex::HasDownwardCellCoverage` still suppresses a fallback quad only after its
|
||||
interior and perimeter probes all hit the existing downward face. The L19 `farHorizonVersion` is 4
|
||||
for this shared boundary-cap change; hashes remain direct at LOD0–3 and salted at LOD4–8. No density
|
||||
query, stratum position, scale, or asynchronous route changes. The strict LOD0–8 roof and paired-edge
|
||||
audit and the matched before/after captures are in
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md).
|
||||
|
||||
### 8.39 Resolved layout snapshots and nominal strate bands — World Assembler L21 (historical)
|
||||
|
||||
`ResolvedLayoutSnapshotV1` is a separate Core-owned record built from the resolved manager slots and
|
||||
the native SurfaceWorld/FloatingIslands plans. It has a canonical JSON representation, schema version,
|
||||
and stable FNV-1a identity. Its physical intervals use half-open micrometre coordinates; recipe anchors
|
||||
retain their serialized fixed-point units. The Unreal adapter exports it beside `world_assembly`, so the
|
||||
layout digest never salts a recipe, manifest, density, or mesh hash. The standalone exporter follows
|
||||
the same builder after plan resolution. It runs once per plan request and is not read from density or
|
||||
tile-worker loops.
|
||||
|
||||
Each record separates the nominal ceiling, content, and bedrock bands. A strict layout requires that
|
||||
sequence for every strate and requires bedrock(N) to touch the next strate's ceiling(N+1). The two
|
||||
bands remain separately owned. Gradient and Interleaved interfaces may declare interpenetration only
|
||||
inside their explicit transition zone; Hard has zero effective width. The manager's `TransitionBlendChunks`
|
||||
is expressed in owner-chunk metres. L21 recorded the older Interleaved allowance as blend width plus
|
||||
warp amplitude; L29 now treats the declared blend width as the complete physical crossing allowance.
|
||||
Domain warp changes finger placement, but cannot extend overlap beyond that width. Compatibility validation reports current
|
||||
gaps, overlaps, ambiguities, inactive/missing bands, and unhonoured transitions while accepting them;
|
||||
strict validation rejects them. Core dispatches per-strate recipe fields without applying that
|
||||
parameter blend, even though the legacy manager helper implements it. Both band types default diggable,
|
||||
with protection represented explicitly.
|
||||
|
||||
Compatibility facts from L21: native SurfaceWorld plan/query bounds use 32 Core voxels (8 m) per
|
||||
dispatch chunk, while FloatingIslands converts the 32-voxel owner chunk through its 12.5/25 cm scale
|
||||
(16 Core voxels, 4 m). In the prototype where the top slot is FloatingIslands, this leaves a nominal
|
||||
60 m interval between the FI bottom (−60 m) and SurfaceWorld top (−120 m); the Core selectors fall
|
||||
through to the compiled analytic FloatingIslands fallback there. Core does not apply the manager's
|
||||
parameter blend at either recipe boundary. The FI v3 upper seal remains a ceiling inside its current
|
||||
content range, and its missing lower bedrock is recorded without changing evaluation. See
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux21/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux21/REPORT.md).
|
||||
|
||||
### 8.40 Fixed-metre strate layout units and shared boundary volumes — World Assembler L22
|
||||
|
||||
`StrateLayoutChunkSizeMeters` is one world-level setting, defaulting to **8 m**, independent of the
|
||||
legacy 32-voxel chunk and Core's 32³ storage tile. The director resolves the setting once to physical
|
||||
micrometres and Core subvoxels; every generator plan and dispatch reads that snapshot. At a 25 cm Core
|
||||
voxel, an 8 m layout chunk is 32 Core voxels (8192 1/256-voxel units); at 20 cm it is 40 Core voxels
|
||||
(10240 units). The Core tile remains 32³ voxels and is not a layout chunk.
|
||||
|
||||
The owner's FI/SW intervals are contiguous: FloatingIslands occupies [−120,+8) m and SurfaceWorld
|
||||
occupies [−632,−120) m. Each strate is ceiling → content → bedrock. Both solid, diggable boundary
|
||||
bands use its generator's existing `BoundarySealThickness` and `BaseDensity`; `InterStrateGapChunks`
|
||||
controls only spacing, and `EdgeSealThickness` remains horizontal. No new thickness parameter or hidden
|
||||
band height was added. The shared composition layer emits both volumes for every generator and the
|
||||
shared mesh path closes their visible ceiling and floor faces.
|
||||
|
||||
The snapshot is schema V2 and is consumed by bounds planning, native and legacy dispatch, solid-band
|
||||
composition, and validation. Strict validation accepts the real owner layouts, and the boundary probe
|
||||
audit agrees at all 27 sampled endpoints in both recipe modes. The owner authored Gradient transition
|
||||
is recorded as an explicit Hard fallback with zero interpenetration; Gradient and Interleaved mixing
|
||||
remain unimplemented and must not be inferred from the declared blend width. Seven validator negative
|
||||
controls continue to reject malformed bands.
|
||||
|
||||
Evidence and owner caveats are in
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux22/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux22/REPORT.md).
|
||||
|
||||
### 8.41 Director-resolved inter-strate passages and walkable landings — World Assembler L23
|
||||
|
||||
Core passage geometry is derived after the director has resolved physical strate bands and native
|
||||
route anchors. `PlanResolvedLayoutPassagesV1` joins every adjacent resolved pair. Its placement hash
|
||||
uses `(seed, upper strate, lower strate, connection, salt)`; `PassageConfig.DistanceMax` is the
|
||||
outer radius around world `(0,0)` (24 m for the owner's first interface), while `DistanceMin` is
|
||||
the inner radius used when a mouth has no route anchor. A route-bound mouth reuses the upper
|
||||
strate's departure or lower strate's arrival anchor, after checking it lies within `DistanceMax`.
|
||||
The legacy `UVoxelStrateManager::GeneratePassages` and its owner-voxel control points remain the
|
||||
legacy generator path; Core does not import the exported legacy `passages` array. The season
|
||||
manifest carries recipes and plans, and the adapter rebuilds the resolved layout and its passages
|
||||
from the current manager slots and route anchors each time it attaches the Core world.
|
||||
|
||||
`ApplyResolvedStrateComposition` composes the current generator field, applies the resolved ceiling
|
||||
and bedrock bands, then unions the passage carve and floor support. Only the hashed capsule path
|
||||
opens a band; the support strip stays below the player capsule. The lower mouth index splits the
|
||||
monotone descent from the short route-bound landing tail, whose height may vary with the route.
|
||||
Both route and passage audits sample the 0.34 m by 1.76 m capsule, support probes, and grade; the
|
||||
negative control compiles the same world without passage carving and must detect a blocked capsule.
|
||||
|
||||
The L23 shared-field change is `WorldAssemblyEvaluatorVersion=9`. It does not change the L22 bands
|
||||
or any generator's field away from a passage. The L23 report includes exact-query/hash comparisons,
|
||||
the blocked control, captures, and the Windows adapter checklist. §8.10's cache, proof, streaming,
|
||||
and async rules remain unchanged.
|
||||
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md)
|
||||
records the audit and captures.
|
||||
|
||||
### 8.42 Per-strate natural passages and the FloatingIslands anchor helix — World Assembler L24
|
||||
|
||||
`FStratePassageConfig.Style` now drives the shared Core planner for every generator. The default is
|
||||
`Worm`; Straight, Spiral and Cascading remain selectable per upper strate. Passage radius, reach,
|
||||
distance and style stay in the resolved-layout input, so equal seeds and route anchors produce equal
|
||||
paths on every peer. The passage carver adds bounded, deterministic wall relief (`0.24 m` before the
|
||||
gravity-facing attenuation) while its support tread follows the same route profile below the capsule.
|
||||
|
||||
FloatingIslands' owner config selects Spiral. A seed-derived anchor island near `(0,0)` is joined to
|
||||
bedrock by a 6 m radius rock root. The 3 m helix descends from the island mouth to the `-119.5 m`
|
||||
bedrock top inside that root, leaving at least the authored clearance around its 1.5 m bore, then
|
||||
widens into the Worm route toward the SurfaceWorld arrival. The selected anchor is ordinal 2; the
|
||||
other island ordinals and their placement hashes are unchanged. FloatingIslands recipe geometry is
|
||||
version 5 and the shared passage evaluator is version 10; the resolved-layout snapshot stays V3.
|
||||
|
||||
The passage naturalness audit rejects a horizontal run above 8 m (segments flatter than 0.5 degrees)
|
||||
or wall relief below 0.20 m peak-to-peak across at least 8 valid samples. The smooth-wall control
|
||||
has zero relief and fails; the anchored Spiral has 0 m flat run and 0.793 m measured relief. Strict
|
||||
layout controls, route binding, capsule clearance, support, grade, and the no-carve negative control
|
||||
remain required. Passage-centered interval proofs remain unknown/fallback rather than falsely
|
||||
certified. At the lower mouth, the shared layer now relies on native terrain plus the passage tread;
|
||||
it does not stamp additional flat route-anchor shelves over the cave opening. The L24 report records
|
||||
compiler/worker parity, unchanged SurfaceWorld hashes away from the route, captures and the Windows
|
||||
adapter checklist.
|
||||
|
||||
The Linux offscreen capture tool accepts `--headlamp=on` to aim brighter readable shading from the
|
||||
camera for interior views and `--clip-plane=NX,NY,NZ,D` to discard the plane's negative half-space for
|
||||
cutaway views. Both affect captures only, not generated geometry.
|
||||
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md)
|
||||
records the evidence and owner-facing caveats.
|
||||
|
||||
### 8.43 Enclosed FloatingIslands helix and native-solid passage support — World Assembler L25
|
||||
|
||||
L24's support tread was composited into the passage field without requiring native solid terrain at
|
||||
the support sample. A 6 m root could not contain the resolved helix either: the centreline reaches
|
||||
5.562 m from the anchor axis and the mouth bore is 4.5 m. With the old root radius, the tread could
|
||||
therefore read as a free-floating spiral outside the rock. This was not a unit conversion error.
|
||||
|
||||
The shared passage composer now emits route-mouth support, tread, and fallback support only where
|
||||
the pre-passage native density is solid. The open-air audit samples each support footprint at 0.25 m
|
||||
spacing and two depths, including route-mouth shelves; its L24 ungated-support control must report a
|
||||
violation. The FI anchor root is now a relief-bearing 13.25 m nominal rock column. Across five
|
||||
heights its measured radius is 12.0–13.375 m. Taking the maximum 5.562 m centreline radius and the
|
||||
full 4.5 m mouth bore at every height gives a conservative minimum wall of 1.938 m, exceeding the
|
||||
previous 1.5 m reserve. The authored mid-bore remains 3 m. The helix is selected from its route-bound
|
||||
root point and joins the lower route before the tunnel hands off through the shared SW ceiling.
|
||||
|
||||
The configured FI Spiral passes the capsule/support/grade audit with zero blocked, unsupported, or
|
||||
grade-failure samples; the L23 smooth-wall control fails naturalness and the L24 ungated-support
|
||||
control fails the open-air audit. The other style overrides also pass the open-air audit, but their
|
||||
capsule results are recorded individually in the L25 report because some overrides are not
|
||||
walkable after floating support is suppressed. Evaluator version is 12, FI geometry version is 7,
|
||||
and resolved-layout schema V3 remains unchanged. The L25 report records measurements, hashes,
|
||||
captures, style results, and the single Unreal adapter version edit.
|
||||
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md)
|
||||
records the evidence and caveats.
|
||||
|
||||
### 8.44 Walkable passages as tunnels in rock — World Assembler L26
|
||||
|
||||
L25 correctly stopped support from appearing in native air, but its SurfaceWorld-recipe passages
|
||||
still crossed a long open volume in the SurfaceWorld arrival strata. The segment map places that gap
|
||||
inside SurfaceWorld (about `-146` to `-497 m`), after the TunnelNetwork transition; it is not TunnelNetwork
|
||||
cave air. Both ends meet native rock. The shared Core planner now finds contiguous unsupported ranges
|
||||
from the native field sign and caches them by resolved-layout identity. It adds a deterministic,
|
||||
relief-bearing rock span only across those ranges. The span is at least 5 m wide, several metres thick,
|
||||
and joins native rock at both ends. The passage carve then opens the bore through the added density,
|
||||
leaving a grounded floor and rock walls/ceiling. SurfaceWorld away from the passage remains unchanged.
|
||||
|
||||
Native route support remains gated on pre-passage solid density. Before adding any support density, the
|
||||
composer also checks the resolved player capsule, so neighboring floor segments cannot restore rock
|
||||
inside the avatar path. This keeps all four style overrides walkable in both recipes. The bridge audit
|
||||
measures its authored density field at 256 route sections, checks the 5 m width and connected ends, and
|
||||
records a relief-bearing thickness range; the open-air audit still runs the L24 negative control.
|
||||
|
||||
The shared evaluator is version 13; FloatingIslands geometry remains version 7 and resolved-layout
|
||||
schema V3 is unchanged. These changes are in Core and the standalone audit/capture path; no Unreal
|
||||
adapter edit is required. The L26 report contains the eight-style table, hashes, captures and run history.
|
||||
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md)
|
||||
records the final evidence.
|
||||
|
||||
### 8.45 Tile-bounded passage evaluation — World Assembler M3 Linux L28
|
||||
|
||||
Each mesh tile scopes passage composition to the inclusive density-sample AABB, including the
|
||||
mesher's normal halo. A conservative feature AABB BVH selects route segments, columns and mouth
|
||||
support for that tile. A per-passage nearest-segment BVH bounds the long helix search; a separate
|
||||
passage-bound BVH rejects far L9 proof boxes. An unindexed native-only proof object uses a conservative
|
||||
linear fallback. No field, union order, serialized layout or recipe version changes.
|
||||
|
||||
The column's exact complex SDF is evaluated only in XY/Z feature candidates. A cheap density-aware
|
||||
radial guard remains for samples sharing the column's Z slab: a positive column distance can still
|
||||
lower positive base density, so skipping that guard needs an exact bound on the base field. This is
|
||||
the remaining SurfaceWorld cost noted in the Linux L28 report. The passage candidate lists and
|
||||
proof bounds preserve the L27 output.
|
||||
|
||||
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md)
|
||||
records counts, parity, costs and audit caveats.
|
||||
|
||||
### 8.46 Honored bedrock/ceiling transitions — World Assembler L29
|
||||
|
||||
The upper strate's `EVoxelStrateTransition` controls the shared interface between its bedrock band
|
||||
and the next strate's ceiling band in the Core composition path. `TransitionBlendChunks` is converted
|
||||
using the resolved layout chunk width (8 m by default). Hard preserves the flat interface. Gradient
|
||||
moves the shared surface with a smooth 2D hash field whose peak-to-peak range is at most one quarter
|
||||
of the declared width. Interleaved uses a seed/interface/coordinate-keyed, domain-warped 3D hash field
|
||||
to set actual overlap between the facing bands from zero to the declared width. The two faces move
|
||||
inward by half that overlap; the warp changes finger placement without increasing the crossing
|
||||
allowance. Both bands keep authored thickness, and their solid intervals touch or overlap in every
|
||||
XY column. Core does not blend unrelated generator fields.
|
||||
|
||||
Each boundary band keeps at least **two Core voxels** of solid thickness (0.5 m at the default 25 cm
|
||||
voxel size). Strict validation rejects thinner bands and declared interpenetration beyond the
|
||||
transition width. The world audit samples a 512 m square XY area, measures actual crossing depth and
|
||||
minimum band thickness, and sweeps every non-passage column through both bands at half-voxel spacing.
|
||||
Passage columns are excluded from the hole sweep. Near an interface mouth, the shared face returns to
|
||||
its nominal height inside the mouth radius plus a 1 m support margin, then fades back to the selected
|
||||
transition over 2 m so the authored landing floor remains supported. Negative controls reject a
|
||||
transition one micrometre wider than declared and a band one micrometre below the minimum. See
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md)
|
||||
for audit measurements, hashes, timings, captures and run history.
|
||||
|
||||
### 8.47 Ground-bound SurfaceWorld departure routes — World Assembler M3 Linux L33
|
||||
|
||||
An intra-strate SurfaceWorld route follows the resolved terrain ground profile. Its route-point Z
|
||||
is the ground height plus the player's half-height; the route does not descend to the strate's
|
||||
bedrock boundary. Bedrock belongs to the separate inter-strate passage, which begins at the
|
||||
SurfaceWorld departure mouth on the ground and tunnels outward and down through native rock and
|
||||
the boundary into the next strate.
|
||||
|
||||
The old fixed candidate envelope used a 64 m chord and 16 m centre spacing. On the L33 upper-SW
|
||||
fixture, replaying that envelope sampled ground from −507.767 to −464.857 m; 366/648 candidate paths
|
||||
exceeded the 15° grade limit. The worst failures clustered near the outer west/northwest terrain
|
||||
shoulder (up to 79.971°) and the steep approach slopes around the origin. Candidate diagnostics
|
||||
separate grade failures from endpoints outside the bounded arrival/departure placement disks.
|
||||
The bounded search now limits its longest chord to the two mouth radii minus a 3 m clearance
|
||||
margin, checks centres every 8 m, and ranks endpoints near the route anchors. It finds a 21.5 m
|
||||
route from (−2.75, 0) to (18.75, 0), sampled on ground and accepted by the full capsule/support/
|
||||
grade audit.
|
||||
|
||||
The SurfaceWorld departure passage starts at the ground mouth with a 1.5 m portal, then smoothsteps
|
||||
to a 3.5 m rock tunnel over 9 m. Its ring is sized and oriented outward from the mouth, so the
|
||||
approach does not cut back under the walk route. The `FloatingIslands → SurfaceWorld → TunnelNetwork`
|
||||
fixture passes all four passage styles, including both interfaces, capsule clearance, support,
|
||||
grade, walkability, naturalness, open-air, column ends, long-bridge and route-binding audits.
|
||||
|
||||
`FloorProfileBlend` previously compared each curved floor profile with its straight chord and
|
||||
applied a 2-voxel limit. That is a curvature measurement, not a seam measurement. It now checks
|
||||
endpoint height and tangent continuity at 0.01 voxel and 0.001 voxel/m; the 3-voxel step and
|
||||
0.25 voxel/m tangent-step controls are both detected. SurfaceWorld owner hashes remain identical
|
||||
to L32 because the route-mouth, expanded column sizing and short-span support changes apply only
|
||||
when SurfaceWorld is the upper strate. No evaluator or recipe version changed.
|
||||
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md)
|
||||
records the four-style audits, captures, determinism, owner hashes, costs and retries.
|
||||
|
||||
### 8.48 Irregular passage-wall relief and grounded tread shoulders — World Assembler M3 L35
|
||||
|
||||
L34's owner-SW profile had a 7.438 m radius repeat with 0.0361 m (3.61 cm) amplitude; an independent
|
||||
mesh-normal scan also found a 4.750 m period at 0.02682 (1.537°). The geometry came from piecewise
|
||||
capsule joins, while broad value-noise bands added a second regular pattern. The shared carve now
|
||||
uses Catmull–Rom centerline interpolation on eligible interior segments, excluding short spans and
|
||||
terminal controls. Passage walls use deterministic, rotated 43/29/19 m relief with reduced terrain
|
||||
band contribution; other `VF_FieldSurfaceRelief` users, including grounded bridge columns, are
|
||||
unchanged. The shared evaluator is version 21.
|
||||
|
||||
The residual radius scan on owner-SW style 0 measures a 7.062 m period at 0.0109 m (1.09 cm),
|
||||
with 0.0044 m (0.44 cm) coherent amplitude. The independent L35 mesh-normal scan passes at 8 m
|
||||
and 0.02496 axial amplitude, below its 0.0255 threshold. The ripple audit samples both side-wall
|
||||
radii every 0.125 m in 32 m windows, scans 0.5–8 m periods, and rejects amplitudes of 0.035 m or
|
||||
more. Its synthetic control uses 2 m radius spheres spaced every 1.5 m; it measures 0.0599 m and
|
||||
must fail.
|
||||
|
||||
At the FI mouth, the flat apron is the shared passage support tread, not `LandingExtensionQ`, a tile
|
||||
edge or a separate support layer. The tread is required by capsule and support probes. L35 gives its
|
||||
outer entry shoulder a shallow taper and seeded grain while preserving the supported walk core and
|
||||
lower-mouth overlap width. The shallow quarter-weight support-height/radius spline keeps the tread
|
||||
continuous without moving its XY footprint.
|
||||
|
||||
Cascade routes also replace identical full-depth cosine lobes with a seeded shallow profile on each
|
||||
turn. Passage, capsule, support, open-air, bridge, binding, strict layout, capture, determinism, ring,
|
||||
and cost evidence are recorded in
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
||||
|
||||
### 8.49 Walk-floor washboard and FI mouth naturalness — World Assembler M3 L36
|
||||
|
||||
L35's exported owner-SW mid-tunnel floor measured a 7.250 m repeat at 0.02252 m amplitude; its
|
||||
upper-SW fixture arrival measured a 1.438 m repeat at 0.02482 m. Both came from segment-built,
|
||||
one-sided support treads meeting the native terrain zero crossing at per-segment joins. The floor
|
||||
probe samples the exported walk mesh along the resolved route, separately from the wall-radius scan.
|
||||
Eligible tread sections overlap by up to 2 m and all sparse route segments now use a bounded
|
||||
signed-distance slab, including SurfaceWorld-to-TunnelNetwork routes. This keeps the tread top stable
|
||||
across segment joins while capsule-clearance and native-solid gates retain walkability and open air.
|
||||
Regular tread tops remain seated 5 cm lower and ease back to 0.20 m within 20 m of a lower landing;
|
||||
seeded shoulder grain is removed, and gravity-facing relief is reduced while side-wall and ceiling
|
||||
relief remain irregular.
|
||||
|
||||
On v23, the same mesh probes measure 7.188 m / 0.00067 m (0.067 cm) on owner-SW and 6.500 m /
|
||||
0.00057 m (0.057 cm) on the upper fixture. The density audit checks five 32 m walk-floor windows at
|
||||
0.125 m spacing, rejects amplitudes of 0.015 m or more, and detects its beaded control. The FI mouth
|
||||
uses a local 2 m symmetric signed-field filter with the route protected and joins route-mouth support
|
||||
to native terrain with a 1.5 m smooth signed-distance union. Its mesh audit reduces the L35
|
||||
42.27° fade-ring jump to 27.13° and finds no detached component above the 0.0001 m² area floor; one
|
||||
8-triangle subthreshold component at 0.0000266 m² is recorded. The evaluator is version 23. Full
|
||||
recipe/style audits, route support, open-air, strict layout, legacy validators, determinism, capture and
|
||||
paired-cost results are in
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
||||
|
||||
### 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)
|
||||
|
||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||
|
||||
@@ -26,12 +26,9 @@ try to run anything.
|
||||
- **Density sign: negative = solid, positive = air** (MC convention at the mesher).
|
||||
`FVoxelModification::Strength` negative = carve. *The #1 source of confusion.*
|
||||
- **Density functions take VOXEL coords, not cm.** The legacy plugin field uses 12.5 cm voxels;
|
||||
the opt-in Core field uses 25 cm voxels and its own 32³-voxel storage tile. The legacy 32³ chunk
|
||||
spans 4 m at 12.5 cm. Strate placement uses the separate world-level `StrateLayoutChunkSizeMeters`
|
||||
setting (8 m by default), resolved once and shared by every generator. At 25 cm that layout unit is
|
||||
32 Core voxels; at 20 cm it is 40. Never infer the layout unit from `CHUNK_SIZE` or the Core tile.
|
||||
Conversions live in `VoxelTypes.h`, the director, and the Core bridge; do not mix scales in queries,
|
||||
anchors, edits, or surface lookups.
|
||||
the opt-in Core field uses 25 cm voxels and its own tile keys. One legacy 32³ chunk is 4 m.
|
||||
Conversions live in `VoxelTypes.h` and the Core bridge; do not mix scales in queries, anchors, edits,
|
||||
or surface lookups.
|
||||
- **Determinism:** all randomness = hash of (coord, seed, strateIndex). No RNG state — same seed ⇒
|
||||
same world. Player edits (diff layer) are the only non-deterministic overlay.
|
||||
- **Core world invariants:** Core density keeps the same sign (negative solid, positive air), but Core
|
||||
|
||||
+32
-267
@@ -29,9 +29,7 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
||||
### Core terminology
|
||||
| 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. |
|
||||
| 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. |
|
||||
| Chunk | 32×32×32 voxels (`CHUNK_SIZE`). Voxel = 12.5 cm (`VOXEL_SIZE`), so a chunk is 4 m. |
|
||||
| Density | Scalar field. `< 0` solid, `> 0` air, `0` = surface (`IsoLevel`). |
|
||||
| Strate | Vertical layer of the world, stacked downward. Has its own generator + params. |
|
||||
| Generator type | Archetype per strate: `TunnelNetwork`, `FlatPlain`, `CrystalChamber`, `Maze`, `SurfaceWorld`, `VerticalShafts`, `FloatingIslands`, `Underwater`. See §8. |
|
||||
@@ -70,10 +68,8 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
||||
(RealtimeMeshComponent)
|
||||
```
|
||||
|
||||
`UVoxelStrateManager` owns resolved half-open metre intervals (`[bottom, top)`) and is the side
|
||||
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.
|
||||
`UVoxelStrateManager` is the side oracle: "what strate is at this Z, what params,
|
||||
what generator type, and is there a passage/elevator SDF near here?"
|
||||
|
||||
---
|
||||
|
||||
@@ -82,28 +78,23 @@ dispatch APIs.
|
||||
Paths relative to `Source/VoxelForge/`. `Public/` = headers, `Private/` = impl.
|
||||
|
||||
### 3.0 Human-scale generation contract (2026-10-01)
|
||||
`VOXEL_SIZE` is **12.5 cm** for the legacy generator. Its 32³-voxel chunks span **4 m**. The
|
||||
world-level strate-layout unit is a separate setting with an **8 m default**; a 16-chunk strate is
|
||||
therefore **128 m**, and a 64-chunk strate is **512 m**. Core still stores work in 32³-voxel tiles;
|
||||
at 25 cm those tiles happen to span 8 m, but they are not the layout unit. The gameplay capsule remains
|
||||
**0.34 m radius / 1.76 m tall** (**2.72 / 14.08 legacy voxels**). Research-field dimensions are authored
|
||||
in metres and converted at runtime. The live
|
||||
strate generator continues to read voxel-valued shape parameters from its assets, so those authored
|
||||
dimensions become half as large in metres unless their asset values are doubled. U1 adds a
|
||||
versioned in-memory conversion for strate height, transition width, and inter-strate gap only; the
|
||||
broader Research8 voxel/chunk/centimetre inventory is not yet migrated.
|
||||
`VOXEL_SIZE` is **12.5 cm**; 32³ voxels make each chunk **4 m**. The gameplay capsule remains
|
||||
**0.34 m radius / 1.76 m tall** (**2.72 / 14.08 voxels**). The default strate envelope is
|
||||
**16 chunks / 512 voxels / 64 m**; serialized owner strate assets that still say 8 chunks must be
|
||||
changed to 16. Research-field dimensions are authored in metres and converted at runtime. The live
|
||||
strate generator continues to read voxel-valued parameters from its assets, so those authored
|
||||
dimensions become half as large in metres unless their asset values are doubled.
|
||||
|
||||
The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells
|
||||
**32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m**
|
||||
radius; **3.0625 m** worst-case usable floor after roughness), slab open span **22.4 m**, shaft
|
||||
diameters **4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. Source
|
||||
new fixed physical dimensions are authored in metres and converted to voxel counts at the boundary.
|
||||
The owner's voxel-valued live strate shape settings are not migrated by U1: unchanged saved values
|
||||
therefore produce half-size rooms, tunnels, features and noise periods in metres. See the U1 report
|
||||
for the conversion coverage and remaining Research8 fields.
|
||||
The owner's voxel-valued live strate assets are not migrated by C++: unchanged saved values therefore
|
||||
produce half-size rooms, tunnels, features and noise periods in metres. See the step-3 migration list
|
||||
in the handoff report for the exact `DA_Settings` fields and values.
|
||||
|
||||
**Prior reauthoring recommendation (not applied in U1; no packages were edited):** preserve
|
||||
`DA_Settings` distances by changing
|
||||
**Owner asset migration (no packages were edited):** preserve `DA_Settings` distances by changing
|
||||
`ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`,
|
||||
`StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`,
|
||||
`EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region
|
||||
@@ -113,9 +104,8 @@ size `8→16 cells`, spacing/far spacing `8→16 voxels`, march step `4→8 voxe
|
||||
`12→24 cells`; it is currently disabled. Keep `MaxClipLevel=6` and zero-valued disabled/unbounded
|
||||
limits unchanged; double finite world radii and gaps, and multiply finite voxel-volume caps by 8.
|
||||
The loaded `Crystal Cave` slot in `DA_Strate3` is 64 chunks tall; use 128 chunks to preserve its
|
||||
former 512 m span. U1 automatically converts only strate height and transition widths and the
|
||||
settings gap at load; the remaining recommendations above are pending. Its voxel-authored
|
||||
live-generator lengths remain half-size until their asset fields are migrated.
|
||||
former 512 m span. Its voxel-authored live-generator lengths remain half-size until that asset is
|
||||
re-authored; the editor-only research field is the step-3 target.
|
||||
|
||||
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³
|
||||
@@ -168,22 +158,17 @@ remains `0`, and lateral regions remain gated off.
|
||||
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**, `VoxelForgeCore`. `AVoxelWorld::bUseVoxelForgeCore` opts into the Core runtime route; it defaults off. |
|
||||
| `../../VoxelForgeCore/VoxelForgeCore.Build.cs` | C++20 runtime module. Reads `Standalone/Core/core_sources.txt`, validates one include shim per canonical source, and fails UBT on missing, extra, or mismatched shims. No unity/PCH; private copied implementation stays under `vfcore::detail`. |
|
||||
| `../../VoxelForgeCore/Private/Manifest/VFCore_*.cpp` | Eighteen one-source UBT shims that include canonical files under `Standalone/`; they do not copy Core implementation. The API-boundary row includes `Core/src/VoxelForgeCore.cpp` directly. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/ThreadLocalStorage.h` | `VoxelForgeCoreThreadLocal::FLazy<T>`: thread-local handle that heap-allocates a worker-private cache on first use. Large worker-local caches use it (HeightfieldColumnCache, IslandColumnCache, GFloorReliefCache, GRoomGraphCache, GSurfColCache, tunnel/diff caches). See ARCHITECTURE §8.10. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h` | Bounded four-way second-tier cache for exact per-column values. Readers use versioned atomic slots without a lock; misses use a set-local spin lock. World-owned lifetime, recipe/plan identity and content hashes, XY and optional sub-domain identity. SurfaceWorld and FloatingIslands share this mechanism; see ARCHITECTURE §8.35. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/ThreadLocalStorage.h` | `VoxelForgeCoreThreadLocal::FLazy<T>`: thread-local handle that heap-allocates a worker-private cache on first use. All large Core and legacy caches use it (HeightfieldColumnCache, IslandColumnCache, GFloorReliefCache, GRoomGraphCache, GSurfColCache, tunnel/diff caches). See ARCHITECTURE §8.10. |
|
||||
| `../../Standalone/Core/src/CoreNamespacePreamble.h` | First include of every Core TU. Forces FP contraction off per compiler (Clang/MSVC/GCC pragmas; unknown compiler = `#error`) and defines `vfcore::fp_contract::selfTest`, which `World::compile` runs before building: a fused multiply-add (e.g. Clang `-ffp-contract=fast`) refuses world generation. The world depends on contraction (Linux2/Linux3 reports), so never remove either. |
|
||||
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. |
|
||||
| `ResolvedLayoutStratePassageCapabilitiesV1::{hasNativeCaveField,hasNativeLatticeField}`, `IsNativeLatticeFieldStratum`, `HasNativeGraphField` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit per-strate native graph capability; `Core.Lattice.PrimitiveField` is restricted to Maze/VerticalShafts recipe version 1. Lattice seeds include world seed, strate, recipe version and stable feature ID; explicit polyline records keep their endpoints and receive conservative bounds. §8.54. |
|
||||
| `FCavernRuntimeFeature::FSegmentNode`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | A deterministic nested segment BVH accelerates native graph distance and conservative interval evaluation; the outer graph BVH still supplies tile candidates and protects `isTileShapeFree`. The 0.4 m feature-union width is separate from the floor fillet width. §8.54. |
|
||||
| `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
|
||||
| `ApplyOwnerFourStratePlan` | `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` | BuildHost-only transient fixed-slot plan from the four staged owner strate assets; leaves the authored settings and strate packages untouched. |
|
||||
| `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.
|
||||
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
|
||||
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. |
|
||||
| `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/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` | 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/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Validates the optional manifest against each resolved `SurfaceWorld` slot (unique strate index and exact top/bottom chunks), then places canonical `world_assembly` JSON and its hash in the Core snapshot. 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. |
|
||||
| `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. |
|
||||
@@ -195,7 +180,7 @@ remains `0`, and lateral regions remain gated off.
|
||||
| Symbol | Line | Notes |
|
||||
|--------|------|-------|
|
||||
| `CHUNK_SIZE` (32) | 211 | Chunk dimensions. 64³ would cut draw calls but makes streaming too bursty; fps is fixed render-side instead. |
|
||||
| `VOXEL_SIZE` (12.5f cm) | 213 | Legacy generator scale; `CHUNK_SIZE` 32 spans 4 m here. Keep separate from the 8 m default layout chunk and 32³ Core tile. |
|
||||
| `VOXEL_SIZE` (12.5f cm) | 213 | World scale; one 32³ chunk is 4 m. |
|
||||
| `EVoxelFace` enum | 266 | 6 cube faces. |
|
||||
| `WorldToChunkCoord` | 287 | World cm → chunk coord (handles negatives via floor). |
|
||||
| `VoxelMath::IsFiniteFast` / `IsFinite` | 24 / 45 | Exact IEEE bit test (`voxel.FastIsFinite` A/B switch). |
|
||||
@@ -222,16 +207,14 @@ all eight archetypes; there is no other path. See [Docs/archive/OPSTACK-PLAN.md]
|
||||
| `FVoxelBoxHypotheses` + `FVoxelBoxSdfInterval` + `VF_ForceHypotheses` / `VF_FoldEffect` / `VF_FoldOp` | The fold that turns a stack into an `EVoxelTileClass`, carrying the SDF interval between independent writers and consumers. Unknown is biased toward `Mixed`; the mapping and soundness rule are written out in the header. |
|
||||
|
||||
### 3.2c Structural primitives — `Public/VoxelDensityPrimitives.h`
|
||||
`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyUpperBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` —
|
||||
`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` —
|
||||
the four world invariants every archetype appends, with landing/floor geometry joined by the
|
||||
manager passage post, **moved here 2026-07-27** so the generator and
|
||||
the operator stack share ONE copy. The XY helper is a radial smooth ramp in actor-space and is a
|
||||
true no-op when `WorldRadiusVoxels == 0`; `VF_XYEdgeSealForcedMarginOverBox` is the shared sound
|
||||
proof for `ClassifyBox` and T1.d's global shell skip. `VoxelGenerator.cpp` keeps same-named
|
||||
`static FORCEINLINE` forwarders for `ApplyBoundarySeal` and `ApplyOriginSpine`; the passage and
|
||||
edge helpers are called directly. `VF_ApplyUpperBoundarySealValue` contains the common upper-band
|
||||
math used by both one-sided and two-sided seals. **Convention: INTERNAL (positive = solid);
|
||||
`VF_ApplyXYEdgeSealMC` is the output wrapper.**
|
||||
edge helpers are called directly. **Convention: INTERNAL (positive = solid); `VF_ApplyXYEdgeSealMC` is the output wrapper.**
|
||||
|
||||
### 3.2d Operator stack — `Public/VoxelDensityOpStack.h` + `Private/VoxelDensityOpStack.cpp`
|
||||
⚠️ **This IS the game's generation path.** `GetDensityAt` builds the stack in its per-chunk refetch
|
||||
@@ -607,16 +590,13 @@ content lists, audio, `GameplayTags`(393). EditConditions show/hide param groups
|
||||
type.
|
||||
|
||||
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:152).
|
||||
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.
|
||||
Maps depth→strate at runtime; owns passages.
|
||||
- `FVoxelPassage` (h:46): standing endpoints, radius, type, wandering control points, and
|
||||
upper/lower local landing descriptors (room, floor, and door; no root connector).
|
||||
- `FStrateSlot` (h:128): content definition, resolved micrometre bounds/height, resolved transition
|
||||
and passage inputs, and compatibility chunk-Z projection + strate index.
|
||||
- `FStrateSlot` (h:128): definition + chunk-Z range + index.
|
||||
| Method | .cpp line | Role |
|
||||
|--------|-----------|------|
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
@@ -624,18 +604,18 @@ chunk APIs use the cached integer projection.
|
||||
| `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. |
|
||||
| `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` / `FindSlotIndexForWorldZCentimetres` | 6239 / 6251 | Chunk callers use the cached compatibility projection; world-Z callers search physical half-open micrometre intervals. |
|
||||
| `GetStrateAt` / `GetStrateIndex` | 6276 / 6287 | World-Z queries against half-open micrometre intervals. |
|
||||
| `FindSlotIndexForChunkZ` | 6111 | Z → layout index. |
|
||||
| `GetStrateAt` / `GetStrateIndex` | 6127 / 6139 | World-Z queries. |
|
||||
| `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` | 6298 | Chunk → definition via the compatibility projection. |
|
||||
| `GetGeneratorTypeForChunk` | 6324 | Chunk → generator type via the compatibility projection. |
|
||||
| `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. |
|
||||
| `GetStrateForChunk` | 6150 | Chunk → definition. |
|
||||
| `GetGeneratorTypeForChunk` | 6176 | Chunk → generator type. |
|
||||
| `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. |
|
||||
| `GetRecipeForChunk` | — | Runtime immutable recipe/vector/strate-seed copy for cooked seasons; editor slot overrides use the same worker hand-off. |
|
||||
| `GetSlabParamsForChunk` | 6415 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
||||
| `GetSlabParamsForChunk` | 6263 | 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. |
|
||||
| `GetGenerationParams` | 6661 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
||||
| `GetGenerationParams` | 6509 | **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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
|
||||
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
||||
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
|
||||
@@ -1137,221 +1117,6 @@ restores the S13 path for the negative control. Runtime CVar capture lives in
|
||||
cold-path decision are in
|
||||
[`Standalone/Research/WorldAssemblerM3/Round18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round18/REPORT.md).
|
||||
|
||||
### 3.21 LOD1 uniform-block proof — World Assembler M3 Linux9
|
||||
|
||||
`FStandaloneShapeWorld::IsNativeSurfaceBox` gates the step-2 proof to a single native SurfaceWorld
|
||||
stratum. `ClassifyBoxDetailed(..., bUseNativeLatticeProof)` delegates to
|
||||
`Heightfield2D::classifyLatticeBox`, whose ground/cap extrema cover the MC lattice and normal halo.
|
||||
`UVoxelMarchingCubesMesher::GenerateMesh` sparsely fills only Mixed blocks and their sample halo;
|
||||
Mixed/Unknown remains on the fill path. The headless `--audit-uniform-block-proofs` forces sample
|
||||
verification, while `--profile-lod-work` reports per-LOD filled samples; both are off by default.
|
||||
Implementation and evidence: [`Standalone/Research/WorldAssemblerM3/Linux9/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux9/REPORT.md).
|
||||
|
||||
### 3.22 Route-invariant Core density — World Assembler M3 Linux13
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `FStandaloneShapeWorld::SetSharedDensitySource`, `SharedDensitySource` | `Standalone/Core/src/ShapeWorldInternal.h`, bound before publication in `Standalone/Core/src/VoxelForgeCore.cpp` | Makes compiled analytic pages delegate field, lattice, detail, classification, and solid queries to the Core world's base shape; `WorldAssemblyEvaluatorVersion` is 6. |
|
||||
|
||||
The base manifest field is authoritative for tile generation and exact queries. See ARCHITECTURE
|
||||
§8.36 and [`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
||||
|
||||
### 3.23 Shared strate boundaries and FloatingIslands seal — World Assembler M3 Linux18
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `FStandaloneShapeWorld::FResolvedStratumBounds`, `MakeResolvedStratumBounds`, `GetResolvedStratumBounds` | `Standalone/Core/src/ShapeWorldInternal.h` | Resolves each strate's physical lower/upper range and seal width for layer ranges, far-field sample protection, and far-shell continuity. Native FloatingIslands uses its plan's owner/Core ratio; SurfaceWorld retains its `32` Core voxels per slot chunk. |
|
||||
| `VF_ApplyUpperBoundarySeal` / `VF_ApplyUpperBoundarySealValue` | `Standalone/Source/VoxelForge/Public/VoxelDensityPrimitives.h` and `Source/VoxelForge/Public/VoxelDensityPrimitives.h` | Shared one-sided top seal used by version-3 native FloatingIslands; the two-sided seal reuses the same upper-band math. |
|
||||
|
||||
Geometry version 3 removes the FI lower all-XY seal while retaining its ceiling through the shared
|
||||
upper-bound path. Versions 1 and 2 retain their serialized two-sided behavior. See ARCHITECTURE §8.37
|
||||
and [`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md).
|
||||
|
||||
### 3.24 Resolved inter-strate passages — World Assembler M3 L23
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `PlanResolvedLayoutPassagesV1`, `ResolvedLayoutPassageV1::lowerMouthControlIndex` | `Standalone/Core/src/ResolvedLayoutInternal.h`, public record in `VoxelForgeCore.h` | Plans deterministic fixed-metre passages from adjacent director-resolved strata, binds route departure/arrival mouths, and records the derived split between the monotone descent and route-bound landing tail. |
|
||||
| `FStandaloneShapeWorld::ApplyResolvedStrateComposition`, `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies resolved boundary bands first, then opens the passage capsule and supported floor along the passage for every native generator. |
|
||||
| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples passage capsules, floor support, mouth ownership/radius, in-strate route bindings, and a blocked-passage negative control. |
|
||||
|
||||
The evaluator version is 9. See ARCHITECTURE §8.41 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux23/REPORT.md).
|
||||
|
||||
### 3.25 Per-strate natural passages and FloatingIslands anchor helix — World Assembler L24
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `FStratePassageConfig::Style`, `EVoxelPassageStyle` | `Source/VoxelForge/Public/VoxelStrateTypes.h` | Selects Straight, Worm, Spiral or Cascading per upper strate; Worm is the default. |
|
||||
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Applies the selected style in the shared planner; a configured FI Spiral follows the anchor root into the bedrock and smoothly transitions into the inter-strate route. |
|
||||
| Anchor root recipe fields and `EvaluateIslandSDF` | `Standalone/Core/src/FloatingIslandsInternal.h` | Add the seed-stable ordinal-2 island/root without changing other island placements; geometry version 5. |
|
||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies deterministic rough wall relief and the supported route profile to all native generator fields. |
|
||||
| `--audit-resolved-layout-passages` naturalness checks | `Standalone/Source/StreamBenchmarkPipeline.h` | Checks 8 m maximum flat run, 0.20 m minimum wall relief, capsule/support/grade, mouths, routes and the blocked-passage control. |
|
||||
| `--headlamp=on`, `--clip-plane=NX,NY,NZ,D` | `Standalone/Source/LinuxOffscreenCapture.cpp` | Adds camera-directed light or discards the plane's negative half-space for offscreen cutaways; neither affects Core output. |
|
||||
|
||||
The shared evaluator is version 10, the FI recipe geometry is version 5, and resolved-layout schema
|
||||
V3 is unchanged. See ARCHITECTURE §8.42 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md).
|
||||
|
||||
### 3.26 Enclosed FloatingIslands helix and open-air support gate — World Assembler L25
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Emits route-mouth support, tread and fallback support only over pre-passage native solid density, shared by every passage style. |
|
||||
| `auditPassageOpenAir` / `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the support footprint at 0.25 m spacing and two depths; compares against L24 ungated support as a required negative control. |
|
||||
| Anchor root dimensions and relief | `Standalone/Core/src/FloatingIslandsInternal.h` | Raises the FI anchor root from a 6 m cylinder to a nominal 13.25 m relief-bearing rock column sized around the 3 m helix and 4.5 m mouth. |
|
||||
| Passage route and root measures | `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Binds the Spiral to the root and reports measured centreline/root clearance. |
|
||||
|
||||
The shared evaluator is version 12 and FI geometry version is 7; resolved-layout schema V3 is
|
||||
unchanged. See ARCHITECTURE §8.43 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md).
|
||||
|
||||
### 3.27 Tunnels in native rock with grounded passage bridges — World Assembler L26
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `BuildPassageBridgePlan` / `PassageBridgeSignedDistance` | `Standalone/Core/src/ShapeWorldInternal.h` | Uses native density sign to plan cached, connected rock spans only across unsupported route ranges; shared by both recipes and all passage styles. |
|
||||
| `PassageBridgeCache` | `Standalone/Source/OwnerWorldLoader.h` | Shares deterministic ranges by resolved-layout identity across shape instances. |
|
||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Adds bridge density before carving the bore; gates native floor support on solid input and player-capsule clearance. |
|
||||
| `auditPassageBridges` / `auditPassageOpenAir` / `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Measures bridge width, thickness, relief and connected ends; checks capsule support, open air, naturalness, route binding and the L24 negative control. |
|
||||
| `PassageBridgeRangeAuditV1` / `passageBridgeRanges()` / `isPassageBridgeSample()` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/VoxelForgeCore.cpp` | Exposes audit-only bridge ranges and density queries to the standalone validator. |
|
||||
|
||||
The shared evaluator is version 13; FloatingIslands geometry remains version 7 and resolved-layout
|
||||
schema V3 is unchanged. See ARCHITECTURE §8.44 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md).
|
||||
|
||||
### 3.28 Tile-bounded passage evaluation — World Assembler M3 Linux L28
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| FScopedPassageTileQueries / BuildPassageFeatureTileList / ResolvedPassageIntersectsBox | Standalone/Core/src/ShapeWorldInternal.h; scope entered by GenerateMesh in Standalone/Core/src/VoxelForgeCore.cpp | Builds conservative per-tile route, column and mouth-support candidates from an AABB BVH; long paths use a nearest-segment BVH and L9 box checks use a separate passage-bound BVH. Empty native-only proof indexes fall back conservatively. The density arithmetic and serialized versions stay unchanged. |
|
||||
|
||||
See ARCHITECTURE §8.45 and
|
||||
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md).
|
||||
|
||||
### 3.29 Honored bedrock/ceiling transitions — World Assembler L29
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `EvaluateResolvedLayoutTransitionBandsV1`, `AuditResolvedStrateTransitions` | Public Core header / `Standalone/Core/src/ResolvedLayoutInternal.h`; `Standalone/Source/StreamBenchmarkPipeline.h` | Resolve the deterministic mode field from seed, interface and coordinates; audit actual crossing depth, minimum band thickness, passage-excluded holes, and strict negative controls. |
|
||||
| `FStandaloneShapeWorld::ApplyResolvedStrateBoundaryBand` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies Hard, Gradient or Interleaved to both facing bands in the shared Core composition path; tapers locally at passage mouths to preserve landing support. |
|
||||
|
||||
The shared evaluator is version 15. Recipe and resolved-layout schema versions are unchanged.
|
||||
The Unreal adapter now forwards the authored transition type and chunk width; those Unreal edits
|
||||
were not compiled here. See ARCHITECTURE §8.46 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md).
|
||||
|
||||
### 3.30 SurfaceWorld as an upper strate and floor-profile joins — World Assembler L33
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `BuildFiniteStrateRoute` | `Standalone/Source/WorldAssemblyPrototype.h` | Samples route heights from the composed terrain ground; bounds candidate chords by the arrival/departure placement radii and records per-segment grade failures. |
|
||||
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Starts an upper-SurfaceWorld departure at its terrain mouth, opens a small portal and smoothsteps to the rock tunnel while orienting the ring outward. |
|
||||
| `BuildPassageVoidCrossingPlan`, `PassageBridgeSignedDistance` | `Standalone/Core/src/ShapeWorldInternal.h` | Sizes unsupported ranges from the full control-segment window only for upper SurfaceWorld; other upper generators keep the previous bracketing-sample bounds. |
|
||||
| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Uses floor height rather than bore-centre height for naturalness and records failed-only wall probes for column audits. |
|
||||
| `FloorProfileBlend` | `Standalone/Core/src/ShapeWorldInternal.h` | Measures endpoint height/tangent continuity; chord deviation is informational, with step and tangent-step negative controls. |
|
||||
|
||||
The owner SurfaceWorld hashes match L32 and evaluator/recipe versions are unchanged. See
|
||||
ARCHITECTURE §8.47 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md).
|
||||
|
||||
### 3.31 Passage-wall ripple and FI tread-shoulder audit — World Assembler M3 L35
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `VF_FieldPassageWallRelief` | `Standalone/Core/src/VoxelForgeFieldShared.h` | Supplies passage-only irregular wall relief at rotated 43/29/19 m scales. Other `VF_FieldSurfaceRelief` users stay unchanged. |
|
||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Catmull–Rom smooths eligible interior sweep joins, applies passage-only wall relief, and tapers/grains the entry support shoulder below the capsule/support probe footprint. |
|
||||
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Gives each Cascade turn a seeded shallow radial profile instead of identical repeated full-depth cosine lobes. |
|
||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples wall radii and walk-floor zero crossings at 0.125 m spacing, scans periods 0.5–8 m in 32 m windows, and rejects wall/floor amplitudes ≥0.035/0.015 m. Sphere and beaded-floor controls must be detected. |
|
||||
|
||||
`WorldAssemblyEvaluatorVersion=21`. See ARCHITECTURE §8.48 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
||||
|
||||
### 3.32 Walk-floor washboard and FI mouth blend — World Assembler M3 L36
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Extends eligible tread sections by up to 2 m and uses bounded signed-distance support on every sparse route, including SurfaceWorld-to-TunnelNetwork, removes seeded shoulder grain, and seats regular tread tops 5 cm lower with a smooth return at the lower landing. Capsule/open-air gates remain. Gravity-facing wall relief is reduced; side and ceiling relief remain. |
|
||||
| `ApplyResolvedStratePassageCarves` mouth support | `Standalone/Core/src/ShapeWorldInternal.h` | Builds exact signed-distance route-mouth tread boxes and smooth-unions them with native terrain over 1.5 m. |
|
||||
| `NativeFloatingIslandDensityAt` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies a local 2 m symmetric signed-field filter around FI passage mouths, exempts resolved route support, and removes isolated negative samples thinner than 2 m on all axes. |
|
||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Keeps the wall-radius audit and adds walk-floor zero-crossing profiles at 0.125 m, with five 32 m windows and a 0.015 m limit plus a beaded-floor negative control. |
|
||||
| `RunMouthFragmentAuditL36.py`, `ProbeWalkFloorProfileL36.py` | `Standalone/Research/WorldAssemblerM3/Linux36/` | Compare L35 floor and FI-mouth controls with L36; the mouth check fails on detached components at or above 0.0001 m² and checks normal jumps across the fade ring. |
|
||||
|
||||
`WorldAssemblyEvaluatorVersion=23`. See ARCHITECTURE §8.49 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
||||
|
||||
### 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.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||
|
||||
@@ -399,7 +399,6 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
|
||||
LegacySettings->WorldRadiusVoxels = 987.0f;
|
||||
LegacySettings->TotalStrates = 1;
|
||||
LegacyDefinition->StrateHeightInChunks = 7;
|
||||
LegacyDefinition->StrateHeightMeters = 7.0 * 8.0;
|
||||
LegacySettings->StratePool.Add(
|
||||
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
|
||||
TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
|
||||
|
||||
@@ -525,7 +525,6 @@ bool FVoxelForgeZeroTransitionBlendTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = World.Definitions[Index].Get();
|
||||
Definition->TransitionBlendChunks = 0;
|
||||
Definition->TransitionWidthMeters = 0.0;
|
||||
Definition->TransitionType = (Index & 1) == 0
|
||||
? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ namespace
|
||||
Settings->Seed = LayoutSeed;
|
||||
Settings->TotalStrates = NumPoolDefinitions;
|
||||
Settings->InterStrateGapChunks = 1;
|
||||
Settings->InterStrateGapMeters = Settings->StrateLayoutChunkSizeMeters;
|
||||
Settings->bOpenSurfaceEntry = true;
|
||||
|
||||
static const ECaveGeneratorType GeneratorTypes[] = {
|
||||
@@ -70,8 +69,6 @@ namespace
|
||||
FString::Printf(TEXT("Layout order test strate %d"), Index));
|
||||
Def->GeneratorType = GeneratorTypes[Index];
|
||||
Def->StrateHeightInChunks = 3 + Index;
|
||||
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
|
||||
* Settings->StrateLayoutChunkSizeMeters;
|
||||
Def->TransitionType = EVoxelStrateTransition::Hard;
|
||||
|
||||
// Non-trivial, distinct passage configs exercise the connection index and the
|
||||
|
||||
@@ -79,8 +79,6 @@ namespace VoxelForgeTest
|
||||
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
Settings->Seed = InSeed;
|
||||
Settings->InterStrateGapChunks = InGapChunks;
|
||||
Settings->InterStrateGapMeters = static_cast<double>(InGapChunks)
|
||||
* Settings->StrateLayoutChunkSizeMeters;
|
||||
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
|
||||
// WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0.
|
||||
|
||||
@@ -112,8 +110,6 @@ namespace VoxelForgeTest
|
||||
// refinement and roll tests use the smaller synthetic volume as a bounded memory
|
||||
// control; the owner-facing showcase opts into 8 chunks below.
|
||||
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
|
||||
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
|
||||
* Settings->StrateLayoutChunkSizeMeters;
|
||||
// Hard transitions: param blending across a boundary would make "which archetype
|
||||
// owns this chunk" ambiguous, and these tests want an unambiguous mapping.
|
||||
Def->TransitionType = EVoxelStrateTransition::Hard;
|
||||
|
||||
@@ -4,27 +4,6 @@
|
||||
|
||||
namespace VoxelClipmapDesiredTiles
|
||||
{
|
||||
FORCEINLINE int64 FloorDivWide(int64 Value, int64 Divisor)
|
||||
{
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int64 Quotient = Value / Divisor;
|
||||
const int64 Remainder = Value % Divisor;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE int64 CeilDivWide(int64 Value, int64 Divisor)
|
||||
{
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int64 Quotient = Value / Divisor;
|
||||
const int64 Remainder = Value % Divisor;
|
||||
return Remainder > 0 ? Quotient + 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE int32 ClampInt32(int64 Value)
|
||||
{
|
||||
return static_cast<int32>(FMath::Clamp<int64>(Value, MIN_int32, MAX_int32));
|
||||
}
|
||||
|
||||
struct FParameters
|
||||
{
|
||||
int32 ClipRadius = 3;
|
||||
@@ -34,10 +13,7 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
|
||||
{
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int32 Quotient = Value / Divisor;
|
||||
const int32 Remainder = Value % Divisor;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
return Value >= 0 ? (Value / Divisor) : -(((-Value) + Divisor - 1) / Divisor);
|
||||
}
|
||||
|
||||
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
|
||||
@@ -48,10 +24,7 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
|
||||
{
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int32 Quotient = Value / Divisor;
|
||||
const int32 Remainder = Value % Divisor;
|
||||
return Remainder > 0 ? Quotient + 1 : Quotient;
|
||||
return -FloorDiv(-Value, Divisor);
|
||||
}
|
||||
|
||||
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
|
||||
@@ -60,12 +33,10 @@ namespace VoxelClipmapDesiredTiles
|
||||
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
||||
int32& OutAxisMin, int32& OutAxisMax)
|
||||
{
|
||||
const int64 SafeRadius = FMath::Max<int64>(0, Radius);
|
||||
const int64 SafeAlignment = FMath::Max<int64>(1, Alignment);
|
||||
const int64 BaseMin = static_cast<int64>(CenterAxis) - SafeRadius;
|
||||
const int64 BaseEnd = static_cast<int64>(CenterAxis) + SafeRadius + 1;
|
||||
OutAxisMin = ClampInt32(FloorDivWide(BaseMin, SafeAlignment) * SafeAlignment);
|
||||
OutAxisMax = ClampInt32(CeilDivWide(BaseEnd, SafeAlignment) * SafeAlignment - 1);
|
||||
const int32 BaseMin = CenterAxis - Radius;
|
||||
const int32 BaseEnd = CenterAxis + Radius + 1;
|
||||
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
|
||||
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
|
||||
};
|
||||
AlignAxis(Center.X, OutMin.X, OutMax.X);
|
||||
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
||||
@@ -78,13 +49,8 @@ namespace VoxelClipmapDesiredTiles
|
||||
{
|
||||
return Radius;
|
||||
}
|
||||
const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8);
|
||||
const int64 Scale = int64{1} << static_cast<uint32>(SafeLevel);
|
||||
const int64 Numerator = static_cast<int64>(Parameters.RenderDistanceChunks)
|
||||
+ Scale - 1;
|
||||
const int64 DesiredRadius = Numerator / Scale;
|
||||
return static_cast<int32>(FMath::Clamp<int64>(
|
||||
FMath::Max<int64>(Radius, DesiredRadius), 0, MAX_int32));
|
||||
return FMath::Max(Radius,
|
||||
(Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel);
|
||||
}
|
||||
|
||||
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
|
||||
@@ -107,12 +73,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
return;
|
||||
}
|
||||
|
||||
OutMinChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) - ViewDistanceDown);
|
||||
OutMaxChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) + ViewDistanceUp);
|
||||
OutMinChunkZ = CenterChunkZ - ViewDistanceDown;
|
||||
OutMaxChunkZ = CenterChunkZ + ViewDistanceUp;
|
||||
if (bHasStrate)
|
||||
{
|
||||
OutMinChunkZ = ClampInt32(static_cast<int64>(StrateBottomChunkZ) - StrateViewMarginChunks);
|
||||
OutMaxChunkZ = ClampInt32(static_cast<int64>(StrateTopChunkZ) + StrateViewMarginChunks);
|
||||
OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks;
|
||||
OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +94,7 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
||||
{
|
||||
const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
|
||||
const int32 Pow = 1 << Level;
|
||||
const FIntVector CenterLevel = FloorDiv(Center, Pow);
|
||||
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
||||
|
||||
@@ -139,23 +105,24 @@ namespace VoxelClipmapDesiredTiles
|
||||
FIntVector RingMin, RingMax;
|
||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||
|
||||
int64 TileMinZ = RingMin.Z;
|
||||
int64 TileMaxZ = RingMax.Z;
|
||||
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
|
||||
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
|
||||
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
||||
{
|
||||
TileMinZ = FMath::Max<int64>(TileMinZ, FloorDiv(MinChunkZ, Pow));
|
||||
TileMaxZ = FMath::Min<int64>(TileMaxZ, FloorDiv(MaxChunkZ, Pow));
|
||||
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
|
||||
DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
|
||||
}
|
||||
|
||||
for (int64 Z = TileMinZ; Z <= TileMaxZ; ++Z)
|
||||
for (int64 Y = RingMin.Y; Y <= RingMax.Y; ++Y)
|
||||
for (int64 X = RingMin.X; X <= RingMax.X; ++X)
|
||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
||||
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
|
||||
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
|
||||
for (int32 DeltaX = RingMin.X - CenterLevel.X;
|
||||
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
|
||||
{
|
||||
const FIntVector Tile(static_cast<int32>(X), static_cast<int32>(Y),
|
||||
static_cast<int32>(Z));
|
||||
const int64 TileBottomChunkZ = static_cast<int64>(Tile.Z) * Pow;
|
||||
const int64 TileTopChunkZ = (static_cast<int64>(Tile.Z) + 1) * Pow - 1;
|
||||
if (TileTopChunkZ < MinChunkZ || TileBottomChunkZ > MaxChunkZ)
|
||||
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
|
||||
const int32 TileMinZ = Tile.Z << Level;
|
||||
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
|
||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -163,12 +130,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
if (Level > 0)
|
||||
{
|
||||
const bool bCovered =
|
||||
(2 * static_cast<int64>(Tile.X) >= PreviousRingMin.X)
|
||||
&& (2 * static_cast<int64>(Tile.X) + 1 <= PreviousRingMax.X)
|
||||
&& (2 * static_cast<int64>(Tile.Y) >= PreviousRingMin.Y)
|
||||
&& (2 * static_cast<int64>(Tile.Y) + 1 <= PreviousRingMax.Y)
|
||||
&& (2 * static_cast<int64>(Tile.Z) >= PreviousRingMin.Z)
|
||||
&& (2 * static_cast<int64>(Tile.Z) + 1 <= PreviousRingMax.Z);
|
||||
(2 * Tile.X >= PreviousRingMin.X)
|
||||
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
|
||||
&& (2 * Tile.Y >= PreviousRingMin.Y)
|
||||
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
|
||||
&& (2 * Tile.Z >= PreviousRingMin.Z)
|
||||
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
|
||||
if (bCovered)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Tasks/Task.h"
|
||||
#include "HAL/PlatformProcess.h"
|
||||
#include "Misc/Crc.h"
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include <cmath>
|
||||
@@ -241,10 +240,10 @@ static std::vector<VoxelForgeCore::WorldAssemblyFeatureQueryV1> VF_ContentFeatur
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
constexpr double Q = WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<std::int64_t,3> Min{static_cast<std::int64_t>(std::floor(MinX*Q)),
|
||||
static_cast<std::int64_t>(std::floor(MinY*Q)),static_cast<std::int64_t>(std::floor(MinZ*Q))};
|
||||
const std::array<std::int64_t,3> Max{static_cast<std::int64_t>(std::ceil(MaxX*Q)),
|
||||
static_cast<std::int64_t>(std::ceil(MaxY*Q)),static_cast<std::int64_t>(std::ceil(MaxZ*Q))};
|
||||
const std::array<int64,3> Min{static_cast<int64>(std::floor(MinX*Q)),
|
||||
static_cast<int64>(std::floor(MinY*Q)),static_cast<int64>(std::floor(MinZ*Q))};
|
||||
const std::array<int64,3> Max{static_cast<int64>(std::ceil(MaxX*Q)),
|
||||
static_cast<int64>(std::ceil(MaxY*Q)),static_cast<int64>(std::ceil(MaxZ*Q))};
|
||||
std::vector<WorldAssemblyFeatureQueryV1> Queries;
|
||||
if (Surface)
|
||||
if (const auto* Plan = Surface->FindPlan(StrateIndex))
|
||||
@@ -323,54 +322,11 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
|
||||
Seed = InSeed;
|
||||
NativeSurfaceSnapshot.Reset();
|
||||
NativeFloatingIslandSnapshot.Reset();
|
||||
CoreGeometryAnchors.Reset();
|
||||
|
||||
if (Settings)
|
||||
{
|
||||
FString ManifestJson, ManifestHash, ManifestError;
|
||||
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
|
||||
FString ResolvedLayoutJson, ResolvedLayoutIdentity;
|
||||
if (VoxelWorld != nullptr
|
||||
&& VoxelWorld->GetVoxelForgeCoreResolvedLayout(ResolvedLayoutJson, ResolvedLayoutIdentity))
|
||||
{
|
||||
FTCHARToUTF8 Utf8Layout(*ResolvedLayoutJson);
|
||||
std::string LayoutError;
|
||||
const std::optional<VoxelForgeCore::ResolvedLayoutSnapshotV3> ResolvedLayout =
|
||||
VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(
|
||||
std::string_view(Utf8Layout.Get(), static_cast<std::size_t>(Utf8Layout.Length())), LayoutError);
|
||||
if (ResolvedLayout)
|
||||
{
|
||||
const auto AddAnchor = [this](const std::string& StableId, int32 StrateIndex,
|
||||
const std::array<std::int64_t, 3>& PositionMicrometres)
|
||||
{
|
||||
const FVector PositionCm(
|
||||
static_cast<double>(PositionMicrometres[0]) / 10000.0,
|
||||
static_cast<double>(PositionMicrometres[1]) / 10000.0,
|
||||
static_cast<double>(PositionMicrometres[2]) / 10000.0);
|
||||
const FString FullName = UTF8_TO_TCHAR(StableId.c_str());
|
||||
FResolvedCoreGeometryAnchor Anchor{ StrateIndex, PositionCm };
|
||||
CoreGeometryAnchors.Add(FullName, Anchor);
|
||||
const int32 Marker = FullName.Find(TEXT("/core/"), ESearchCase::CaseSensitive, ESearchDir::FromEnd);
|
||||
if (Marker != INDEX_NONE)
|
||||
{
|
||||
const FString ShortName = FullName.Mid(Marker + 6);
|
||||
if (!ShortName.IsEmpty()) CoreGeometryAnchors.Add(ShortName, Anchor);
|
||||
}
|
||||
};
|
||||
for (const VoxelForgeCore::ResolvedLayoutStampV1& Stamp : ResolvedLayout->stamps)
|
||||
AddAnchor(Stamp.stableId, Stamp.strateIndex, Stamp.positionMicrometres);
|
||||
for (const VoxelForgeCore::ResolvedCavernRoomV1& Room : ResolvedLayout->cavernRooms)
|
||||
AddAnchor(Room.stableId, Room.strateIndex, Room.centerMicrometres);
|
||||
for (const VoxelForgeCore::ResolvedCavernEdgeV1& Edge : ResolvedLayout->cavernEdges)
|
||||
if (!Edge.polylineMicrometres.empty())
|
||||
AddAnchor(Edge.stableId, Edge.strateIndex, Edge.polylineMicrometres.front());
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("[VoxelContentManager] Could not read resolved Core geometry anchors: %s"),
|
||||
UTF8_TO_TCHAR(LayoutError.c_str()));
|
||||
}
|
||||
}
|
||||
const bool bHasRuntimeAssembly = VoxelWorld != nullptr
|
||||
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
|
||||
if (!bHasRuntimeAssembly)
|
||||
@@ -603,13 +559,9 @@ int32 UVoxelContentManager::RegionSize() const
|
||||
// truncation, a negative-coord cell is queued under one region but routed back to another → its march
|
||||
// result is dropped → blank chunk. Truncation == floor for A>=0 and for R==1, which is exactly why only
|
||||
// NEGATIVE coordinates at R>=2 were affected. R (region size) is always > 0 here.
|
||||
static FORCEINLINE int32 FloorDivPos(int64 A, int32 R)
|
||||
static FORCEINLINE int32 FloorDivPos(int32 A, int32 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));
|
||||
return (A >= 0) ? (A / R) : -(((-A) + R - 1) / R);
|
||||
}
|
||||
|
||||
static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
|
||||
@@ -621,10 +573,10 @@ static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
|
||||
static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell,
|
||||
int32 FarR, int32 R)
|
||||
{
|
||||
const int32 MinX = FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R);
|
||||
const int32 MaxX = FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R);
|
||||
const int32 MinY = FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R);
|
||||
const int32 MaxY = FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R);
|
||||
const int32 MinX = FloorDivPos(PlayerCell.X - FarR, R);
|
||||
const int32 MaxX = FloorDivPos(PlayerCell.X + FarR, R);
|
||||
const int32 MinY = FloorDivPos(PlayerCell.Y - FarR, R);
|
||||
const int32 MaxY = FloorDivPos(PlayerCell.Y + FarR, R);
|
||||
return Region.X >= MinX && Region.X <= MaxX && Region.Y >= MinY && Region.Y <= MaxY;
|
||||
}
|
||||
|
||||
@@ -1086,19 +1038,8 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
|
||||
OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual,
|
||||
FMath::Abs(static_cast<double>(Density)));
|
||||
++OutAudit.AnchorChecks;
|
||||
// Core content crossings use the same finite-step column refinement as runtime
|
||||
// placement. Allow at most half a Core voxel of field residual in this audit.
|
||||
// Les croisements de contenu Core gardent le raffinement discret du runtime;
|
||||
// l'audit tolere au plus un demi-voxel Core de residu de champ.
|
||||
if (FMath::Abs(Density)>0.5f)
|
||||
{
|
||||
OutError=FString::Printf(
|
||||
TEXT("Populated fixture is not anchored on the canonical field: residual=%.4f core_vox=(%.3f,%.3f,%.3f) local_cm=(%.2f,%.2f,%.2f)."),
|
||||
Density, Local.X/CurrentCtx.VoxelSizeCm,
|
||||
Local.Y/CurrentCtx.VoxelSizeCm, Local.Z/CurrentCtx.VoxelSizeCm,
|
||||
Local.X, Local.Y, Local.Z);
|
||||
return false;
|
||||
}
|
||||
if (FMath::Abs(Density)>0.25f)
|
||||
{ OutError=TEXT("Populated fixture is not anchored on the canonical field."); return false; }
|
||||
}
|
||||
}
|
||||
if (!Spawns.IsEmpty())
|
||||
@@ -1161,22 +1102,17 @@ void UVoxelContentManager::RebuildDesiredCells(FDecoGrid& G, const FIntPoint& Pl
|
||||
const int32 R = RegionSize();
|
||||
|
||||
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
|
||||
const FIntPoint RMin(FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R),
|
||||
FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R));
|
||||
const FIntPoint RMax(FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R),
|
||||
FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R));
|
||||
const FIntPoint RMin(FloorDivPos(PlayerCell.X - FarR, R),
|
||||
FloorDivPos(PlayerCell.Y - FarR, R));
|
||||
const FIntPoint RMax(FloorDivPos(PlayerCell.X + FarR, R),
|
||||
FloorDivPos(PlayerCell.Y + FarR, R));
|
||||
|
||||
TSet<FIntPoint> DesiredRegions;
|
||||
constexpr int64 MaxRegionReserve = 4096;
|
||||
const int64 WidthX = static_cast<int64>(RMax.X) - RMin.X + 1;
|
||||
const int64 WidthY = static_cast<int64>(RMax.Y) - RMin.Y + 1;
|
||||
const int64 ReserveCount = WidthY > 0 && WidthX > MaxRegionReserve / WidthY
|
||||
? MaxRegionReserve : FMath::Max<int64>(0, WidthX * WidthY);
|
||||
DesiredRegions.Reserve(static_cast<int32>(ReserveCount));
|
||||
for (int64 ry = RMin.Y; ry <= RMax.Y; ++ry)
|
||||
for (int64 rx = RMin.X; rx <= RMax.X; ++rx)
|
||||
DesiredRegions.Reserve((RMax.X - RMin.X + 1) * (RMax.Y - RMin.Y + 1));
|
||||
for (int32 ry = RMin.Y; ry <= RMax.Y; ++ry)
|
||||
for (int32 rx = RMin.X; rx <= RMax.X; ++rx)
|
||||
{
|
||||
DesiredRegions.Add(FIntPoint(static_cast<int32>(rx), static_cast<int32>(ry)));
|
||||
DesiredRegions.Add(FIntPoint(rx, ry));
|
||||
}
|
||||
|
||||
// Unload loaded regions no longer desired (plain DestroyComponent — no per-instance removal).
|
||||
@@ -1528,15 +1464,8 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
|
||||
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
|
||||
|
||||
const int64 Span = FMath::Max<int64>(0,
|
||||
static_cast<int64>(Comp.CountMax) - Comp.CountMin);
|
||||
const uint64 CountRange = static_cast<uint64>(Span) + 1u;
|
||||
const float CountOffset = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(CH ^ 0x1234u)) * static_cast<float>(CountRange);
|
||||
const int64 CountWide = static_cast<int64>(Comp.CountMin)
|
||||
+ static_cast<int64>(CountOffset);
|
||||
const int32 Count = static_cast<int32>(FMath::Clamp<int64>(
|
||||
CountWide, MIN_int32, MAX_int32));
|
||||
const int32 Span = FMath::Max(0, Comp.CountMax - Comp.CountMin);
|
||||
const int32 Count = Comp.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(CH ^ 0x1234u)) * (float)(Span + 1));
|
||||
for (int32 ii = 0; ii < Count; ++ii)
|
||||
{
|
||||
if (CompBudget <= 0) break;
|
||||
@@ -1585,15 +1514,8 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
|
||||
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
|
||||
|
||||
const int64 SubSpan = FMath::Max<int64>(0,
|
||||
static_cast<int64>(Sub.CountMax) - Sub.CountMin);
|
||||
const uint64 SubCountRange = static_cast<uint64>(SubSpan) + 1u;
|
||||
const float SubCountOffset = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(SH ^ 0x1234u)) * static_cast<float>(SubCountRange);
|
||||
const int64 SubCountWide = static_cast<int64>(Sub.CountMin)
|
||||
+ static_cast<int64>(SubCountOffset);
|
||||
const int32 SubCount = static_cast<int32>(FMath::Clamp<int64>(
|
||||
SubCountWide, MIN_int32, MAX_int32));
|
||||
const int32 SubSpan = FMath::Max(0, Sub.CountMax - Sub.CountMin);
|
||||
const int32 SubCount = Sub.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(SH ^ 0x1234u)) * (float)(SubSpan + 1));
|
||||
for (int32 sj = 0; sj < SubCount; ++sj)
|
||||
{
|
||||
if (CompBudget <= 0) break;
|
||||
@@ -2452,28 +2374,6 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
|
||||
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
|
||||
if (ExclWorld > 0.0f) bAnyExclusion = true;
|
||||
|
||||
if (!L.CoreGeometryStableName.IsEmpty())
|
||||
{
|
||||
const FResolvedCoreGeometryAnchor* Anchor = CoreGeometryAnchors.Find(L.CoreGeometryStableName);
|
||||
if (Anchor == nullptr || Anchor->StrateIndex != StrateIndex) continue;
|
||||
const uint32 StableNameHash = FCrc::StrCrc32(*L.CoreGeometryStableName);
|
||||
const uint32 H = DecoHash(static_cast<int32>(StableNameHash), StrateIndex,
|
||||
0, 0, 0, EntryIdx, LocalSeed, 0xC04E6E0u);
|
||||
if (VoxelHash::ToFloat01(H) > L.SpawnProbability) continue;
|
||||
const float LocalX = static_cast<float>(Anchor->LocalPositionCm.X);
|
||||
const float LocalY = static_cast<float>(Anchor->LocalPositionCm.Y);
|
||||
const float dx = LocalX - LocalPlayer.X, dy = LocalY - LocalPlayer.Y;
|
||||
const float DistSq = dx * dx + dy * dy;
|
||||
if (DistSq <= GatherWorld * GatherWorld)
|
||||
{
|
||||
// A Core-bound landmark is one stable candidate, independent of the hash lattice.
|
||||
// Un landmark lie a Core reste un candidat stable, independant de la grille de hachage.
|
||||
Cands.Add({ FIntVector(EntryIdx, 0, EntryIdx), EntryIdx, H, LocalX, LocalY,
|
||||
ExclWorld, L.Priority, DistSq <= RadiusWorld * RadiusWorld });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (L.AnchorMode == ELandmarkAnchor::HashLattice)
|
||||
{
|
||||
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
|
||||
|
||||
@@ -155,11 +155,9 @@ int32 UVoxelDensityVolume::NumLevels() const
|
||||
|
||||
FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B)
|
||||
{
|
||||
// True floor division (B > 0). Remainder adjustment handles INT_MIN without negating it.
|
||||
if (B <= 0) { return 0; }
|
||||
const int32 Quotient = A / B;
|
||||
const int32 Remainder = A % B;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
// True floor division (B > 0). FMath::DivideAndRoundDown truncates toward zero for negatives —
|
||||
// a footgun the content manager hit too (see FloorDivPos there). Cells span the origin, so floor.
|
||||
return (A >= 0) ? (A / B) : -(((-A) + B - 1) / B);
|
||||
}
|
||||
|
||||
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
|
||||
@@ -182,8 +180,7 @@ void UVoxelDensityVolume::EnsureAllocated()
|
||||
for (int32 L = 0; L < N; ++L)
|
||||
{
|
||||
FClipLevel& Lv = Levels[L];
|
||||
Lv.Step = static_cast<int32>(uint32{1}
|
||||
<< static_cast<uint32>(FMath::Clamp(L, 0, 8)));
|
||||
Lv.Step = 1 << L;
|
||||
Lv.OriginCells = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||
Lv.bHasData = false;
|
||||
Lv.Density.SetNumZeroed(Count); // start all-air (0)
|
||||
@@ -635,8 +632,7 @@ void UVoxelDensityVolume::ProcessOneFill(const FPendingFill& F)
|
||||
if (!Gen) return;
|
||||
if (bShuttingDown.load(std::memory_order_relaxed) || bFillThreadStop.load(std::memory_order_relaxed)) return;
|
||||
|
||||
const int32 Step = static_cast<int32>(uint32{1}
|
||||
<< static_cast<uint32>(FMath::Clamp(F.Level, 0, 8)));
|
||||
const int32 Step = 1 << F.Level;
|
||||
FFillResult R;
|
||||
R.Level = F.Level;
|
||||
R.Epoch = F.Epoch;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
#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
@@ -2590,8 +2590,8 @@ static float EvaluateInternal(const UVoxelGenerator& Generator,
|
||||
{
|
||||
EnsurePlan(Generator);
|
||||
}
|
||||
const bool bChunkResolved = (Context != nullptr && Context->bChunkResolved)
|
||||
|| (ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved);
|
||||
const bool bChunkResolved = Context != nullptr && Context->bChunkResolved
|
||||
|| ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved;
|
||||
const FIntVector Chunk = Context != nullptr && Context->bChunkResolved
|
||||
? Context->Chunk
|
||||
: ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved
|
||||
|
||||
@@ -1958,11 +1958,10 @@ namespace VoxelSeasonCompositionPrivate
|
||||
}
|
||||
|
||||
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
|
||||
double LayoutChunkMeters, FVoxelSeasonFixedStrate& OutFixed)
|
||||
FVoxelSeasonFixedStrate& OutFixed)
|
||||
{
|
||||
OutFixed = FVoxelSeasonFixedStrate();
|
||||
OutFixed.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
|
||||
Definition.StrateHeightMeters / FMath::Max(0.001, LayoutChunkMeters)));
|
||||
OutFixed.HeightInChunks = Definition.StrateHeightInChunks;
|
||||
OutFixed.Archetype = Definition.GeneratorType;
|
||||
OutFixed.SourceDefinitionPath = Definition.GetPathName();
|
||||
OutFixed.bUsesRecipe = false;
|
||||
@@ -2000,9 +1999,7 @@ namespace VoxelSeasonCompositionPrivate
|
||||
return false;
|
||||
}
|
||||
FVoxelSeasonFixedStrate Fixed;
|
||||
const double LayoutChunkMeters = Settings.WorldSettings != nullptr
|
||||
? Settings.WorldSettings->StrateLayoutChunkSizeMeters : 8.0;
|
||||
CopyDefinitionParams(*Definition, LayoutChunkMeters, Fixed);
|
||||
CopyDefinitionParams(*Definition, Fixed);
|
||||
Fixed.Seed = SeasonSeed;
|
||||
if (Fixed.SourceDefinitionPath.IsEmpty())
|
||||
{
|
||||
@@ -2977,9 +2974,7 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* Wo
|
||||
if (WorldSettings != nullptr)
|
||||
{
|
||||
Settings.SelectedStrateCount = WorldSettings->TotalStrates;
|
||||
Settings.InterStrateGapChunks = FMath::Max(0, FMath::RoundToInt(
|
||||
WorldSettings->InterStrateGapMeters * 1000000.0
|
||||
/ FMath::Max(1.0, static_cast<double>(WorldSettings->StrateLayoutChunkSizeMeters) * 1000000.0)));
|
||||
Settings.InterStrateGapChunks = WorldSettings->InterStrateGapChunks;
|
||||
Settings.OriginSpineRadius = WorldSettings->OriginSpineRadius;
|
||||
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
|
||||
Settings.SeasonNumber = WorldSettings->CurrentSeason;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "VoxelSettings.h"
|
||||
|
||||
#include "VoxelForgeAssetVersion.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelSeasonAsset.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -11,74 +9,6 @@
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include <string>
|
||||
|
||||
void UVoxelSettings::Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
|
||||
Super::Serialize(Ar);
|
||||
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
|
||||
< VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
|
||||
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
|
||||
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
|
||||
* LayoutChunkMeters;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetUpgrade] type=Settings asset=%s gap_chunks=%d gap_m=%.6f layout_chunk_m=%.6f"),
|
||||
*GetPathName(), InterStrateGapChunks, InterStrateGapMeters, LayoutChunkMeters);
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelSettings::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
||||
>= VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the legacy count against this settings asset's saved physical layout width.
|
||||
// Convertit les anciens comptes avec la largeur physique sauvegardee dans ces reglages.
|
||||
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
|
||||
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
|
||||
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
|
||||
* LayoutChunkMeters;
|
||||
|
||||
TSet<UVoxelStrateDefinition*> Migrated;
|
||||
const auto MigrateStrate = [this, LayoutChunkMeters, &Migrated](
|
||||
const TSoftObjectPtr<UVoxelStrateDefinition>& SoftDefinition)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = SoftDefinition.LoadSynchronous();
|
||||
if (Definition == nullptr || Migrated.Contains(Definition)
|
||||
|| Definition->GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
||||
>= VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Migrated.Add(Definition);
|
||||
const int32 OldHeightChunks = Definition->StrateHeightInChunks;
|
||||
const int32 OldTransitionChunks = Definition->TransitionBlendChunks;
|
||||
Definition->StrateHeightMeters = static_cast<double>(FMath::Max(1, OldHeightChunks))
|
||||
* LayoutChunkMeters;
|
||||
Definition->TransitionWidthMeters = static_cast<double>(FMath::Max(0, OldTransitionChunks))
|
||||
* LayoutChunkMeters;
|
||||
Definition->MarkPackageDirty();
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f effective_layout_chunk_m=%.6f"),
|
||||
*Definition->GetPathName(), OldHeightChunks, Definition->StrateHeightMeters,
|
||||
OldTransitionChunks, Definition->TransitionWidthMeters, LayoutChunkMeters);
|
||||
};
|
||||
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Definition : StratePool)
|
||||
{
|
||||
MigrateStrate(Definition);
|
||||
}
|
||||
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Definition : FixedStrates)
|
||||
{
|
||||
MigrateStrate(Definition.Value);
|
||||
}
|
||||
MarkPackageDirty();
|
||||
}
|
||||
|
||||
int32 UVoxelSettings::GetEffectiveWorldSeed() const
|
||||
{
|
||||
if (!Season.IsNull())
|
||||
|
||||
@@ -297,11 +297,7 @@ void RecordTile(const FTileSample& Sample)
|
||||
"\"submitted_t_s\":%s,\"worker_start_t_s\":%s,\"worker_end_t_s\":%s,\"worker_thread_id\":%u,"
|
||||
"\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu,"
|
||||
"\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,\"worker_task_s\":%s,"
|
||||
"\"generation_s\":%s,"
|
||||
#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"),
|
||||
"\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
|
||||
Sample.TileX, Sample.TileY, Sample.TileZ,
|
||||
Sample.Level, Sample.WorkPriority,
|
||||
static_cast<unsigned long long>(Sample.RequestOrdinal),
|
||||
@@ -316,12 +312,8 @@ void RecordTile(const FTileSample& Sample)
|
||||
static_cast<unsigned long long>(Sample.GeometryBytes),
|
||||
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
||||
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
||||
*Number(Sample.WorkerTaskSeconds), *Number(Sample.GenerationSeconds),
|
||||
#if PLATFORM_LINUX
|
||||
*Number(Sample.CoreGenerateSeconds), *Number(Sample.MeshCopySeconds),
|
||||
*Number(Sample.StreamBuildSeconds),
|
||||
#endif
|
||||
*Number(Sample.ClassifySeconds),
|
||||
*Number(Sample.WorkerTaskSeconds),
|
||||
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds),
|
||||
*Number(Sample.MeshSeconds), *Number(Sample.StreamSeconds), *Number(Sample.ApplySeconds)));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
// Serialized strate-definition migrations.
|
||||
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelForgeAssetVersion.h"
|
||||
|
||||
#include "Serialization/CustomVersion.h"
|
||||
#include "VoxelPassageGeometry.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = VoxelForgeAssetVersion::TunnelFloorSlopeDegrees;
|
||||
// The field name is retained for package compatibility, but version 1 changes its meaning
|
||||
// from a raw tangent/gradient to authored degrees. This GUID is local to the plugin.
|
||||
const FGuid GVoxelForgeStrateSerializationVersion(
|
||||
0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
|
||||
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = 1;
|
||||
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
|
||||
VoxelForgeAssetVersion::GUID,
|
||||
VoxelForgeAssetVersion::MetreAuthoringV1,
|
||||
GVoxelForgeStrateSerializationVersion,
|
||||
GVoxelForgeStrateGentleSlopeDegreesVersion,
|
||||
TEXT("VoxelForgeStrateSerialization"));
|
||||
|
||||
float LegacyGradientToDegrees(float LegacyGradient)
|
||||
@@ -55,31 +58,6 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelStrateDefinition::Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
|
||||
Super::Serialize(Ar);
|
||||
|
||||
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
|
||||
< VoxelForgeAssetVersion::MetreAuthoringV1)
|
||||
{
|
||||
// A stand-alone strate has no owning settings object during Serialize. Convert using the
|
||||
// current 8 m layout default here; UVoxelSettings corrects referenced old strates using
|
||||
// their saved effective width during its own load upgrade.
|
||||
// Une strate seule n'a pas ses reglages proprietaires ici; UVoxelSettings corrige ensuite
|
||||
// les references avec leur largeur physique sauvegardee.
|
||||
constexpr double LegacyDefaultLayoutChunkMeters = 8.0;
|
||||
StrateHeightMeters = static_cast<double>(FMath::Max(1, StrateHeightInChunks))
|
||||
* LegacyDefaultLayoutChunkMeters;
|
||||
TransitionWidthMeters = static_cast<double>(FMath::Max(0, TransitionBlendChunks))
|
||||
* LegacyDefaultLayoutChunkMeters;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f fallback_layout_chunk_m=%.6f"),
|
||||
*GetPathName(), StrateHeightInChunks, StrateHeightMeters,
|
||||
TransitionBlendChunks, TransitionWidthMeters, LegacyDefaultLayoutChunkMeters);
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelStrateDefinition::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
@@ -89,7 +67,7 @@ void UVoxelStrateDefinition::PostLoad()
|
||||
// is normalized to the ignored compatibility default before any runtime copy is made.
|
||||
GenerationParams.bTunnelFloorEnabled = true;
|
||||
|
||||
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
||||
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion)
|
||||
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include <cmath> // std::llround
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Crc.h"
|
||||
#include "Misc/Parse.h"
|
||||
@@ -25,27 +24,6 @@ namespace
|
||||
{
|
||||
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-
|
||||
// preserving rewrites in this round are deliberately unconditional; this switch isolates
|
||||
// the projection reuse without changing any density arithmetic.
|
||||
@@ -343,7 +321,7 @@ namespace
|
||||
const FVector ChunkCenter(
|
||||
(ChunkCoord.X + 0.5f) * (float)CHUNK_SIZE,
|
||||
(ChunkCoord.Y + 0.5f) * (float)CHUNK_SIZE,
|
||||
(ChunkCoord.Z + 0.5f) * Manager->GetStrateLayoutChunkWidthVoxels());
|
||||
(ChunkCoord.Z + 0.5f) * (float)CHUNK_SIZE);
|
||||
const float ChunkRadius = (float)CHUNK_SIZE * 0.8660254f + 3.0f;
|
||||
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
|
||||
Cache.FloorProjections.SetNum(Passages.Num());
|
||||
@@ -1125,7 +1103,7 @@ static bool VF_FindWalkableTunnelAir(
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(Position.X / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(Position.Y / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(Position.Z / Manager->GetStrateLayoutChunkWidthVoxels()));
|
||||
FMath::FloorToInt(Position.Z / (float)CHUNK_SIZE));
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord);
|
||||
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
|
||||
const FVoxelPassage* PassageData = Passages.GetData();
|
||||
@@ -1160,7 +1138,7 @@ static bool VF_IsAnyPassageFloorAt(
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(Position.X / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(Position.Y / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(Position.Z / Manager->GetStrateLayoutChunkWidthVoxels()));
|
||||
FMath::FloorToInt(Position.Z / (float)CHUNK_SIZE));
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord);
|
||||
const TArray<FVoxelPassage>& Passages = Manager->GetPassages();
|
||||
const FVoxelPassage* PassageData = Passages.GetData();
|
||||
@@ -1191,17 +1169,6 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
UE_LOG(LogTemp, Error, TEXT("[StrateManager] No settings provided!"));
|
||||
return false;
|
||||
}
|
||||
if (!FMath::IsFinite(Settings->StrateLayoutChunkSizeMeters)
|
||||
|| Settings->StrateLayoutChunkSizeMeters < 0.25f
|
||||
|| Settings->StrateLayoutChunkSizeMeters > 64.0f)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[StrateManager] StrateLayoutChunkSizeMeters must be in [0.25, 64] m."));
|
||||
return false;
|
||||
}
|
||||
StrateLayoutChunkSizeMicrometres = static_cast<uint32>(std::llround(
|
||||
static_cast<double>(Settings->StrateLayoutChunkSizeMeters) * 1000000.0));
|
||||
StrateLayoutChunkWidthVoxels = static_cast<float>(
|
||||
static_cast<double>(StrateLayoutChunkSizeMicrometres) / (10000.0 * VOXEL_SIZE));
|
||||
|
||||
FVoxelSeasonManifest SeasonManifest;
|
||||
bool bUseSeason = false;
|
||||
@@ -1255,8 +1222,7 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
? SeasonManifest.Strates.Num() : Settings->TotalStrates;
|
||||
const int32 LayoutGapChunks = bUseSeason
|
||||
? SeasonManifest.InterStrateGapChunks
|
||||
: FMath::Max(0, FMath::RoundToInt(static_cast<double>(Settings->InterStrateGapMeters)
|
||||
* 1000000.0 / std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
||||
: FMath::Max(0, Settings->InterStrateGapChunks);
|
||||
|
||||
//=========================================================================
|
||||
// STEP 1: Build shuffled pool (seed-based randomization)
|
||||
@@ -1323,11 +1289,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the stack in physical integer micrometres first. The historical chunk indices are
|
||||
// projected from these half-open bounds only after each slot is complete.
|
||||
int64 CurrentTopWorldMicrometres = StrateLayoutChunkSizeMicrometres;
|
||||
const int64 ResolvedGapMicrometres = static_cast<int64>(LayoutGapChunks)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
// Current Z position (in chunks). Starts at 0 and goes downward (negative).
|
||||
int32 CurrentTopZ = 0;
|
||||
|
||||
for (int32 i = 0; i < TotalStrates; i++)
|
||||
{
|
||||
@@ -1370,8 +1333,7 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
|
||||
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
|
||||
Slot.Definition->StrateHeightMeters = static_cast<double>(SeasonStrate.HeightInChunks)
|
||||
* StrateLayoutChunkSizeMicrometres / 1000000.0;
|
||||
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
|
||||
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
|
||||
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
|
||||
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams;
|
||||
@@ -1380,31 +1342,18 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
Slot.Definition->FloatingIslandParams = SeasonStrate.Params.FloatingIslandParams;
|
||||
|
||||
Slot.HeightInChunks = SeasonStrate.HeightInChunks;
|
||||
const int32 LayoutChunkVoxels=FMath::RoundToInt(StrateLayoutChunkWidthVoxels);
|
||||
Slot.TopChunkZ = SeasonStrate.TopWorldZ / LayoutChunkVoxels - 1;
|
||||
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
|
||||
|| SeasonStrate.BottomWorldZ % LayoutChunkVoxels != 0
|
||||
|| Slot.TopChunkZ != ExpectedTopChunk
|
||||
|| Slot.BottomChunkZ != ExpectedBottomChunk
|
||||
|| Slot.TopWorldMicrometres != CurrentTopWorldMicrometres
|
||||
|| Slot.HeightMicrometres != static_cast<int64>(Slot.HeightInChunks)
|
||||
* StrateLayoutChunkSizeMicrometres)
|
||||
Slot.TopChunkZ = SeasonStrate.TopWorldZ / CHUNK_SIZE - 1;
|
||||
Slot.BottomChunkZ = SeasonStrate.BottomWorldZ / CHUNK_SIZE;
|
||||
if (SeasonStrate.TopWorldZ % CHUNK_SIZE != 0
|
||||
|| SeasonStrate.BottomWorldZ % CHUNK_SIZE != 0
|
||||
|| Slot.TopChunkZ != CurrentTopZ
|
||||
|| Slot.BottomChunkZ != CurrentTopZ - (Slot.HeightInChunks - 1))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[StrateManager] Season slot %d bounds do not describe the declared stacked layout."), i);
|
||||
RestorePreviousLayout();
|
||||
return false;
|
||||
}
|
||||
CurrentTopWorldMicrometres = Slot.BottomWorldMicrometres - ResolvedGapMicrometres;
|
||||
SeasonStrates.Add(SeasonStrate);
|
||||
}
|
||||
else
|
||||
@@ -1427,54 +1376,15 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve the authored chunk height into an exact physical interval.
|
||||
Slot.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
|
||||
Slot.Definition->StrateHeightMeters * 1000000.0
|
||||
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
||||
Slot.TopWorldMicrometres = CurrentTopWorldMicrometres;
|
||||
Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
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;
|
||||
// Compute Z range from definition's height
|
||||
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
||||
Slot.TopChunkZ = CurrentTopZ;
|
||||
Slot.BottomChunkZ = CurrentTopZ - (Slot.HeightInChunks - 1);
|
||||
}
|
||||
|
||||
Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
|
||||
Slot.ResolvedTransitionBlendChunks = FMath::Max(0, FMath::RoundToInt(
|
||||
Slot.Definition->TransitionWidthMeters * 1000000.0
|
||||
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
|
||||
Slot.TransitionWidthMicrometres = static_cast<int64>(Slot.ResolvedTransitionBlendChunks)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig;
|
||||
Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.MouthRadius);
|
||||
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);
|
||||
// Move the cursor down for the next strate, leaving a solid-bedrock gap of
|
||||
// InterStrateGapChunks chunks between this strate and the next.
|
||||
CurrentTopZ = Slot.BottomChunkZ - 1 - LayoutGapChunks;
|
||||
|
||||
StrateLayout.Add(Slot);
|
||||
|
||||
@@ -1507,7 +1417,6 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
OriginSpineRadius = bUseSeason
|
||||
? SeasonManifest.OriginSpineRadius : Settings->OriginSpineRadius;
|
||||
InterStrateGapChunks = LayoutGapChunks;
|
||||
ResolvedInterStrateGapMicrometres = ResolvedGapMicrometres;
|
||||
if (bUseSeason)
|
||||
{
|
||||
ActiveSeasonContentHash = SeasonManifest.ContentHash;
|
||||
@@ -1616,8 +1525,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
||||
}
|
||||
VF_SetComposerRuntimeBounds(
|
||||
Override->Params,
|
||||
TargetSlot->LegacyTopWorldZVoxels,
|
||||
TargetSlot->LegacyBottomWorldZVoxels);
|
||||
(float)(TargetSlot->TopChunkZ + 1) * CHUNK_SIZE,
|
||||
(float)TargetSlot->BottomChunkZ * CHUNK_SIZE);
|
||||
Override->bUseRecipe = bUseRecipe;
|
||||
if (Recipe != nullptr)
|
||||
{
|
||||
@@ -1636,8 +1545,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
||||
Override->bUseRegions = true;
|
||||
Override->Regions = *InRegions;
|
||||
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
|
||||
Override->Regions.StrateTopWorldZ = TargetSlot->LegacyTopWorldZVoxels;
|
||||
Override->Regions.StrateBottomWorldZ = TargetSlot->LegacyBottomWorldZVoxels;
|
||||
Override->Regions.StrateTopWorldZ = (float)(TargetSlot->TopChunkZ + 1) * CHUNK_SIZE;
|
||||
Override->Regions.StrateBottomWorldZ = (float)TargetSlot->BottomChunkZ * CHUNK_SIZE;
|
||||
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
|
||||
Override->Regions.StrateTopWorldZ,
|
||||
Override->Regions.StrateBottomWorldZ);
|
||||
@@ -1884,36 +1793,14 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
// that opens the next room rather than a second vertical spine.
|
||||
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
|
||||
if (!UpperDef) continue;
|
||||
// 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);
|
||||
const FStratePassageConfig& Cfg = UpperDef->PassageConfig;
|
||||
|
||||
// Upper strate floor and lower strate ceiling (differ when there's a bedrock gap).
|
||||
const float UpperTopZ = Upper.LegacyTopWorldZVoxels;
|
||||
const float UpperBottomZ = Upper.LegacyBottomWorldZVoxels;
|
||||
const float LowerTopZ = Lower.LegacyTopWorldZVoxels;
|
||||
const float UpperMax = Upper.LegacyHeightVoxels * 0.9f;
|
||||
const float LowerMax = Lower.LegacyHeightVoxels * 0.9f;
|
||||
const float UpperTopZ = (float)(Upper.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
const float UpperBottomZ = (float)(Upper.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float LowerTopZ = (float)(Lower.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
const float UpperMax = (float)Upper.HeightInChunks * CHUNK_SIZE * 0.9f;
|
||||
const float LowerMax = (float)Lower.HeightInChunks * CHUNK_SIZE * 0.9f;
|
||||
|
||||
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||
@@ -2043,7 +1930,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
LowerDef->FloatingIslandParams,
|
||||
LowerQuerySeed,
|
||||
LowerTopZ,
|
||||
Lower.LegacyBottomWorldZVoxels,
|
||||
(float)(Lower.BottomChunkZ) * CHUNK_SIZE,
|
||||
PX,
|
||||
PY,
|
||||
MaxLateralSnap,
|
||||
@@ -2103,7 +1990,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
if (bAimedLowerAtPlayerFitPoint)
|
||||
{
|
||||
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
|
||||
const float LowerBottomZ = Lower.LegacyBottomWorldZVoxels;
|
||||
const float LowerBottomZ = (float)(Lower.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
|
||||
const float InnerTopZ = LowerTopZ - LowerSealThickness;
|
||||
if (VoxelMath::IsFinite(SuggestedLowerPoint.X)
|
||||
@@ -2271,7 +2158,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
Cfg.MouthRadius,
|
||||
LowerDoorDirection,
|
||||
LowerTopZ,
|
||||
Lower.LegacyBottomWorldZVoxels,
|
||||
(float)(Lower.BottomChunkZ) * CHUNK_SIZE,
|
||||
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
|
||||
bAimedLowerAtPlayerFitPoint);
|
||||
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
||||
@@ -2696,8 +2583,8 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
|
||||
{
|
||||
const FStrateSlot& Top = StrateLayout[0];
|
||||
const float TopZ = Top.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Top.LegacyBottomWorldZVoxels;
|
||||
const float TopZ = (float)(Top.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
const float BottomZ = (float)Top.BottomChunkZ * CHUNK_SIZE;
|
||||
const float TopSeal = Top.Definition
|
||||
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
|
||||
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
|
||||
@@ -2720,7 +2607,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
// the top seal into the finite landing room; lower strate seals remain untouched.
|
||||
Entry.Radius = FMath::Max(OriginSpineRadius,
|
||||
VoxelPassageGeometry::MetersToVoxels(0.25f));
|
||||
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + StrateLayoutChunkWidthVoxels);
|
||||
Entry.UpperPoint = FVector(0.0f, 0.0f, TopZ + CHUNK_SIZE);
|
||||
// The capsule's lower tangent meets the room ceiling; it never bores through the
|
||||
// room's floor or creates an origin column in strate 0.
|
||||
Entry.LowerPoint = FVector(
|
||||
@@ -2784,7 +2671,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
||||
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||
|
||||
// The shortlist is rebuilt once for a (manager, version, chunk) and is shared by the tube,
|
||||
// landing, and floor paths. No source-fit stencil or topology search is allowed below it.
|
||||
@@ -2930,7 +2817,7 @@ void UVoxelStrateManager::ApplyPassageModifier(
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
||||
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||
const FVoxelPassage* PassageData = Passages.GetData();
|
||||
const FVector Position(WorldX, WorldY, WorldZ);
|
||||
@@ -3023,7 +2910,7 @@ void UVoxelStrateManager::ApplyPassageLandingAir(
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
||||
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||
if (Nearby.Num() == 0) return;
|
||||
|
||||
@@ -3134,7 +3021,7 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC(
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
||||
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||
const FVoxelPassage* PassageData = Passages.GetData();
|
||||
const FVector Position(WorldX, WorldY, WorldZ);
|
||||
@@ -3230,7 +3117,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
||||
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||
if (Nearby.Num() == 0) return;
|
||||
|
||||
@@ -3357,7 +3244,7 @@ void UVoxelStrateManager::ApplyPassageNativeFloorMC(
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
||||
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||
if (Nearby.Num() == 0
|
||||
|| VoxelPassageGeometry::VerticalShaftConnectorAirMarker())
|
||||
@@ -3548,7 +3435,7 @@ void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldZ / StrateLayoutChunkWidthVoxels));
|
||||
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||
const FVoxelPassage* PassageData = Passages.GetData();
|
||||
const FVector Position(WorldX, WorldY, WorldZ);
|
||||
@@ -6022,8 +5909,8 @@ bool UVoxelStrateManager::AnyLandingFloorAtLattice(
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||
@@ -6065,8 +5952,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearBox(
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
|
||||
VoxelBox, TopZ, BottomZ,
|
||||
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
||||
@@ -6086,8 +5973,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearBox(
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
|
||||
VoxelBox, TopZ, BottomZ,
|
||||
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
||||
@@ -6111,8 +5998,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearLattice(
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
|
||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius,
|
||||
@@ -6149,8 +6036,8 @@ bool UVoxelStrateManager::AnyOriginLandingAirNearLattice(
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||
@@ -6226,8 +6113,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
|
||||
@@ -6244,36 +6131,15 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
||||
|
||||
int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
||||
{
|
||||
// Legacy chunk callers use the projected interval cached during Initialize. The source bounds
|
||||
// are the slot's physical micrometre interval; these indices do not define or stack strates.
|
||||
for (int32 i = 0; i < StrateLayout.Num(); ++i)
|
||||
// Linear search through strate layout.
|
||||
// With ~10-20 strates this is fine. If we ever have hundreds,
|
||||
// switch to binary search (layout is sorted by Z).
|
||||
for (int32 i = 0; i < StrateLayout.Num(); i++)
|
||||
{
|
||||
const FStrateSlot& Slot = StrateLayout[i];
|
||||
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)
|
||||
if (ChunkZ <= Slot.TopChunkZ && ChunkZ >= Slot.BottomChunkZ)
|
||||
{
|
||||
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 i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
@@ -6281,8 +6147,9 @@ int32 UVoxelStrateManager::FindSlotIndexForWorldZCentimetres(double WorldZCentim
|
||||
|
||||
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
||||
{
|
||||
if (!FMath::IsFinite(WorldZ)) return nullptr;
|
||||
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||
// Convert world Z to chunk Z coordinate
|
||||
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||
if (SlotIdx >= 0)
|
||||
{
|
||||
return StrateLayout[SlotIdx].Definition;
|
||||
@@ -6292,8 +6159,8 @@ UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
||||
|
||||
int32 UVoxelStrateManager::GetStrateIndex(float WorldZ) const
|
||||
{
|
||||
if (!FMath::IsFinite(WorldZ)) return -1;
|
||||
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||
if (SlotIdx >= 0)
|
||||
{
|
||||
return StrateLayout[SlotIdx].StrateIndex;
|
||||
@@ -6406,13 +6273,9 @@ bool UVoxelStrateManager::IsGapChunk(const FIntVector& ChunkCoord) const
|
||||
if (StrateLayout.Num() == 0) return false;
|
||||
|
||||
// Above the top strate or below the bottom strate = open air, NOT a gap.
|
||||
const int64 ChunkCenterMicrometres = static_cast<int64>(ChunkCoord.Z)
|
||||
* StrateLayoutChunkSizeMicrometres + StrateLayoutChunkSizeMicrometres / 2;
|
||||
if (ChunkCenterMicrometres >= StrateLayout[0].TopWorldMicrometres
|
||||
|| ChunkCenterMicrometres < StrateLayout.Last().BottomWorldMicrometres)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int32 StackTop = StrateLayout[0].TopChunkZ;
|
||||
const int32 StackBottom = StrateLayout.Last().BottomChunkZ;
|
||||
if (ChunkCoord.Z > StackTop || ChunkCoord.Z < StackBottom) return false;
|
||||
|
||||
// Inside the stack's Z span but not in any strate slot → it's a bedrock gap.
|
||||
return FindSlotIndexForChunkZ(ChunkCoord.Z) < 0;
|
||||
@@ -6455,9 +6318,9 @@ FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVecto
|
||||
|
||||
// Fill in the runtime Z bounds (voxel coordinates, same convention as
|
||||
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
||||
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*StrateLayoutChunkWidthVoxels.
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*CHUNK_SIZE.
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
|
||||
return Result;
|
||||
}
|
||||
@@ -6490,8 +6353,8 @@ FMazeGenerationParams UVoxelStrateManager::GetMazeParamsForChunk(const FIntVecto
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -6517,8 +6380,8 @@ FSurfaceGenerationParams UVoxelStrateManager::GetSurfaceParamsForChunk(const FIn
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -6544,8 +6407,8 @@ FVerticalShaftParams UVoxelStrateManager::GetVerticalShaftParamsForChunk(const F
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -6571,8 +6434,8 @@ FFloatingIslandParams UVoxelStrateManager::GetFloatingIslandParamsForChunk(const
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -6606,14 +6469,14 @@ FBiomeContext UVoxelStrateManager::GetBiomeContextForChunk(const FIntVector& Chu
|
||||
|
||||
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
||||
{
|
||||
if (!FMath::IsFinite(WorldZ)) return false;
|
||||
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||
const int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / CHUNK_SIZE);
|
||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||
if (SlotIdx < 0) return false;
|
||||
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||
// Return the resolved physical bounds in Unreal centimeters.
|
||||
OutTopZ = static_cast<float>(static_cast<double>(Slot.TopWorldMicrometres) / 10000.0);
|
||||
OutBottomZ = static_cast<float>(static_cast<double>(Slot.BottomWorldMicrometres) / 10000.0);
|
||||
// Voxel-space Z bounds → Unreal units. Ceiling = top chunk's upper edge.
|
||||
OutTopZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE * VOXEL_SIZE;
|
||||
OutBottomZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE * VOXEL_SIZE;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6626,8 +6489,8 @@ FStrateDisturbanceParams UVoxelStrateManager::GetDisturbanceParamsForChunk(const
|
||||
}
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||
FStrateDisturbanceParams Result = Slot.Definition->Disturbances;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -6659,8 +6522,8 @@ float UVoxelStrateManager::GetWaterLevelWorldZForChunk(const FIntVector& ChunkCo
|
||||
}
|
||||
if (Rel <= 0.0f) return -FLT_MAX;
|
||||
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float TopZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
return FMath::Lerp(BottomZ, TopZ, FMath::Clamp(Rel, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
@@ -6704,8 +6567,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
FindComposerOverride(Slot.StrateIndex))
|
||||
{
|
||||
FStrateGenerationParams Result = Override->Params.TunnelNetworkParams;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||
// 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.
|
||||
return ResolveFinalParams(Result);
|
||||
@@ -6715,8 +6578,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
||||
{
|
||||
FStrateGenerationParams Result = SeasonStrates[SlotIdx].Params.TunnelNetworkParams;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||
// Season vectors are already final, measured slot records. Vertical structural posts own
|
||||
// their boundary; blending them with another selected recipe would describe neither one.
|
||||
return ResolveFinalParams(Result);
|
||||
@@ -6733,17 +6596,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
//
|
||||
// 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
|
||||
BaseParams.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
BaseParams.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
|
||||
// Core currently dispatches one generator field per resolved strate. Gradient and
|
||||
// interleaved metadata therefore use the explicitly declared Hard fallback for every
|
||||
// legacy parameter lookup as well; mixing parameter vectors here would disagree with the
|
||||
// shared ceiling/content/bedrock composition layer.
|
||||
if (bHardTransitionFallbackForAssembly)
|
||||
{
|
||||
return ResolveFinalParams(BaseParams);
|
||||
}
|
||||
BaseParams.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
BaseParams.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE;
|
||||
|
||||
//=========================================================================
|
||||
// BOUNDARY BLENDING (transition-type aware)
|
||||
@@ -6781,23 +6635,18 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
//---------------------------------------------------------------------
|
||||
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
|
||||
//---------------------------------------------------------------------
|
||||
// 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.
|
||||
// DistFromBottom = how many chunks above the bottom edge of this strate.
|
||||
// When 0, we're right at the boundary. When == TransitionBlendChunks, we're at
|
||||
// the outer edge of the transition zone.
|
||||
const int64 DistFromBottomMicrometres = static_cast<int64>(ChunkCoord.Z)
|
||||
* StrateLayoutChunkSizeMicrometres - Slot.BottomWorldMicrometres;
|
||||
const int32 DistFromBottom = static_cast<int32>(VF_FloorDivide64(
|
||||
DistFromBottomMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||
int32 DistFromBottom = ChunkCoord.Z - Slot.BottomChunkZ;
|
||||
|
||||
if (SlotIdx + 1 < StrateLayout.Num())
|
||||
{
|
||||
// The upper strate (this one) controls the transition type at its lower edge
|
||||
const EVoxelStrateTransition TransType = Slot.ResolvedTransitionType;
|
||||
const EVoxelStrateTransition TransType = Slot.Definition->TransitionType;
|
||||
|
||||
// Per-definition blend distance.
|
||||
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
|
||||
Slot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||
const int32 EffectiveBlend = Slot.Definition->TransitionBlendChunks;
|
||||
|
||||
// Prepare the neighbor's params (only used for Gradient and Interleaved)
|
||||
const FStrateSlot& BelowSlot = StrateLayout[SlotIdx + 1];
|
||||
@@ -6821,8 +6670,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
if (EffectiveBlend > 0 && DistFromBottom < EffectiveBlend)
|
||||
{
|
||||
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
|
||||
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
|
||||
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||
|
||||
// Linear alpha: 0 at EffectiveBlend chunks away, 1 at the boundary
|
||||
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
|
||||
@@ -6859,8 +6708,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
if (DistFromBottom < CheckRange)
|
||||
{
|
||||
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
|
||||
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
|
||||
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||
|
||||
// Sample 3D Perlin noise to warp the boundary position.
|
||||
// XY frequency 0.15 gives medium-scale variation (~6-7 chunks per cycle).
|
||||
@@ -6898,18 +6747,14 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
//---------------------------------------------------------------------
|
||||
// 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].
|
||||
const int64 DistFromTopMicrometres = Slot.TopWorldMicrometres
|
||||
- (static_cast<int64>(ChunkCoord.Z) + 1) * StrateLayoutChunkSizeMicrometres;
|
||||
const int32 DistFromTop = static_cast<int32>(VF_FloorDivide64(
|
||||
DistFromTopMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||
int32 DistFromTop = Slot.TopChunkZ - ChunkCoord.Z;
|
||||
|
||||
if (SlotIdx > 0)
|
||||
{
|
||||
// The strate ABOVE controls the transition at its lower edge (= our upper edge)
|
||||
const FStrateSlot& AboveSlot = StrateLayout[SlotIdx - 1];
|
||||
const EVoxelStrateTransition TransType = AboveSlot.ResolvedTransitionType;
|
||||
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
|
||||
AboveSlot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||
const EVoxelStrateTransition TransType = AboveSlot.Definition->TransitionType;
|
||||
const int32 EffectiveBlend = AboveSlot.Definition->TransitionBlendChunks;
|
||||
|
||||
switch (TransType)
|
||||
{
|
||||
@@ -6924,8 +6769,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
if (EffectiveBlend > 0 && DistFromTop < EffectiveBlend)
|
||||
{
|
||||
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
|
||||
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
|
||||
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||
|
||||
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
|
||||
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
||||
@@ -6946,8 +6791,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
if (DistFromTop < CheckRange)
|
||||
{
|
||||
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
|
||||
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
|
||||
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * CHUNK_SIZE;
|
||||
|
||||
// Same noise function but with a different seed offset to avoid
|
||||
// symmetry between top and bottom boundaries of adjacent strates
|
||||
|
||||
@@ -82,17 +82,6 @@
|
||||
#include <atomic>
|
||||
#include <unordered_set>
|
||||
|
||||
static FORCEINLINE int32 VF_ClipLevelScale(int32 Level)
|
||||
{
|
||||
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
|
||||
return static_cast<int32>(uint32{1} << SafeLevel);
|
||||
}
|
||||
static FORCEINLINE int32 VF_ClipLevelExtentVoxels(int32 Level)
|
||||
{
|
||||
return static_cast<int32>(static_cast<uint32>(CHUNK_SIZE)
|
||||
* static_cast<uint32>(VF_ClipLevelScale(Level)));
|
||||
}
|
||||
|
||||
static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile,
|
||||
const FVector& BoundsMin, const FVector& BoundsMax);
|
||||
static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B);
|
||||
@@ -157,10 +146,6 @@ public:
|
||||
FVoxelForgeCoreTaskScheduler Scheduler;
|
||||
std::unordered_set<VoxelForgeCore::TileKey, VoxelForgeCore::TileKeyHash> RetainedTiles;
|
||||
FCriticalSection RetainedTilesLock;
|
||||
FCriticalSection ShapeReadinessLock;
|
||||
std::unordered_set<VoxelForgeCore::TileKey, VoxelForgeCore::TileKeyHash> ShapeFreeTiles;
|
||||
std::unordered_set<VoxelForgeCore::TileKey, VoxelForgeCore::TileKeyHash> ShapeDependentTiles;
|
||||
std::atomic<bool> bSnapshotReady{false};
|
||||
std::atomic<bool> bReady{false};
|
||||
std::atomic<bool> bInitialFailed{false};
|
||||
std::atomic<bool> bRegionCompileInFlight{false};
|
||||
@@ -238,11 +223,11 @@ namespace
|
||||
GVoxelForgeStreamingResultBacklogCap,
|
||||
TEXT("Maximum queued results plus admitted tile tasks; 0 disables the safety cap. This is separate from the worker-slot cap."));
|
||||
|
||||
int32 GVoxelForgeStreamingReadyQueueDepth = 16;
|
||||
int32 GVoxelForgeStreamingReadyQueueDepth = 8;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingReadyQueueDepth(
|
||||
TEXT("voxel.StreamingReadyQueueDepth"),
|
||||
GVoxelForgeStreamingReadyQueueDepth,
|
||||
TEXT("Extra admitted tile tasks kept ready beyond worker slots, up to four worker-sets/64 tasks; 0 disables the refill queue."));
|
||||
TEXT("Extra admitted tile tasks kept ready beyond the worker slots, up to one worker-set; 0 disables the refill queue."));
|
||||
|
||||
int32 GVoxelForgeStreamingBackgroundWorkerCap = 1;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingBackgroundWorkerCap(
|
||||
@@ -312,8 +297,8 @@ namespace
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingStartupApplyBoost(
|
||||
TEXT("voxel.StreamingStartupApplyBoost"),
|
||||
GVoxelForgeStreamingStartupApplyBoost,
|
||||
TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 24-mesh cap until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
|
||||
constexpr int32 StreamingStartupMaxMeshApplies = 24;
|
||||
TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 12-mesh budget until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
|
||||
constexpr int32 StreamingStartupMaxMeshApplies = 12;
|
||||
constexpr float StreamingStartupApplyBudgetMilliseconds = 14.0f;
|
||||
constexpr double StreamingStartupApplyMaxSeconds = 30.0;
|
||||
|
||||
@@ -2207,7 +2192,7 @@ void AVoxelWorld::BeginPlay()
|
||||
CommandLineStreamingReadyQueueDepth))
|
||||
{
|
||||
GVoxelForgeStreamingReadyQueueDepth = FMath::Clamp(
|
||||
CommandLineStreamingReadyQueueDepth, 0, 64);
|
||||
CommandLineStreamingReadyQueueDepth, 0, 32);
|
||||
}
|
||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingBackgroundWorkerCap="),
|
||||
CommandLineStreamingBackgroundWorkerCap))
|
||||
@@ -2634,8 +2619,7 @@ void AVoxelWorld::BeginPlay()
|
||||
FTaskGraphInterface& TaskGraph = FTaskGraphInterface::Get();
|
||||
LowLevelTasks::FScheduler& TaskScheduler = LowLevelTasks::FScheduler::Get();
|
||||
const int32 QueueDepth = FMath::Clamp(
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0,
|
||||
FMath::Min(64, FMath::Max(1, GetMaxConcurrentStreamingTasks()) * 4));
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, GetMaxConcurrentStreamingTasks());
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("worker_backend_config"), FString::Printf(
|
||||
TEXT("\"logical_cores\":%d,\"effective_tile_slots\":%d,"
|
||||
"\"task_api\":\"UE::Tasks::Launch\",\"task_scheduler_workers\":%u,"
|
||||
@@ -6252,19 +6236,17 @@ static void VF_AppendTouchingAncestors(
|
||||
const FVoxelTileKey& Tile, int32 AncestorLevel, CandidateArray& OutCandidates)
|
||||
{
|
||||
if (AncestorLevel <= Tile.Level) { return; }
|
||||
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
|
||||
const auto FloorDiv = [Divisor](int64 Value)
|
||||
const int32 Divisor = 1 << (AncestorLevel - Tile.Level);
|
||||
const auto FloorDiv = [Divisor](int32 Value)
|
||||
{
|
||||
const int64 Quotient = Value / Divisor;
|
||||
const int64 Remainder = Value % Divisor;
|
||||
return static_cast<int32>(Remainder < 0 ? Quotient - 1 : Quotient);
|
||||
return Value >= 0 ? Value / Divisor : -(((-Value) + Divisor - 1) / Divisor);
|
||||
};
|
||||
const auto AxisCandidates = [&FloorDiv, Divisor](int32 Value)
|
||||
{
|
||||
TArray<int32, TInlineAllocator<2>> Candidates;
|
||||
int32 First = FloorDiv(Value);
|
||||
if ((Value % Divisor) == 0) { --First; }
|
||||
const int32 Last = FloorDiv(static_cast<int64>(Value) + 1);
|
||||
const int32 Last = FloorDiv(Value + 1);
|
||||
for (int32 Coord = First; Coord <= Last; ++Coord) { Candidates.Add(Coord); }
|
||||
return Candidates;
|
||||
};
|
||||
@@ -6352,7 +6334,7 @@ void AVoxelWorld::UpdateVisualStalenessAudit()
|
||||
const FVector& WorldPosition, int32 Level, auto& OutCandidates)
|
||||
{
|
||||
const FVector LocalVoxel = WorldToLocalVoxel(WorldPosition);
|
||||
const float TileExtentVoxels = static_cast<float>(VF_ClipLevelExtentVoxels(Level));
|
||||
const float TileExtentVoxels = static_cast<float>(CHUNK_SIZE * (1 << Level));
|
||||
TArray<int32, TInlineAllocator<2>> XCandidates;
|
||||
TArray<int32, TInlineAllocator<2>> YCandidates;
|
||||
TArray<int32, TInlineAllocator<2>> ZCandidates;
|
||||
@@ -7196,7 +7178,7 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
{
|
||||
const FIntVector Min = SteadyLevelMins.FindChecked(Level);
|
||||
const FIntVector Max = SteadyLevelMaxs.FindChecked(Level);
|
||||
const int32 ExtentVoxels = VF_ClipLevelExtentVoxels(Level);
|
||||
const int32 ExtentVoxels = CHUNK_SIZE << Level;
|
||||
const double TileExtentMetres = static_cast<double>(ExtentVoxels) * VOXEL_SIZE / 100.0;
|
||||
const double SpanX = static_cast<double>(Max.X - Min.X + 1) * TileExtentMetres;
|
||||
const double SpanY = static_cast<double>(Max.Y - Min.Y + 1) * TileExtentMetres;
|
||||
@@ -7568,14 +7550,6 @@ bool AVoxelWorld::GetVoxelForgeCoreWorldAssembly(
|
||||
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutContentHash.IsEmpty();
|
||||
}
|
||||
|
||||
bool AVoxelWorld::GetVoxelForgeCoreResolvedLayout(
|
||||
FString& OutCanonicalJson, FString& OutStableIdentity) const
|
||||
{
|
||||
OutCanonicalJson = VoxelForgeCoreResolvedLayoutJson;
|
||||
OutStableIdentity = VoxelForgeCoreResolvedLayoutIdentity;
|
||||
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutStableIdentity.IsEmpty();
|
||||
}
|
||||
|
||||
bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const
|
||||
{
|
||||
OutDensity = 0.0f;
|
||||
@@ -7594,8 +7568,6 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
OutFailureReason.Reset();
|
||||
VoxelForgeCoreWorldAssemblyJson.Reset();
|
||||
VoxelForgeCoreWorldAssemblyHash.Reset();
|
||||
VoxelForgeCoreResolvedLayoutJson.Reset();
|
||||
VoxelForgeCoreResolvedLayoutIdentity.Reset();
|
||||
if (!bUseVoxelForgeCore) return true;
|
||||
if (!Settings || !StrateManager)
|
||||
{
|
||||
@@ -7628,17 +7600,6 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason);
|
||||
return false;
|
||||
}
|
||||
if (const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout = Snapshot->resolvedLayout())
|
||||
{
|
||||
const std::string CanonicalLayout = ResolvedLayout->ToJson();
|
||||
if (CanonicalLayout.empty())
|
||||
{
|
||||
OutFailureReason = TEXT("the resolved Core layout could not be serialized canonically.");
|
||||
return false;
|
||||
}
|
||||
VoxelForgeCoreResolvedLayoutJson = UTF8_TO_TCHAR(CanonicalLayout.c_str());
|
||||
VoxelForgeCoreResolvedLayoutIdentity = UTF8_TO_TCHAR(ResolvedLayout->stableIdentity.c_str());
|
||||
}
|
||||
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
|
||||
{
|
||||
const std::string CanonicalAssembly = Assembly->ToJson();
|
||||
@@ -7698,20 +7659,10 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
};
|
||||
for (const VoxelForgeCore::TileKey& Key : InitialDesired)
|
||||
{
|
||||
const int32 Extent = VF_ClipLevelExtentVoxels(Key.lod);
|
||||
const int64 OriginXWide = static_cast<int64>(Key.coordinate.x) * Extent;
|
||||
const int64 OriginYWide = static_cast<int64>(Key.coordinate.y) * Extent;
|
||||
const int64 OriginZWide = static_cast<int64>(Key.coordinate.z) * Extent;
|
||||
if (OriginXWide < MIN_int32 || OriginYWide < MIN_int32 || OriginZWide < MIN_int32
|
||||
|| OriginXWide + Extent > MAX_int32 || OriginYWide + Extent > MAX_int32
|
||||
|| OriginZWide + Extent > MAX_int32)
|
||||
{
|
||||
OutFailureReason = TEXT("Core initial clipmap exceeds the supported voxel-coordinate range.");
|
||||
return false;
|
||||
}
|
||||
const int32 OriginX = static_cast<int32>(OriginXWide);
|
||||
const int32 OriginY = static_cast<int32>(OriginYWide);
|
||||
const int32 OriginZ = static_cast<int32>(OriginZWide);
|
||||
const int32 Extent = 32 << Key.lod;
|
||||
const int32 OriginX = Key.coordinate.x * Extent;
|
||||
const int32 OriginY = Key.coordinate.y * Extent;
|
||||
const int32 OriginZ = Key.coordinate.z * Extent;
|
||||
IncludeExtent(ClipMin.x, ClipMax.x, OriginX, Extent);
|
||||
IncludeExtent(ClipMin.y, ClipMax.y, OriginY, Extent);
|
||||
IncludeExtent(ClipMin.z, ClipMax.z, OriginZ, Extent);
|
||||
@@ -7738,7 +7689,6 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
Request.includeFarWorld = true;
|
||||
|
||||
VoxelForgeCoreState = State;
|
||||
State->bSnapshotReady.store(true, std::memory_order_release);
|
||||
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
|
||||
UE::Tasks::Launch(TEXT("VoxelForgeCoreCompile"),
|
||||
[this, State, Request, InitialDesired, ActiveCoreClipRadius, ActiveCoreMaxClipLevel,
|
||||
@@ -7758,17 +7708,10 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
|
||||
std::string Error;
|
||||
bool bCompiled = false;
|
||||
{
|
||||
FWriteScopeLock Lock(State->WorldLock);
|
||||
bCompiled = State->World->compile(Request, State->Scheduler, Error);
|
||||
if (bCompiled)
|
||||
{
|
||||
// The initial compile builds into local state and atomically publishes an immutable
|
||||
// shape. Proven native-only tiles may hold shared WorldLock readers during this work;
|
||||
// take the writer only for region retention after publication.
|
||||
// La compilation initiale construit un etat local puis publie une forme immuable.
|
||||
// Les tuiles natives validees gardent le verrou lecteur partage; le verrou ecrivain
|
||||
// ne sert qu'a retenir les regions apres publication.
|
||||
FWriteScopeLock Lock(State->WorldLock);
|
||||
if (State->World)
|
||||
{
|
||||
State->World->retainTileRegions(InitialDesired);
|
||||
FScopeLock RetainedLock(&State->RetainedTilesLock);
|
||||
@@ -7815,15 +7758,7 @@ void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString&
|
||||
}
|
||||
VoxelForgeCoreWorldAssemblyJson.Reset();
|
||||
VoxelForgeCoreWorldAssemblyHash.Reset();
|
||||
VoxelForgeCoreResolvedLayoutJson.Reset();
|
||||
VoxelForgeCoreResolvedLayoutIdentity.Reset();
|
||||
bUseVoxelForgeCore = false;
|
||||
// The Core adapter sets this before validation; legacy generation must not inherit it.
|
||||
// L'adaptateur Core l'active avant validation ; le générateur historique ne doit pas en hériter.
|
||||
if (StrateManager != nullptr)
|
||||
{
|
||||
StrateManager->SetHardTransitionFallbackForAssembly(false);
|
||||
}
|
||||
CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||
if (Mesher != nullptr)
|
||||
{
|
||||
@@ -9961,9 +9896,6 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
Sample.QueueWaitSeconds = WorkerQueueSeconds + ResultQueueSeconds;
|
||||
Sample.WorkerTaskSeconds = WorkerTaskSeconds;
|
||||
Sample.GenerationSeconds = GenerationSeconds;
|
||||
Sample.CoreGenerateSeconds = Result.CoreGenerateSeconds;
|
||||
Sample.MeshCopySeconds = Result.MeshCopySeconds;
|
||||
Sample.StreamBuildSeconds = Result.StreamBuildSeconds;
|
||||
Sample.ClassifySeconds = Result.ClassifySeconds;
|
||||
Sample.MeshSeconds = Result.MeshSeconds;
|
||||
Sample.StreamSeconds = Result.StreamSeconds;
|
||||
@@ -10124,11 +10056,8 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
||||
{
|
||||
BandChunkLo = MeshBandChunkLo;
|
||||
BandChunkHi = MeshBandChunkHi;
|
||||
BandVoxLo = static_cast<int32>(FMath::Clamp<int64>(
|
||||
static_cast<int64>(MeshBandChunkLo) * CHUNK_SIZE, MIN_int32, MAX_int32));
|
||||
BandVoxHi = static_cast<int32>(FMath::Clamp<int64>(
|
||||
(static_cast<int64>(MeshBandChunkHi) + 1) * CHUNK_SIZE - 1,
|
||||
MIN_int32, MAX_int32));
|
||||
BandVoxLo = MeshBandChunkLo * CHUNK_SIZE;
|
||||
BandVoxHi = (MeshBandChunkHi + 1) * CHUNK_SIZE - 1;
|
||||
}
|
||||
|
||||
FChunkResult Result;
|
||||
@@ -10291,7 +10220,7 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
ReadyToHide.Reserve(TileComponents.Num());
|
||||
auto GetAncestorAtLevel = [](const FVoxelTileKey& Tile, int32 AncestorLevel)
|
||||
{
|
||||
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
|
||||
const int32 Divisor = 1 << (AncestorLevel - Tile.Level);
|
||||
return FVoxelTileKey(VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord, Divisor), AncestorLevel);
|
||||
};
|
||||
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& Pair : TileComponents)
|
||||
@@ -10414,10 +10343,7 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
// Integer floor-division (correct for negatives), scalar + vector.
|
||||
static FORCEINLINE int32 VF_FloorDiv(int32 V, int32 D)
|
||||
{
|
||||
if (D <= 0) { return 0; }
|
||||
const int32 Quotient = V / D;
|
||||
const int32 Remainder = V % D;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
return V >= 0 ? (V / D) : -(((-V) + D - 1) / D);
|
||||
}
|
||||
static FORCEINLINE FIntVector VF_FloorDiv(const FIntVector& V, int32 D)
|
||||
{
|
||||
@@ -11497,13 +11423,13 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
|
||||
{
|
||||
VF_OuterShell(Settings, R, MaxLevelBase, MaxLevel, ROuter);
|
||||
}
|
||||
const int32 PowMax = VF_ClipLevelScale(MaxLevel);
|
||||
const int32 PowMax = 1 << MaxLevel;
|
||||
const FIntVector CMax = VF_FloorDiv(Center, PowMax);
|
||||
|
||||
FVector CV;
|
||||
if (bUseVoxelForgeCore)
|
||||
{
|
||||
const double TileExtentVoxels = static_cast<double>(VF_ClipLevelExtentVoxels(Tile.Level));
|
||||
const double TileExtentVoxels = static_cast<double>(CHUNK_SIZE << Tile.Level);
|
||||
CV = FVector(
|
||||
(static_cast<double>(Tile.Coord.X) + 0.5) * TileExtentVoxels,
|
||||
(static_cast<double>(Tile.Coord.Y) + 0.5) * TileExtentVoxels,
|
||||
@@ -11519,9 +11445,9 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
|
||||
VF_FloorDiv(FMath::FloorToInt(CV.Y), SizeMax),
|
||||
VF_FloorDiv(FMath::FloorToInt(CV.Z), SizeMax));
|
||||
|
||||
return FMath::Abs(static_cast<int64>(TMax.X) - CMax.X) <= ROuter
|
||||
&& FMath::Abs(static_cast<int64>(TMax.Y) - CMax.Y) <= ROuter
|
||||
&& FMath::Abs(static_cast<int64>(TMax.Z) - CMax.Z) <= ROuter;
|
||||
return FMath::Abs(TMax.X - CMax.X) <= ROuter
|
||||
&& FMath::Abs(TMax.Y - CMax.Y) <= ROuter
|
||||
&& FMath::Abs(TMax.Z - CMax.Z) <= ROuter;
|
||||
}
|
||||
|
||||
int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
||||
@@ -11874,10 +11800,11 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
}
|
||||
}
|
||||
const int32 MaxTasks = GetMaxConcurrentStreamingTasks();
|
||||
// Keep several bounded worker-sets ready so frame-paced admission can feed the
|
||||
// background lane continuously without moving mesh application onto the game thread.
|
||||
// Keep at most one worker-set of ready work so an admission pass can feed idle slots
|
||||
// without letting far work consume the near reserve. / Garder une file d'un ensemble
|
||||
// de workers pour nourrir les slots sans consommer la reserve proche.
|
||||
const int32 ReadyQueueDepth = FMath::Clamp(
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4));
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
|
||||
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
|
||||
const int32 AuthoredNearTaskReserve = GetNearTaskReserve(MaxTasks);
|
||||
const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition));
|
||||
@@ -12113,7 +12040,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
LevelSummaries.Num(), Leavers.Num(), CollisionOnlyTiles.Num()));
|
||||
for (const FDesiredLevelSummary& Summary : LevelSummaries)
|
||||
{
|
||||
const int32 ExtentVoxels = VF_ClipLevelExtentVoxels(Summary.Level);
|
||||
const int32 ExtentVoxels = CHUNK_SIZE << Summary.Level;
|
||||
const double TileExtentMetres = static_cast<double>(ExtentVoxels) * VOXEL_SIZE / 100.0;
|
||||
const double SpanX = static_cast<double>(Summary.Max.X - Summary.Min.X + 1) * TileExtentMetres;
|
||||
const double SpanY = static_cast<double>(Summary.Max.Y - Summary.Min.Y + 1) * TileExtentMetres;
|
||||
@@ -12164,9 +12091,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
for (const FVoxelTileKey& T : LoadedTiles)
|
||||
{
|
||||
if (T.Level < CutMin) continue;
|
||||
const int64 LevelScale = VF_ClipLevelScale(T.Level);
|
||||
const int64 CLo = static_cast<int64>(T.Coord.Z) * LevelScale;
|
||||
const int64 CHi = (static_cast<int64>(T.Coord.Z) + 1) * LevelScale - 1;
|
||||
const int32 CLo = T.Coord.Z << T.Level;
|
||||
const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1;
|
||||
const bool bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi;
|
||||
const bool bInNew = CLo >= NewLo && CHi <= NewHi;
|
||||
const bool bOutOld = CHi < MeshBandChunkLo || CLo > MeshBandChunkHi;
|
||||
@@ -12386,7 +12312,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
}
|
||||
Heading = Heading.GetSafeNormal();
|
||||
const FVector PlayerLocalCm = WorldToLocalCm(CenterPosition);
|
||||
const double TileSizeCm = static_cast<double>(VF_ClipLevelExtentVoxels(Level))
|
||||
const double TileSizeCm = static_cast<double>(CHUNK_SIZE << Level)
|
||||
* GetActiveVoxelSizeCm();
|
||||
LevelTiles.Sort([&](const FVoxelTileKey& A, const FVoxelTileKey& B)
|
||||
{
|
||||
@@ -12787,7 +12713,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
|
||||
const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); // Core and worker lane clamped. / Core et voie worker plafonnes.
|
||||
const int32 ReadyQueueDepth = FMath::Clamp(
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4));
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
|
||||
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
|
||||
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
|
||||
const int32 NearReserve = EffectiveNearTaskReserve >= 0
|
||||
@@ -12844,7 +12770,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
const int32 FullRes = GVoxelForgeTestFullResClipLevels >= 0
|
||||
? FMath::Clamp(GVoxelForgeTestFullResClipLevels, 1, 9)
|
||||
: (Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2);
|
||||
const int32 Extent = VF_ClipLevelExtentVoxels(Tile.Level);
|
||||
const int32 Extent = CHUNK_SIZE << Tile.Level;
|
||||
|
||||
const int32 Cells = (Tile.Level < FullRes)
|
||||
? CHUNK_SIZE
|
||||
@@ -12909,9 +12835,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
// et mélanges de matériaux qu'avant la coupe) — la tuile ne peut rendre que des artefacts
|
||||
// ⇒ on n'émet RIEN. Résultat vide via ProcessQueue (bookkeeping normal : PendingTiles,
|
||||
// LoadedTiles, epoch) ; jamais figé — le changement de bande re-queue via BandRemeshQueue.
|
||||
const int64 BandHeightVoxels =
|
||||
(static_cast<int64>(BandChunkHi) - BandChunkLo + 1) * CHUNK_SIZE;
|
||||
if (static_cast<int64>(Step) > BandHeightVoxels)
|
||||
if (Step > (BandChunkHi - BandChunkLo + 1) * CHUNK_SIZE)
|
||||
{
|
||||
FChunkResult Empty;
|
||||
Empty.Tile = Tile;
|
||||
@@ -13095,33 +13019,9 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (PendingTiles.Contains(Tile)) return;
|
||||
if (PendingTiles.Contains(Tile) || !IsVoxelForgeCoreReady()) return;
|
||||
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
|
||||
if (!State.IsValid()||State->bInitialFailed.load(std::memory_order_acquire)
|
||||
||State->bRegionCompileFailed.load(std::memory_order_acquire)
|
||||
||State->bShuttingDown.load(std::memory_order_acquire))return;
|
||||
const bool bShapeReady=State->bReady.load(std::memory_order_acquire);
|
||||
if(!bShapeReady)
|
||||
{
|
||||
if(!State->bSnapshotReady.load(std::memory_order_acquire))return;
|
||||
const VoxelForgeCore::TileKey CoreKey{
|
||||
{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level};
|
||||
bool bShapeFree=false;
|
||||
{
|
||||
FScopeLock ReadinessLock(&State->ShapeReadinessLock);
|
||||
if(State->ShapeFreeTiles.contains(CoreKey))bShapeFree=true;
|
||||
else if(State->ShapeDependentTiles.contains(CoreKey))return;
|
||||
else
|
||||
{
|
||||
FReadScopeLock WorldReadLock(State->WorldLock);
|
||||
if(!State->World)return;
|
||||
bShapeFree=State->World->isTileShapeFree(CoreKey);
|
||||
if(bShapeFree)State->ShapeFreeTiles.insert(CoreKey);
|
||||
else State->ShapeDependentTiles.insert(CoreKey);
|
||||
}
|
||||
}
|
||||
if(!bShapeFree)return;
|
||||
}
|
||||
if (!State.IsValid() || State->bRegionCompileFailed.load(std::memory_order_acquire)) return;
|
||||
if (IsStreamingResultBacklogCapped())
|
||||
{
|
||||
StreamingResultBacklogCapStops.fetch_add(1, std::memory_order_relaxed);
|
||||
@@ -13130,7 +13030,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
|
||||
const int32 MaxTasks = GetMaxConcurrentStreamingTasks();
|
||||
const int32 ReadyQueueDepth = FMath::Clamp(
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4));
|
||||
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
|
||||
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
|
||||
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
|
||||
const int32 NearReserve = EffectiveNearTaskReserve >= 0
|
||||
@@ -13238,11 +13138,6 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
Result.WorkPriority = WorkPriority;
|
||||
Result.GenerationStartCycles = FPlatformTime::Cycles64();
|
||||
Result.GenerationOrdinal = NextStreamingGenerationOrdinal.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
#if PLATFORM_LINUX
|
||||
const bool bMeasureWorkerPhases = VoxelForgeStartupTrace::IsActive();
|
||||
#else
|
||||
constexpr bool bMeasureWorkerPhases = false;
|
||||
#endif
|
||||
|
||||
auto EnqueueObsolete = [&]()
|
||||
{
|
||||
@@ -13298,14 +13193,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
FReadScopeLock Lock(State->WorldLock);
|
||||
if (State->World)
|
||||
{
|
||||
const uint64 CoreGenerateStartCycles = bMeasureWorkerPhases
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
CoreMesh = State->World->generateTile(CoreKey, Options, CoreError);
|
||||
if (CoreGenerateStartCycles != 0)
|
||||
{
|
||||
Result.CoreGenerateSeconds = FPlatformTime::ToSeconds64(
|
||||
FPlatformTime::Cycles64() - CoreGenerateStartCycles);
|
||||
}
|
||||
}
|
||||
}
|
||||
Result.GenerationEndCycles = FPlatformTime::Cycles64();
|
||||
@@ -13347,8 +13235,6 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
|
||||
if (!Result.bEmpty)
|
||||
{
|
||||
const uint64 MeshCopyStartCycles = bMeasureWorkerPhases
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
FVoxelMeshData MeshData;
|
||||
MeshData.Vertices.Reserve(Result.NumVertices);
|
||||
MeshData.Normals.Reserve(Result.NumVertices);
|
||||
@@ -13369,20 +13255,8 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
MeshData.NumSurfaceVertices = static_cast<int32>(CoreMesh->groups.surfaceVertexCount);
|
||||
MeshData.NumCeilingTriangles = Result.NumCeilingTriangles;
|
||||
MeshData.NumSkirtTriangles = Result.NumSkirtTriangles;
|
||||
if (MeshCopyStartCycles != 0)
|
||||
{
|
||||
Result.MeshCopySeconds = FPlatformTime::ToSeconds64(
|
||||
FPlatformTime::Cycles64() - MeshCopyStartCycles);
|
||||
}
|
||||
const uint64 StreamBuildStartCycles = bMeasureWorkerPhases
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
|
||||
BuildTileStreamSet(*Result.Streams, MeshData);
|
||||
if (StreamBuildStartCycles != 0)
|
||||
{
|
||||
Result.StreamBuildSeconds = FPlatformTime::ToSeconds64(
|
||||
FPlatformTime::Cycles64() - StreamBuildStartCycles);
|
||||
}
|
||||
}
|
||||
Result.GenerationEndCycles = FPlatformTime::Cycles64();
|
||||
if (Cancellation->bObsolete.load(std::memory_order_acquire)
|
||||
@@ -14072,8 +13946,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
// Convertir les bornes physiques de tuile vers les strates owner, y compris les rapports fractionnaires.
|
||||
const double OwnerChunksPerCoreTile = static_cast<double>(GetActiveVoxelSizeCm())
|
||||
/ VOXEL_SIZE;
|
||||
const double TileExtentCoreVoxels =
|
||||
static_cast<double>(VF_ClipLevelExtentVoxels(Tile.Level));
|
||||
const double TileExtentCoreVoxels = static_cast<double>(CHUNK_SIZE << Tile.Level);
|
||||
const int32 MinOwnerChunkX = FMath::FloorToInt(
|
||||
Tile.Coord.X * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE);
|
||||
const int32 MinOwnerChunkY = FMath::FloorToInt(
|
||||
@@ -15169,7 +15042,6 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
}
|
||||
|
||||
FString AssemblyHash = VoxelForgeCoreWorldAssemblyHash;
|
||||
FString ResolvedLayoutIdentity = VoxelForgeCoreResolvedLayoutIdentity;
|
||||
if (AssemblyHash.IsEmpty())
|
||||
{
|
||||
FString AssemblyJson;
|
||||
@@ -15206,17 +15078,12 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
AssemblyHash = FString::Printf(TEXT("%016llx"),
|
||||
static_cast<unsigned long long>(Assembly->contentHash));
|
||||
}
|
||||
if (const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout = Snapshot->resolvedLayout())
|
||||
{
|
||||
ResolvedLayoutIdentity = UTF8_TO_TCHAR(ResolvedLayout->stableIdentity.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
|
||||
const uint32 CoreVoxelSizeMicrons = static_cast<uint32>(
|
||||
FMath::RoundToInt(GetActiveVoxelSizeCm() * 10000.0f));
|
||||
if (AssemblyHash.IsEmpty() && ResolvedLayoutIdentity.IsEmpty()
|
||||
&& !bHybridMesher && CoreVoxelSizeMicrons == 250000)
|
||||
if (AssemblyHash.IsEmpty() && !bHybridMesher && CoreVoxelSizeMicrons == 250000)
|
||||
{
|
||||
return SeasonHash;
|
||||
}
|
||||
@@ -15241,11 +15108,6 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
Payload += TEXT("|world_assembly=");
|
||||
Payload += AssemblyHash;
|
||||
}
|
||||
if (!ResolvedLayoutIdentity.IsEmpty())
|
||||
{
|
||||
Payload += TEXT("|resolved_layout=");
|
||||
Payload += ResolvedLayoutIdentity;
|
||||
}
|
||||
if (CoreVoxelSizeMicrons != 250000)
|
||||
{
|
||||
Payload += FString::Printf(TEXT("|core_voxel_size_microns=%u"), CoreVoxelSizeMicrons);
|
||||
@@ -15276,7 +15138,7 @@ void AVoxelWorld::RemeshDirtyChunks(const TArray<FIntVector>& DirtyCoords, const
|
||||
// BackgroundHigh; the old component remains visible until its post-edit replacement is applied.
|
||||
auto TileCoversDirtyCoord = [&DirtyCoords](const FVoxelTileKey& Tile) -> bool
|
||||
{
|
||||
const int32 Span = VF_ClipLevelScale(Tile.Level);
|
||||
const int32 Span = 1 << Tile.Level;
|
||||
const FIntVector MinChunk = Tile.Coord * Span;
|
||||
const FIntVector MaxChunk = MinChunk + FIntVector(Span, Span, Span);
|
||||
for (const FIntVector& Coord : DirtyCoords)
|
||||
|
||||
@@ -261,12 +261,8 @@ namespace VoxelHash
|
||||
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
|
||||
{
|
||||
uint32 H = Seed;
|
||||
// Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for
|
||||
// positive cells and makes the world hash compiler-dependent.
|
||||
const uint32 BiasedX = static_cast<uint32>(CellX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedY = static_cast<uint32>(CellY) + 0x7FFFFFFFu;
|
||||
H ^= Mix(BiasedX);
|
||||
H ^= Mix(BiasedY * 2654435761u);
|
||||
H ^= Mix((uint32)(CellX + 0x7FFFFFFF));
|
||||
H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u);
|
||||
return Mix(H);
|
||||
}
|
||||
|
||||
@@ -278,14 +274,10 @@ namespace VoxelHash
|
||||
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
|
||||
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
|
||||
uint32 H = Seed ^ 0xDEADBEEF;
|
||||
const uint32 BiasedMinX = static_cast<uint32>(MinX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedMinY = static_cast<uint32>(MinY) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedMaxX = static_cast<uint32>(MaxX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedMaxY = static_cast<uint32>(MaxY) + 0x7FFFFFFFu;
|
||||
H ^= Mix(BiasedMinX);
|
||||
H ^= Mix(BiasedMinY * 2654435761u);
|
||||
H ^= Mix(BiasedMaxX * 374761393u);
|
||||
H ^= Mix(BiasedMaxY * 668265263u);
|
||||
H ^= Mix((uint32)(MinX + 0x7FFFFFFF));
|
||||
H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u);
|
||||
H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u);
|
||||
H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u);
|
||||
return Mix(H);
|
||||
}
|
||||
|
||||
@@ -485,13 +477,10 @@ namespace VoxelNoise
|
||||
|
||||
// Hash the neighbor cell to get a feature point position [0,1)
|
||||
// Using three different hash mixes for X, Y, Z offsets
|
||||
const uint32 BiasedX = static_cast<uint32>(NX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedY = static_cast<uint32>(NY) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedZ = static_cast<uint32>(NZ) + 0x7FFFFFFFu;
|
||||
uint32 H = VoxelHash::Mix(
|
||||
BiasedX
|
||||
^ VoxelHash::Mix(BiasedY * 2654435761u)
|
||||
^ VoxelHash::Mix(BiasedZ * 374761393u)
|
||||
(uint32)(NX + 0x7FFFFFFF)
|
||||
^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u)
|
||||
^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u)
|
||||
);
|
||||
|
||||
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
|
||||
|
||||
@@ -340,14 +340,6 @@ private:
|
||||
float SuppressRadiusWorld = 0.0f;
|
||||
};
|
||||
|
||||
// Game-thread lookup copied from the immutable resolved Core layout.
|
||||
// Recherche game-thread copiee depuis la disposition Core resolue et immuable.
|
||||
struct FResolvedCoreGeometryAnchor
|
||||
{
|
||||
int32 StrateIndex = INDEX_NONE;
|
||||
FVector LocalPositionCm = FVector::ZeroVector;
|
||||
};
|
||||
|
||||
/** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle
|
||||
* (cheap, O(1)/column); other archetypes ray-march the density column. No UObject access except
|
||||
* Generator (thread-safe). Determinism-critical. Resolves the dominant biome PER COLUMN
|
||||
@@ -444,7 +436,6 @@ private:
|
||||
// (kept until the cell leaves range so the surface-find isn't repeated). Strate-bounded.
|
||||
// Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint.
|
||||
TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
|
||||
TMap<FString, FResolvedCoreGeometryAnchor> CoreGeometryAnchors;
|
||||
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
|
||||
|
||||
// Set in BeginDestroy; worker tasks check it before touching us.
|
||||
|
||||
@@ -41,27 +41,21 @@
|
||||
// SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée.
|
||||
// C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la
|
||||
// raison pour laquelle cette méthode existe.
|
||||
FORCEINLINE void VF_ApplyUpperBoundarySealValue(float& Density, float WorldZ,
|
||||
float StrateTopZ, float Thickness, float BaseDensity)
|
||||
{
|
||||
const float DistTop = StrateTopZ - WorldZ;
|
||||
if (DistTop >= 0.0f && DistTop < Thickness)
|
||||
{
|
||||
float SealFactor = 1.0f - (DistTop / Thickness);
|
||||
SealFactor = SmoothStep01(SealFactor);
|
||||
Density = FMath::Max(Density, SealFactor * BaseDensity);
|
||||
}
|
||||
}
|
||||
|
||||
FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
||||
float StrateTopZ, float StrateBottomZ,
|
||||
float Thickness, float BaseDensity)
|
||||
{
|
||||
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
||||
|
||||
const float DistTop = StrateTopZ - WorldZ; // + si on est sous le plafond
|
||||
const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol
|
||||
|
||||
VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity);
|
||||
if (DistTop >= 0.0f && DistTop < Thickness)
|
||||
{
|
||||
float SealFactor = 1.0f - (DistTop / Thickness);
|
||||
SealFactor = SmoothStep01(SealFactor);
|
||||
Density = FMath::Max(Density, SealFactor * BaseDensity);
|
||||
}
|
||||
if (DistBot >= 0.0f && DistBot < Thickness)
|
||||
{
|
||||
float SealFactor = 1.0f - (DistBot / Thickness);
|
||||
@@ -70,16 +64,6 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
|
||||
}
|
||||
}
|
||||
|
||||
// The same structural seal, limited to a strate's upper boundary. Native finite fields use this
|
||||
// when their lower boundary intentionally has no seal; keeping the math here makes that ceiling
|
||||
// use the same shared density primitive as every two-sided strate seal.
|
||||
FORCEINLINE void VF_ApplyUpperBoundarySeal(float& Density, float WorldZ,
|
||||
float StrateTopZ, float Thickness, float BaseDensity)
|
||||
{
|
||||
if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return;
|
||||
VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// CARVE DE PASSAGE / PASSAGE CARVING
|
||||
//=============================================================================
|
||||
|
||||
@@ -21,8 +21,6 @@ class UVoxelSettings : public UPrimaryDataAsset
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
virtual void PostLoad() override;
|
||||
|
||||
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
|
||||
int32 GetEffectiveWorldSeed() const;
|
||||
@@ -413,23 +411,14 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
|
||||
int32 TotalStrates = 10;
|
||||
|
||||
// Legacy count retained so pre-metre assets still deserialize during the upgrade.
|
||||
// Ancien compte conserve pour lire les assets precedant les metres.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (DeprecatedProperty, DeprecationMessage = "Use InterStrateGapMeters."))
|
||||
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock.
|
||||
// 0 = strates touch (just the seals between them). Higher = a thick rock layer
|
||||
// between each biome that the player must dig through at (0,0), and that the
|
||||
// auto-carved passages tunnel across.
|
||||
// 0 → adjacent layers (default) · 1-2 → a real bedrock band · 4+ → deep separation
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "0", ClampMax = "16"))
|
||||
int32 InterStrateGapChunks = 0;
|
||||
|
||||
// Physical separation between strata; the layout remains aligned to whole layout chunks.
|
||||
// Separation physique; la disposition reste alignee sur des chunks entiers.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (Units = "m", ClampMin = "0", ClampMax = "1024"))
|
||||
double InterStrateGapMeters = 0.0;
|
||||
|
||||
// Physical width of one vertical strate-layout chunk. This is independent of the
|
||||
// Core tile width (32^3 voxels) and of the source generator's voxel resolution.
|
||||
// 8 m preserves the owner's current SurfaceWorld extent at 25 cm Core voxels.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates|World Assembly",
|
||||
meta = (ClampMin = "0.25", ClampMax = "64.0", Units = "m"))
|
||||
float StrateLayoutChunkSizeMeters = 8.0f;
|
||||
|
||||
//=========================================================================
|
||||
// SPINE & INTER-STRATE CONNECTIONS (the (0,0) descent axis)
|
||||
//=========================================================================
|
||||
|
||||
@@ -35,9 +35,6 @@ namespace VoxelForgeStartupTrace
|
||||
double ResultQueueSeconds = 0.0;
|
||||
double WorkerTaskSeconds = 0.0;
|
||||
double GenerationSeconds = 0.0;
|
||||
double CoreGenerateSeconds = 0.0;
|
||||
double MeshCopySeconds = 0.0;
|
||||
double StreamBuildSeconds = 0.0;
|
||||
double ClassifySeconds = 0.0;
|
||||
double MeshSeconds = 0.0;
|
||||
double StreamSeconds = 0.0;
|
||||
|
||||
@@ -48,7 +48,6 @@ class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
virtual void PostLoad() override;
|
||||
|
||||
//=========================================================================
|
||||
@@ -67,14 +66,14 @@ public:
|
||||
// DIMENSIONS
|
||||
//=========================================================================
|
||||
|
||||
// Legacy serialized chunk count kept for Blueprint/package compatibility during the unit migration.
|
||||
// Ancien nombre de chunks conserve pour la compatibilite Blueprint/package pendant la migration.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (DeprecatedProperty, DeprecationMessage = "Use StrateHeightMeters."))
|
||||
// How many chunks tall this strate is.
|
||||
// Each strate can have a different height!
|
||||
// Human-scale defaults use 16 chunks = 64 m. This gives a 20 m+ vertical budget for
|
||||
// cathedral rooms, tall shafts, and the surface sky cap without changing the lateral gate.
|
||||
// 1 chunk = 32 voxels x 12.5cm = 4 m; 16 chunks preserves the 64 m authored default.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "256"))
|
||||
int32 StrateHeightInChunks = 16;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (Units = "m", ClampMin = "1", ClampMax = "2048"))
|
||||
double StrateHeightMeters = 128.0;
|
||||
|
||||
//=========================================================================
|
||||
// BOUNDARY TRANSITION
|
||||
//=========================================================================
|
||||
@@ -100,13 +99,9 @@ public:
|
||||
// 2 → moderate (good default)
|
||||
// 4 → wide, gentle blend across many chunks
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
|
||||
meta = (DeprecatedProperty, DeprecationMessage = "Use TransitionWidthMeters."))
|
||||
meta = (ClampMin = "1", ClampMax = "8", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
|
||||
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
|
||||
//=========================================================================
|
||||
@@ -120,11 +115,6 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
|
||||
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
|
||||
|
||||
// Optional native Core authoring; leaving it empty keeps the established generator path.
|
||||
// Edition Core native facultative; laissez vide pour garder le generateur existant.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Core Geometry")
|
||||
FVoxelForgeCoreGeometry CoreGeometry;
|
||||
|
||||
/**
|
||||
* Authoritative per-strate worm switch. Existing assets default to enabled. The manager
|
||||
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
|
||||
|
||||
@@ -121,8 +121,8 @@ struct FVoxelPassage
|
||||
* FStrateSlot — Runtime info for one strate in the layout.
|
||||
*
|
||||
* Created during Initialize() and stored in the StrateLayout array.
|
||||
* Each slot owns its resolved half-open physical Z interval. Chunk coordinates and legacy
|
||||
* authoring values below are projections kept for existing callers and assets.
|
||||
* Each slot knows its definition, its Z-range in chunk coordinates,
|
||||
* and its index in the sequence.
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FStrateSlot
|
||||
@@ -136,33 +136,6 @@ struct FStrateSlot
|
||||
// Strate index (0 = topmost, increases downward)
|
||||
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.
|
||||
// Example: TopZ = 0, BottomZ = -3 means this strate spans chunks Z=0 to Z=-3
|
||||
int32 TopChunkZ = 0;
|
||||
@@ -271,17 +244,6 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Strate")
|
||||
int32 GetStrateIndex(float WorldZ) const;
|
||||
|
||||
// Director-resolved physical strate chunk width expressed in owner voxel units.
|
||||
// This is independent of Core's 32^3 tile width.
|
||||
float GetStrateLayoutChunkWidthVoxels() const { return StrateLayoutChunkWidthVoxels; }
|
||||
uint32 GetStrateLayoutChunkSizeMicrometres() const { return StrateLayoutChunkSizeMicrometres; }
|
||||
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; }
|
||||
|
||||
/** Layout/passage generation counter (bumped by every Initialize → GeneratePassages).
|
||||
* Hot-path callers key thread_local memos on this so an editor rebuild (RebuildStrates /
|
||||
* live edit) can never serve a stale strate index or passage shortlist. */
|
||||
@@ -559,15 +521,9 @@ protected:
|
||||
// Copied from VoxelSettings::OriginSpineRadius.
|
||||
float OriginSpineRadius = 28.0f;
|
||||
|
||||
// Compatibility gap projection, in layout chunks. Physical authority is the micrometre value
|
||||
// resolved during Initialize below.
|
||||
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
|
||||
// VoxelSettings::InterStrateGapChunks during Initialize().
|
||||
int32 InterStrateGapChunks = 0;
|
||||
int64 ResolvedInterStrateGapMicrometres = 0;
|
||||
|
||||
// Resolved once from VoxelSettings::StrateLayoutChunkSizeMeters during Initialize().
|
||||
float StrateLayoutChunkWidthVoxels = 32.0f;
|
||||
uint32 StrateLayoutChunkSizeMicrometres = 8000000;
|
||||
mutable bool bHardTransitionFallbackForAssembly = false;
|
||||
|
||||
// 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
|
||||
@@ -584,7 +540,6 @@ protected:
|
||||
// Find which slot a chunk Z coordinate falls into.
|
||||
// Returns INDEX into StrateLayout, or -1 if not found.
|
||||
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
||||
int32 FindSlotIndexForWorldZCentimetres(double WorldZCentimetres) const;
|
||||
|
||||
#if WITH_EDITOR
|
||||
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
|
||||
|
||||
@@ -200,222 +200,16 @@ enum class ECaveGeneratorType : uint8
|
||||
Underwater UMETA(DisplayName = "Underwater (flooded cavern system)"),
|
||||
};
|
||||
|
||||
// Optional native-Core recipe selected independently for one strate.
|
||||
// Recette Core native facultative, choisie independamment pour chaque strate.
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelForgeCoreGeneratorRecipe : uint8
|
||||
{
|
||||
None UMETA(DisplayName = "Off (legacy recipe)"),
|
||||
NativeCaves UMETA(DisplayName = "Native caves"),
|
||||
NativeLattice UMETA(DisplayName = "Native lattice"),
|
||||
ProfiledHeightfield UMETA(DisplayName = "Profiled heightfield")
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelForgeCoreHeightfieldProfile : uint8
|
||||
{
|
||||
PeriodicHills,
|
||||
MirroredRelief,
|
||||
FlatField
|
||||
};
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelForgeCoreStampKind : uint8
|
||||
{
|
||||
CarveEllipsoid,
|
||||
CarveArch,
|
||||
FillPillar,
|
||||
FillDais,
|
||||
FillMonolith,
|
||||
FillPlatform
|
||||
};
|
||||
|
||||
// Stable authored volume stamp. All distances and positions are metres at the asset boundary.
|
||||
// Volume stable, defini par son auteur. Toutes les positions et distances sont en metres.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreStampDefinition
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
EVoxelForgeCoreStampKind Kind = EVoxelForgeCoreStampKind::CarveEllipsoid;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector AnchorPositionMeters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double PlacementRadiusMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
||||
FVector HalfExtentsMeters = FVector(2.0, 2.0, 2.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double SmoothWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityAmplitudeMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityWavelengthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double RoundingRadiusMeters = 0.5;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double MinimumSpacingMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (ClampMin = "0", ClampMax = "3"))
|
||||
uint8 YawQuarterTurns = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
bool bAttachToFloor = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
double FloorMeters = 0.0;
|
||||
};
|
||||
|
||||
// Authored cavern chamber, in local world metres.
|
||||
// Salle cavernicole authored, en metres locaux du monde.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreRoomDefinition
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector CenterMeters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
||||
FVector HalfExtentsMeters = FVector(8.0, 8.0, 4.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
double FloorMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double SmoothWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double UnionWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityAmplitudeMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityWavelengthMeters = 0.0;
|
||||
};
|
||||
|
||||
// Authored cubic tunnel between a stable start anchor and a stable room name.
|
||||
// Tunnel cubique authored entre une ancre stable et une salle nommee.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreTunnelDefinition
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString StartAnchorStableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
FString EndRoomStableName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint0Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint1Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint2Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
|
||||
FVector ControlPoint3Meters = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
|
||||
double BoreRadiusMeters = 2.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "1"))
|
||||
double BoreHeightMeters = 4.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double FloorSupportDepthMeters = 0.5;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double SmoothWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double UnionWidthMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityAmplitudeMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
|
||||
double IrregularityWavelengthMeters = 0.0;
|
||||
|
||||
// Lattice tunnels require an explicit bounded polyline; cave tunnels use the cubic curve.
|
||||
// Les tunnels en treillis exigent une polyligne bornee; les tunnels caves utilisent la courbe cubique.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVector> PolylineMeters;
|
||||
};
|
||||
|
||||
// Optional Core authored recipe. Empty arrays and None preserve the prior asset output.
|
||||
// Recette Core authored facultative. None et les tableaux vides conservent la sortie historique.
|
||||
USTRUCT(BlueprintType)
|
||||
struct VOXELFORGE_API FVoxelForgeCoreGeometry
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
EVoxelForgeCoreGeneratorRecipe GeneratorRecipe = EVoxelForgeCoreGeneratorRecipe::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
EVoxelForgeCoreHeightfieldProfile HeightfieldProfile = EVoxelForgeCoreHeightfieldProfile::PeriodicHills;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
|
||||
double HeightfieldBaseMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
|
||||
FVector2D HeightfieldAmplitudeMeters = FVector2D(20.0, 20.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
|
||||
FVector2D HeightfieldWavelengthMeters = FVector2D(256.0, 256.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (ClampMin = "0.01", ClampMax = "4.0"))
|
||||
double MirrorLambda = 1.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
|
||||
double MinimumArenaClearanceMeters = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVoxelForgeCoreStampDefinition> Stamps;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVoxelForgeCoreRoomDefinition> Rooms;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
|
||||
TArray<FVoxelForgeCoreTunnelDefinition> Tunnels;
|
||||
};
|
||||
|
||||
/**
|
||||
* EVoxelStrateTransition — How two adjacent strates meet at their shared boundary.
|
||||
* EVoxelStrateTransition — How two adjacent strates blend at their shared boundary.
|
||||
*
|
||||
* Each strate boundary can use a different transition style. The transition type
|
||||
* is defined on the UPPER strate's definition and controls the boundary between
|
||||
* itself and the strate below it.
|
||||
*
|
||||
* In the Core composition path, TransitionBlendChunks defines the full permitted
|
||||
* crossing depth between the upper bedrock and lower ceiling bands. Each band keeps
|
||||
* its authored thickness and the solid bands always overlap or touch. The legacy manager
|
||||
* may also blend generation parameters for Gradient; Core does not blend unrelated fields.
|
||||
*
|
||||
* Gradient: A low-amplitude smooth height field moves the shared interface and both
|
||||
* content-facing band surfaces. Best for gentle geological variation.
|
||||
* Gradient: Smooth linear interpolation of generation params across TransitionBlendChunks.
|
||||
* This is the classic approach — no seam visible, but also no dramatic
|
||||
* geological feature at the boundary. Best for similar-looking strates.
|
||||
*
|
||||
* Hard: No blending at all. The strate's params apply right up to the boundary,
|
||||
* then instantly switch to the neighbor's params. The abrupt change in
|
||||
@@ -423,21 +217,26 @@ struct VOXELFORGE_API FVoxelForgeCoreGeometry
|
||||
* discontinuity at the boundary. Best for strates that should feel like
|
||||
* distinct geological layers with a visible dividing line.
|
||||
*
|
||||
* Interleaved: A deterministic 3D domain-warped hash field sets the overlap depth per XY
|
||||
* column from zero to the declared width, creating irregular fingers.
|
||||
* Best for strates that should feel grown together.
|
||||
* Interleaved: The boundary position is warped by a 3D noise field, so "fingers"
|
||||
* of one strate reach into the other. Some XY columns transition early,
|
||||
* others late, creating an irregular, interlocking boundary. The warp
|
||||
* amplitude is ~2 chunks, and Z frequency is lower than XY to produce
|
||||
* horizontal finger-like intrusions. Best for strates that should feel
|
||||
* like they grew into each other over geological time.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelStrateTransition : uint8
|
||||
{
|
||||
// Smoothly undulates the shared bedrock/ceiling interface in the Core composition path.
|
||||
// Smooth linear interpolation of all generation params across the blend zone.
|
||||
// No visible boundary — params change gradually over TransitionBlendChunks distance.
|
||||
Gradient UMETA(DisplayName = "Gradient (smooth blend)"),
|
||||
|
||||
// No blending — params switch instantly at the boundary.
|
||||
// Creates natural cliffs, ledges, or material discontinuities.
|
||||
Hard UMETA(DisplayName = "Hard (sharp boundary with cliff)"),
|
||||
|
||||
// A 3D domain-warped hash field creates finger-like intrusions within the declared width.
|
||||
// 3D noise warps the boundary position per XY column.
|
||||
// Creates finger-like intrusions of one strate into the other.
|
||||
Interleaved UMETA(DisplayName = "Interleaved (domain-warped fingers)")
|
||||
};
|
||||
|
||||
@@ -2053,6 +1852,8 @@ struct VOXELFORGE_API FStrateDisturbanceParams
|
||||
UENUM(BlueprintType)
|
||||
enum class EVoxelPassageStyle : uint8
|
||||
{
|
||||
// “Straight” is retained as the authored style name for the base connection, but its runtime
|
||||
// geometry is a gentle two-leg switchback rather than a vertical shaft.
|
||||
Straight UMETA(DisplayName = "Straight (walkable sloped tunnel)"),
|
||||
Worm UMETA(DisplayName = "Worm (organic meander around the axis)"),
|
||||
Spiral UMETA(DisplayName = "Spiral (corkscrew descent)"),
|
||||
@@ -2077,12 +1878,14 @@ struct VOXELFORGE_API FStratePassageConfig
|
||||
|
||||
// Tunnel shape.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage")
|
||||
EVoxelPassageStyle Style = EVoxelPassageStyle::Worm;
|
||||
EVoxelPassageStyle Style = EVoxelPassageStyle::Straight;
|
||||
|
||||
// ----- WIDTH (tapers along the length) -----
|
||||
// Radius at the two mouths (entry/exit) and at the middle. Equal = uniform tube;
|
||||
// Core raises both radii to at least 12 owner voxels (1.5 m) for capsule clearance.
|
||||
// The passage landing remains wider for turning.
|
||||
// The walkable default is 1.5x the original comfortable profile: 2.25 m diameter at mouths
|
||||
// and 1.875 m at the waist when the stored radii remain 9/7.5 voxels at 12.5 cm per voxel.
|
||||
// Double these asset values to retain the previous 4.5/3.75 m dimensions. The §6.5 landing
|
||||
// room remains wider for turning.
|
||||
// Mouth > Mid = chambers at the ends with a squeeze between; Mid > Mouth = a bulge.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Width", meta = (ClampMin = "1.0"))
|
||||
float MouthRadius = 9.0f;
|
||||
@@ -2109,8 +1912,8 @@ struct VOXELFORGE_API FStratePassageConfig
|
||||
float DistanceMax = 192.0f;
|
||||
|
||||
// ----- SHAPE detail -----
|
||||
// Max sideways variation from the winding path's axis (voxels). Core keeps the route inside
|
||||
// DistanceMax and adds enough distance to meet the walkable grade.
|
||||
// Exotic styles only: max sideways excursion from the axis (voxels). 24 = 6 m; 0 = straight
|
||||
// even in Worm style. The default walkable switchback does not use this wander value.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0"))
|
||||
float Wander = 24.0f;
|
||||
|
||||
@@ -2123,8 +1926,8 @@ struct VOXELFORGE_API FStratePassageConfig
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Shape", meta = (ClampMin = "0.0"))
|
||||
float VerticalWobble = 0.0f;
|
||||
|
||||
// Spiral style: helix radius (voxels) and minimum number of turns. Core adds turns when
|
||||
// needed to meet the walkable grade. The default stored radius is 3 m at the legacy scale.
|
||||
// Spiral style: helix radius (voxels) and number of full turns over the descent. The default
|
||||
// radius is 6 m.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Passage|Spiral", meta = (ClampMin = "1.0"))
|
||||
float SpiralRadius = 24.0f;
|
||||
|
||||
@@ -2491,11 +2294,6 @@ struct VOXELFORGE_API FStrateLandmark
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
|
||||
ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice;
|
||||
|
||||
// Optional stable Core feature binding; an empty value keeps lattice/mouth placement unchanged.
|
||||
// Liaison facultative vers une ancre Core stable; vide, le placement existant reste identique.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Core Geometry")
|
||||
FString CoreGeometryStableName;
|
||||
|
||||
// ----- Hash lattice (AnchorMode == HashLattice — scattered placement; cheap at any radius) -----
|
||||
|
||||
// Average spacing between instances, IN CHUNKS = the lattice cell size (one candidate per cell, so cost
|
||||
|
||||
@@ -332,33 +332,16 @@ struct FVoxelTileKey
|
||||
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
|
||||
|
||||
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
|
||||
int32 StepVoxels() const
|
||||
{
|
||||
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));
|
||||
}
|
||||
int32 StepVoxels() const { return 1 << Level; }
|
||||
int32 ExtentVoxels() const { return CHUNK_SIZE << Level; }
|
||||
|
||||
// Min-corner of the tile in VOXEL coords (what the mesher takes).
|
||||
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));
|
||||
}
|
||||
FIntVector OriginVoxels() const { return Coord * (CHUNK_SIZE << Level); }
|
||||
|
||||
// Tile centre in world cm (for distance sorting / LOD selection).
|
||||
FVector CenterCm() const
|
||||
{
|
||||
const double Ext = static_cast<double>(ExtentVoxels()) * static_cast<double>(VOXEL_SIZE);
|
||||
const double Ext = (double)(CHUNK_SIZE << Level) * (double)VOXEL_SIZE;
|
||||
return FVector((Coord.X + 0.5) * Ext, (Coord.Y + 0.5) * Ext, (Coord.Z + 0.5) * Ext);
|
||||
}
|
||||
|
||||
|
||||
@@ -145,10 +145,6 @@ struct FChunkResult
|
||||
// per-tile profiling remains opt-in.
|
||||
uint64 GenerationStartCycles = 0;
|
||||
uint64 GenerationEndCycles = 0;
|
||||
// Optional phase timings recorded only while the bounded startup trace is active.
|
||||
double CoreGenerateSeconds = 0.0;
|
||||
double MeshCopySeconds = 0.0;
|
||||
double StreamBuildSeconds = 0.0;
|
||||
uint64 ApplyStartCycles = 0;
|
||||
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
|
||||
// Ordinals make admitted order and actual worker start order auditable across MPSC results.
|
||||
@@ -618,7 +614,6 @@ public:
|
||||
FVector GetVoxelForgeCoreDefaultCenterWorld() const;
|
||||
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
|
||||
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
|
||||
bool GetVoxelForgeCoreResolvedLayout(FString& OutCanonicalJson, FString& OutStableIdentity) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
|
||||
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
||||
@@ -783,8 +778,6 @@ private:
|
||||
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
|
||||
FString VoxelForgeCoreWorldAssemblyJson;
|
||||
FString VoxelForgeCoreWorldAssemblyHash;
|
||||
FString VoxelForgeCoreResolvedLayoutJson;
|
||||
FString VoxelForgeCoreResolvedLayoutIdentity;
|
||||
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||
bool bCoreRegionCompilePending = false;
|
||||
TArray<FVoxelTileKey> PendingCoreRegionTiles;
|
||||
|
||||
@@ -7,13 +7,6 @@ public class VoxelForge : ModuleRules
|
||||
{
|
||||
public VoxelForge(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
if (Target.Platform == UnrealTargetPlatform.Linux)
|
||||
{
|
||||
// Test translation units have independent anonymous-namespace helpers that collide in
|
||||
// Linux Unity batches. Keep Linux compilation one source per TU; Windows remains unchanged.
|
||||
bUseUnity = false;
|
||||
}
|
||||
|
||||
// PCH = Precompiled Headers - speeds up compilation
|
||||
// UseExplicitOrSharedPCHs is the modern recommended setting
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
@@ -39,13 +39,6 @@ public class VoxelForgeCore : ModuleRules
|
||||
|
||||
PublicDependencyModuleNames.Add("Core");
|
||||
CppStandard = CppStandardVersion.Cpp20;
|
||||
if (Target.Platform == UnrealTargetPlatform.Linux)
|
||||
{
|
||||
// The compatibility include directory contains Unreal-shaped placeholders. On Linux,
|
||||
// let generated module TUs pass those names through to UE while Core shims opt in.
|
||||
PrivateDefinitions.Add("VFCORE_COMPAT_FORWARD_HEADERS=1");
|
||||
MinCpuArchX64 = MinimumCpuArchitectureX64.AVX2;
|
||||
}
|
||||
// Canonical Standalone code predates UE's stricter warning defaults; keep this narrow to legal local shadowing.
|
||||
CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off;
|
||||
FPSemantics = FPSemanticsMode.Precise;
|
||||
|
||||
@@ -878,8 +878,6 @@ struct FExploreArguments
|
||||
bool bPassageFloorWritesOverride = false;
|
||||
int32 PassageFloorWrites = 1;
|
||||
bool bStandaloneWorldExport = false;
|
||||
bool bOwnerFourStratePlan = false;
|
||||
bool bSkipStandaloneCoreAdapterParity = false;
|
||||
bool bCoreWorldTest = false;
|
||||
bool bCoreWorldStreamTraceOnly = false;
|
||||
bool bRenderCameraOverride = false;
|
||||
@@ -924,12 +922,6 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
int32 StandaloneWorldExport = 0;
|
||||
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
||||
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
|
||||
int32 OwnerFourStratePlan = 0;
|
||||
FParse::Value(*Params, TEXT("ownerfourstrateplan="), OwnerFourStratePlan);
|
||||
OutArguments.bOwnerFourStratePlan = OwnerFourStratePlan != 0;
|
||||
int32 SkipStandaloneCoreAdapterParity = 0;
|
||||
FParse::Value(*Params, TEXT("skipstandalonecoreadapterparity="), SkipStandaloneCoreAdapterParity);
|
||||
OutArguments.bSkipStandaloneCoreAdapterParity = SkipStandaloneCoreAdapterParity != 0;
|
||||
int32 CoreWorldTest = 0;
|
||||
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
|
||||
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
|
||||
@@ -1274,11 +1266,12 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
return false;
|
||||
}
|
||||
|
||||
// Passage walks need both mouths; a render/export-only generation can inspect an end slot.
|
||||
// Parity exports also accept the game's authored single-strate layout.
|
||||
const bool bRequiresInteriorSlot = OutArguments.bWalk;
|
||||
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot ? 0 : 1;
|
||||
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot
|
||||
// The historical explorer contract needs two adjacent mouths around an interior slot. A
|
||||
// parity export can instead target the game's authored single-strate layout, which has no
|
||||
// such mouths and is valid for export-only mode. Build() still validates the slot against
|
||||
// the loaded settings layout.
|
||||
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig ? 0 : 1;
|
||||
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig
|
||||
? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2;
|
||||
if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot)
|
||||
{
|
||||
@@ -1291,8 +1284,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
else
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("slot must be in [0,%d]; walk mode requires an interior slot in [1,%d] for both passage mouths."),
|
||||
MaxSyntheticStrates - 1, MaxSyntheticStrates - 2);
|
||||
TEXT("slot must be an interior slot in [1,%d] so arrival and departure mouths exist."),
|
||||
MaxSyntheticStrates - 2);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1505,17 +1498,6 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.bOwnerFourStratePlan
|
||||
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
|
||||
{
|
||||
OutError = TEXT("ownerfourstrateplan requires settingsref and must omit strateref.");
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.bSkipStandaloneCoreAdapterParity && !OutArguments.bStandaloneWorldExport)
|
||||
{
|
||||
OutError = TEXT("skipstandalonecoreadapterparity requires standaloneworldexport.");
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.bCoreWorldTest
|
||||
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
|
||||
{
|
||||
@@ -1720,66 +1702,6 @@ struct FExploreMeshBvhNode
|
||||
}
|
||||
};
|
||||
|
||||
bool ApplyOwnerFourStratePlan(UVoxelSettings* Settings,
|
||||
TArray<TStrongObjectPtr<UVoxelStrateDefinition>>& OutDefinitions,
|
||||
bool bDuplicateDefinitions, FString& OutError)
|
||||
{
|
||||
if (Settings == nullptr)
|
||||
{
|
||||
OutError = TEXT("The transient owner plan has no settings object.");
|
||||
return false;
|
||||
}
|
||||
if (!Settings->Season.IsNull())
|
||||
{
|
||||
OutError = TEXT("The transient four-strate owner plan cannot replace an assigned season manifest.");
|
||||
return false;
|
||||
}
|
||||
|
||||
static const TCHAR* AssetPaths[] = {
|
||||
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
|
||||
};
|
||||
Settings->TotalStrates = UE_ARRAY_COUNT(AssetPaths);
|
||||
Settings->StratePool.Reset();
|
||||
Settings->FixedStrates.Reset();
|
||||
OutDefinitions.Reset();
|
||||
OutDefinitions.Reserve(UE_ARRAY_COUNT(AssetPaths));
|
||||
|
||||
for (int32 Index = 0; Index < UE_ARRAY_COUNT(AssetPaths); ++Index)
|
||||
{
|
||||
UVoxelStrateDefinition* AuthoredDefinition = LoadObject<UVoxelStrateDefinition>(
|
||||
nullptr, AssetPaths[Index]);
|
||||
if (AuthoredDefinition == nullptr)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Could not load staged owner strate asset '%s'."), AssetPaths[Index]);
|
||||
return false;
|
||||
}
|
||||
UVoxelStrateDefinition* Definition = AuthoredDefinition;
|
||||
if (bDuplicateDefinitions)
|
||||
{
|
||||
Definition = DuplicateObject<UVoxelStrateDefinition>(
|
||||
AuthoredDefinition, GetTransientPackage(),
|
||||
FName(*FString::Printf(TEXT("VoxelForgeOwnerPlan_Strate_%d"), Index)));
|
||||
if (Definition == nullptr)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Could not make a transient copy of owner strate asset '%s'."), AssetPaths[Index]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Settings->FixedStrates.Add(Index,
|
||||
TSoftObjectPtr<UVoxelStrateDefinition>(Definition));
|
||||
OutDefinitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeOwnerPlan] transient fixed slots=4 assets=DA_Strate1,DA_Strate2,DA_Strate3,DA_Strate4 settings_asset_untouched=1"));
|
||||
return true;
|
||||
}
|
||||
|
||||
struct FExploreWorld
|
||||
{
|
||||
TStrongObjectPtr<UVoxelSettings> Settings;
|
||||
@@ -1912,7 +1834,7 @@ struct FExploreWorld
|
||||
Settings->InterStrateGapChunks, Settings->SkirtCells);
|
||||
}
|
||||
|
||||
const int32 NumStrates = Arguments.bOwnerFourStratePlan ? 4 : bUseAuthoredSettings
|
||||
const int32 NumStrates = bUseAuthoredSettings
|
||||
? (Arguments.bGameRuntimeConfig
|
||||
? FMath::Max(Settings->TotalStrates, 1)
|
||||
: FMath::Max(Settings->TotalStrates, Arguments.Slot + 2))
|
||||
@@ -1942,18 +1864,10 @@ struct FExploreWorld
|
||||
// loaded package remains untouched.
|
||||
Settings->TotalStrates = NumStrates;
|
||||
|
||||
if (Arguments.bOwnerFourStratePlan)
|
||||
{
|
||||
if (!ApplyOwnerFourStratePlan(Settings.Get(), Definitions, true, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (bUseAuthoredStrate)
|
||||
{
|
||||
// Keep the historical single-asset interior probe isolated from loaded packages.
|
||||
// The owner’s snapshot states that every slot uses DA_Strate3. Clone it per slot so
|
||||
// commandlet overrides never mutate a loaded package or leak between slots.
|
||||
Settings->StratePool.Reset();
|
||||
Settings->FixedStrates.Reset();
|
||||
}
|
||||
@@ -2009,7 +1923,6 @@ struct FExploreWorld
|
||||
}
|
||||
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
|
||||
}
|
||||
}
|
||||
|
||||
auto ApplyTargetOverrides = [&Arguments](UVoxelStrateDefinition* Definition) -> bool
|
||||
{
|
||||
@@ -13559,16 +13472,14 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||
|
||||
const std::array<std::uint64_t, 4> Actual = StandaloneCoreAggregateHashes(Rows);
|
||||
const std::array<std::uint64_t, 4> Expected{
|
||||
0xde345030348f2c5dull, 0xcbf0888004618b87ull,
|
||||
0x7a11e28e052d69a8ull, 0x52b88607e8dbcbfdull};
|
||||
0xdb2991c73215af4cull, 0x8236c5edd7fc8e4full,
|
||||
0x4c48fa75a543d58eull, 0xbea22e76b34a4dfcull};
|
||||
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
|
||||
constexpr std::uint64_t ExpectedWalkSamples = 21425;
|
||||
const bool bPassageWalkabilityPassed = Stats.walkSamples == ExpectedWalkSamples
|
||||
&& Stats.walkFailures == 0;
|
||||
if (Rows.size() != 2578 || Actual != Expected || !bPassageWalkabilityPassed)
|
||||
const bool bPassageWalkabilityPassed = Stats.walkSamples == 5642 && Stats.walkFailures == 0;
|
||||
if (Rows.size() != 2226 || Actual != Expected || !bPassageWalkabilityPassed)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected_walk=%llu"),
|
||||
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"),
|
||||
static_cast<unsigned long long>(WorkerLimit),
|
||||
static_cast<unsigned long long>(Rows.size()),
|
||||
static_cast<unsigned long long>(Actual[0]),
|
||||
@@ -13576,13 +13487,12 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||
static_cast<unsigned long long>(Actual[2]),
|
||||
static_cast<unsigned long long>(Actual[3]),
|
||||
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
|
||||
static_cast<unsigned long long>(Stats.walkSamples),
|
||||
static_cast<unsigned long long>(ExpectedWalkSamples));
|
||||
static_cast<unsigned long long>(Stats.walkSamples));
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display,
|
||||
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"),
|
||||
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"),
|
||||
static_cast<unsigned long long>(WorkerLimit),
|
||||
OwnerClipRadius,
|
||||
OwnerMaxLod,
|
||||
@@ -13590,9 +13500,7 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||
static_cast<unsigned long long>(Actual[0]),
|
||||
static_cast<unsigned long long>(Actual[1]),
|
||||
static_cast<unsigned long long>(Actual[2]),
|
||||
static_cast<unsigned long long>(Actual[3]),
|
||||
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
|
||||
static_cast<unsigned long long>(ExpectedWalkSamples));
|
||||
static_cast<unsigned long long>(Actual[3]));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -13607,24 +13515,6 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
*Arguments.SettingsReference);
|
||||
return false;
|
||||
}
|
||||
TStrongObjectPtr<UVoxelSettings> OwnerPlanSettings;
|
||||
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
|
||||
UVoxelSettings* ExportSettings = AuthoredSettings;
|
||||
if (Arguments.bOwnerFourStratePlan)
|
||||
{
|
||||
OwnerPlanSettings = TStrongObjectPtr<UVoxelSettings>(
|
||||
DuplicateObject<UVoxelSettings>(AuthoredSettings, GetTransientPackage()));
|
||||
if (!OwnerPlanSettings.IsValid())
|
||||
{
|
||||
OutError = TEXT("Could not make a transient copy of owner settings for the four-strate plan.");
|
||||
return false;
|
||||
}
|
||||
ExportSettings = OwnerPlanSettings.Get();
|
||||
if (!ApplyOwnerFourStratePlan(ExportSettings, OwnerPlanDefinitions, false, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
UVoxelStrateManager* Manager = NewObject<UVoxelStrateManager>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
int32 Seed = AuthoredSettings->Seed;
|
||||
@@ -13636,7 +13526,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
OriginSpineRadius = Season->GetOriginSpineRadius();
|
||||
WorldRadiusVoxels = Season->GetWorldRadiusVoxels();
|
||||
}
|
||||
if (!Manager->Initialize(ExportSettings, Seed))
|
||||
if (!Manager->Initialize(AuthoredSettings, Seed))
|
||||
{
|
||||
OutError = TEXT("Owner settings did not produce a valid resolved strate layout.");
|
||||
return false;
|
||||
@@ -13644,7 +13534,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
Seed = Manager->GetWorldSeed();
|
||||
UVoxelGenerator* Generator = NewObject<UVoxelGenerator>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient);
|
||||
Generator->InitializeSettings(ExportSettings);
|
||||
Generator->InitializeSettings(AuthoredSettings);
|
||||
Generator->SetStrateManager(Manager);
|
||||
|
||||
TArray<FStandaloneExportSample> Samples;
|
||||
@@ -13778,25 +13668,25 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export"));
|
||||
Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm);
|
||||
Writer->WriteValue(TEXT("seed"), Seed);
|
||||
Writer->WriteValue(TEXT("open_surface_entry"), ExportSettings->bOpenSurfaceEntry);
|
||||
Writer->WriteValue(TEXT("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry);
|
||||
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||
Writer->WriteValue(TEXT("world_radius_voxels"), WorldRadiusVoxels);
|
||||
Writer->WriteValue(TEXT("edge_seal_thickness"), ExportSettings->EdgeSealThickness);
|
||||
Writer->WriteValue(TEXT("edge_seal_thickness"), AuthoredSettings->EdgeSealThickness);
|
||||
Writer->WriteValue(TEXT("total_strates"), Manager->GetNumStrates());
|
||||
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), ExportSettings->InterStrateGapChunks);
|
||||
Writer->WriteValue(TEXT("clip_radius"), ExportSettings->ClipRadius);
|
||||
Writer->WriteValue(TEXT("max_clip_level"), ExportSettings->MaxClipLevel);
|
||||
Writer->WriteValue(TEXT("full_res_clip_levels"), ExportSettings->FullResClipLevels);
|
||||
Writer->WriteValue(TEXT("coarse_tile_cells"), ExportSettings->CoarseTileCells);
|
||||
Writer->WriteValue(TEXT("render_distance_chunks"), ExportSettings->RenderDistanceChunks);
|
||||
Writer->WriteValue(TEXT("generate_skirts"), ExportSettings->bGenerateSkirts);
|
||||
Writer->WriteValue(TEXT("skirt_cells"), ExportSettings->SkirtCells);
|
||||
Writer->WriteValue(TEXT("lod_octave_drop"), ExportSettings->LODOctaveDrop);
|
||||
Writer->WriteValue(TEXT("strate_content_cut_min_level"), ExportSettings->GetEffectiveStrateContentCutMinLevel());
|
||||
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), AuthoredSettings->InterStrateGapChunks);
|
||||
Writer->WriteValue(TEXT("clip_radius"), AuthoredSettings->ClipRadius);
|
||||
Writer->WriteValue(TEXT("max_clip_level"), AuthoredSettings->MaxClipLevel);
|
||||
Writer->WriteValue(TEXT("full_res_clip_levels"), AuthoredSettings->FullResClipLevels);
|
||||
Writer->WriteValue(TEXT("coarse_tile_cells"), AuthoredSettings->CoarseTileCells);
|
||||
Writer->WriteValue(TEXT("render_distance_chunks"), AuthoredSettings->RenderDistanceChunks);
|
||||
Writer->WriteValue(TEXT("generate_skirts"), AuthoredSettings->bGenerateSkirts);
|
||||
Writer->WriteValue(TEXT("skirt_cells"), AuthoredSettings->SkirtCells);
|
||||
Writer->WriteValue(TEXT("lod_octave_drop"), AuthoredSettings->LODOctaveDrop);
|
||||
Writer->WriteValue(TEXT("strate_content_cut_min_level"), AuthoredSettings->GetEffectiveStrateContentCutMinLevel());
|
||||
TSet<const UObject*> ActiveObjects;
|
||||
StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"),
|
||||
StandaloneSerializeStruct(ExportSettings->GetClass(), ExportSettings,
|
||||
ActiveObjects, ExportSettings));
|
||||
StandaloneSerializeStruct(AuthoredSettings->GetClass(), AuthoredSettings,
|
||||
ActiveObjects, AuthoredSettings));
|
||||
|
||||
Writer->WriteArrayStart(TEXT("strates"));
|
||||
for (const FStrateSlot& Slot : Manager->GetLayout())
|
||||
@@ -13866,68 +13756,6 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
Writer->WriteObjectEnd();
|
||||
Writer->Close();
|
||||
|
||||
// Export the exact Core manifest and resolved layout that the Unreal adapter just
|
||||
// generated from these live settings and slots. The standalone research tools consume
|
||||
// these snapshots instead of maintaining a second copy of the planning rules.
|
||||
FString RuntimeSnapshotJson;
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
ExportSettings, Manager, 3, 8, StandaloneCoreVoxelSizeCm,
|
||||
RuntimeSnapshotJson, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FTCHARToUTF8 RuntimeSnapshotUtf8(*RuntimeSnapshotJson);
|
||||
std::string CoreSnapshotError;
|
||||
const std::optional<VoxelForgeCore::ResolvedWorldSnapshot> CoreSnapshot =
|
||||
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
|
||||
std::string_view(RuntimeSnapshotUtf8.Get(),
|
||||
static_cast<std::size_t>(RuntimeSnapshotUtf8.Length())), CoreSnapshotError);
|
||||
if (!CoreSnapshot || CoreSnapshot->worldAssembly() == nullptr
|
||||
|| CoreSnapshot->resolvedLayout() == nullptr)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core adapter export did not produce a complete manifest and resolved layout: %s"),
|
||||
UTF8_TO_TCHAR(CoreSnapshotError.c_str()));
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string CoreAssemblyText = CoreSnapshot->worldAssembly()->ToJson();
|
||||
const std::string CoreLayoutText = CoreSnapshot->resolvedLayout()->ToJson();
|
||||
if (CoreAssemblyText.empty() || CoreLayoutText.empty())
|
||||
{
|
||||
OutError = TEXT("Core adapter export produced an empty canonical manifest or layout.");
|
||||
return false;
|
||||
}
|
||||
const FString CanonicalAssemblyJson = UTF8_TO_TCHAR(CoreAssemblyText.c_str());
|
||||
const FString CanonicalLayoutJson = UTF8_TO_TCHAR(CoreLayoutText.c_str());
|
||||
TSharedPtr<FJsonObject> ExportRoot;
|
||||
const TSharedRef<TJsonReader<>> ExportReader = TJsonReaderFactory<>::Create(Json);
|
||||
if (!FJsonSerializer::Deserialize(ExportReader, ExportRoot) || !ExportRoot.IsValid())
|
||||
{
|
||||
OutError = TEXT("Could not reopen the owner export to attach the Core plan snapshots.");
|
||||
return false;
|
||||
}
|
||||
TSharedPtr<FJsonObject> AssemblyObject;
|
||||
const TSharedRef<TJsonReader<>> AssemblyReader =
|
||||
TJsonReaderFactory<>::Create(CanonicalAssemblyJson);
|
||||
if (!FJsonSerializer::Deserialize(AssemblyReader, AssemblyObject) || !AssemblyObject.IsValid())
|
||||
{
|
||||
OutError = TEXT("Could not parse the Core adapter's canonical manifest for export.");
|
||||
return false;
|
||||
}
|
||||
ExportRoot->SetObjectField(TEXT("world_assembly"), AssemblyObject.ToSharedRef());
|
||||
ExportRoot->SetStringField(TEXT("core_world_assembly_json"), CanonicalAssemblyJson);
|
||||
ExportRoot->SetStringField(TEXT("resolved_layout_json"), CanonicalLayoutJson);
|
||||
FString ExportedJson;
|
||||
const TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> ExportWriter =
|
||||
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&ExportedJson);
|
||||
if (!FJsonSerializer::Serialize(ExportRoot.ToSharedRef(), ExportWriter))
|
||||
{
|
||||
OutError = TEXT("Could not serialize the owner export with its canonical Core plan.");
|
||||
return false;
|
||||
}
|
||||
Json = MoveTemp(ExportedJson);
|
||||
|
||||
const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json"));
|
||||
if (!FFileHelper::SaveStringToFile(
|
||||
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
@@ -13936,16 +13764,11 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
return false;
|
||||
}
|
||||
if (OutJson) *OutJson = Json;
|
||||
if (!Arguments.bSkipStandaloneCoreAdapterParity && !VerifyStandaloneCoreAdapter(Json, OutError))
|
||||
if (!VerifyStandaloneCoreAdapter(Json, OutError))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
|
||||
return false;
|
||||
}
|
||||
if (Arguments.bSkipStandaloneCoreAdapterParity)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreAdapter] skipped legacy W1/W8/W16 fixture parity for this export-only update."));
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
|
||||
Seed, StandaloneCoreVoxelSizeCm, Manager->GetNumStrates(), Manager->GetPassages().Num(),
|
||||
@@ -14985,7 +14808,7 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
||||
UVoxelSettings* Source, int32 Seed,
|
||||
TStrongObjectPtr<UVoxelSettings>& OutSettings,
|
||||
TStrongObjectPtr<UVoxelStrateDefinition>& OutSurfaceDefinition,
|
||||
FString& OutError, bool bCompactTracePlan = false)
|
||||
FString& OutError)
|
||||
{
|
||||
if (Source == nullptr)
|
||||
{
|
||||
@@ -15001,10 +14824,11 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
||||
nullptr, TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"));
|
||||
if (SurfaceDefinition == nullptr
|
||||
|| SurfaceDefinition->GeneratorType != ECaveGeneratorType::SurfaceWorld
|
||||
|| SurfaceDefinition->StrateHeightInChunks <= 0)
|
||||
|| SurfaceDefinition->StrateHeightInChunks <= 0
|
||||
|| Source->TotalStrates != 1)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("The staged owner assets do not contain a usable DA_Strate1 SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."),
|
||||
TEXT("The staged owner assets do not describe the logged single SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."),
|
||||
SurfaceDefinition != nullptr ? 1 : 0,
|
||||
SurfaceDefinition != nullptr ? static_cast<int32>(SurfaceDefinition->GeneratorType) : INDEX_NONE,
|
||||
SurfaceDefinition != nullptr ? SurfaceDefinition->StrateHeightInChunks : INDEX_NONE,
|
||||
@@ -15019,24 +14843,9 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
||||
return false;
|
||||
}
|
||||
OutSettings = TStrongObjectPtr<UVoxelSettings>(SettingsCopy);
|
||||
if (bCompactTracePlan)
|
||||
{
|
||||
SurfaceDefinition = DuplicateObject<UVoxelStrateDefinition>(SurfaceDefinition,
|
||||
GetTransientPackage(), TEXT("VoxelForgeCompactSurfaceTraceDefinition"));
|
||||
if (SurfaceDefinition == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not make a transient DA_Strate1 copy for the compact common-plan trace.");
|
||||
return false;
|
||||
}
|
||||
// L14's historical fixed-length SurfaceWorld route search cannot place the
|
||||
// new trace fixture even at 16 chunks. Use the same simple, walkable FlatPlain
|
||||
// plan in both host revisions so the streaming comparison reaches AVoxelWorld.
|
||||
SurfaceDefinition->GeneratorType = ECaveGeneratorType::FlatPlain;
|
||||
SurfaceDefinition->StrateHeightInChunks = 16;
|
||||
}
|
||||
OutSurfaceDefinition = TStrongObjectPtr<UVoxelStrateDefinition>(SurfaceDefinition);
|
||||
SettingsCopy->Seed = Seed;
|
||||
SettingsCopy->TotalStrates = 1;
|
||||
SettingsCopy->TotalStrates = Source->TotalStrates;
|
||||
SettingsCopy->StratePool.Reset();
|
||||
SettingsCopy->FixedStrates.Reset();
|
||||
SettingsCopy->FixedStrates.Add(0,
|
||||
@@ -15046,10 +14855,8 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
|
||||
SettingsCopy->bUseWorldAssemblyManifestOverride = false;
|
||||
const int32 BottomChunkZ = 1 - SurfaceDefinition->StrateHeightInChunks;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 generator=%d seed=%d strata=1 bounds=[0,%d] height_chunks=%d compact_trace=%d gap=%d imported_override=cleared"),
|
||||
static_cast<int32>(SurfaceDefinition->GeneratorType), Seed, BottomChunkZ,
|
||||
SurfaceDefinition->StrateHeightInChunks,
|
||||
bCompactTracePlan ? 1 : 0, SettingsCopy->InterStrateGapChunks);
|
||||
TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 seed=%d strata=1 bounds=[0,%d] height_chunks=%d gap=%d imported_override=cleared"),
|
||||
Seed, BottomChunkZ, SurfaceDefinition->StrateHeightInChunks, SettingsCopy->InterStrateGapChunks);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16625,15 +16432,7 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
}
|
||||
float StagedWalkSpeedCmPerSecond = 0.0f;
|
||||
float StagedSprintSpeedCmPerSecond = 0.0f;
|
||||
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(
|
||||
if (!ReadStagedPlayerSpeeds(
|
||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
||||
{
|
||||
return false;
|
||||
@@ -17855,15 +17654,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
|
||||
float StagedWalkSpeedCmPerSecond = 0.0f;
|
||||
float StagedSprintSpeedCmPerSecond = 0.0f;
|
||||
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(
|
||||
if (!ReadStagedPlayerSpeeds(
|
||||
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
|
||||
{
|
||||
return false;
|
||||
@@ -17872,26 +17663,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings);
|
||||
TStrongObjectPtr<UVoxelSettings> OwnerSurfaceSettings;
|
||||
TStrongObjectPtr<UVoxelStrateDefinition> OwnerSurfaceDefinition;
|
||||
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
|
||||
if (Arguments.bOwnerFourStratePlan)
|
||||
{
|
||||
UVoxelSettings* PlanSettings = DuplicateObject<UVoxelSettings>(
|
||||
AuthoredSettings, GetTransientPackage(), TEXT("VoxelForgeOwnerFourStrateTraceSettings"));
|
||||
if (PlanSettings == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not duplicate owner settings for the transient four-strate Core trace.");
|
||||
return false;
|
||||
}
|
||||
PlanSettings->Seed = OwnerSeed;
|
||||
if (!ApplyOwnerFourStratePlan(PlanSettings, OwnerPlanDefinitions, true, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OwnerSurfaceSettings = TStrongObjectPtr<UVoxelSettings>(PlanSettings);
|
||||
}
|
||||
else if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed,
|
||||
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError,
|
||||
Arguments.bCoreWorldStreamTraceOnly))
|
||||
if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed,
|
||||
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -18049,25 +17822,6 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
|
||||
return false;
|
||||
}
|
||||
FVector StartupTraceSpawnCm = FVector::ZeroVector;
|
||||
FString StartupTraceSpawnText;
|
||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceSpawnCm="),
|
||||
StartupTraceSpawnText, false))
|
||||
{
|
||||
TArray<FString> Components;
|
||||
StartupTraceSpawnText.ParseIntoArray(Components, TEXT(","), true);
|
||||
if (Components.Num() != 3
|
||||
|| !LexTryParseString(StartupTraceSpawnCm.X, *Components[0])
|
||||
|| !LexTryParseString(StartupTraceSpawnCm.Y, *Components[1])
|
||||
|| !LexTryParseString(StartupTraceSpawnCm.Z, *Components[2])
|
||||
|| !FMath::IsFinite(StartupTraceSpawnCm.X)
|
||||
|| !FMath::IsFinite(StartupTraceSpawnCm.Y)
|
||||
|| !FMath::IsFinite(StartupTraceSpawnCm.Z))
|
||||
{
|
||||
OutError = TEXT("voxel.StartupTraceSpawnCm must be finite X,Y,Z centimetres.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient);
|
||||
if (PawnRoot == nullptr)
|
||||
{
|
||||
@@ -18076,7 +17830,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
}
|
||||
PlayerPawn->SetRootComponent(PawnRoot);
|
||||
PawnRoot->RegisterComponent();
|
||||
PlayerPawn->SetActorLocation(StartupTraceSpawnCm, false, nullptr, ETeleportType::TeleportPhysics);
|
||||
PlayerPawn->SetActorLocation(FVector::ZeroVector, false, nullptr, ETeleportType::TeleportPhysics);
|
||||
PlayerController->Possess(PlayerPawn);
|
||||
if (TestWorld->GetFirstPlayerController() != PlayerController
|
||||
|| PlayerController->GetPawn() != PlayerPawn)
|
||||
@@ -18105,10 +17859,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1);
|
||||
Actor->CoreClipRadius = 3;
|
||||
Actor->CoreMaxClipLevel = 8;
|
||||
// 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->bCaptureVoxelForgeCoreHashesForValidation = true;
|
||||
Actor->FinishSpawning(ActorTransform);
|
||||
TestWorld->InitializeActorsForPlay(FURL());
|
||||
TestWorld->BeginPlay();
|
||||
@@ -18118,11 +17869,6 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
return false;
|
||||
}
|
||||
Actor->SetActorTickEnabled(true);
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("trace_spawn_location"), FString::Printf(
|
||||
TEXT("\"location_cm\":[%s,%s,%s]"),
|
||||
*FString::SanitizeFloat(StartupTraceSpawnCm.X),
|
||||
*FString::SanitizeFloat(StartupTraceSpawnCm.Y),
|
||||
*FString::SanitizeFloat(StartupTraceSpawnCm.Z)));
|
||||
|
||||
auto DesiredTilesLoaded = [](const AVoxelWorld* World)
|
||||
{
|
||||
@@ -18277,7 +18023,6 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
const double CarveBrushRadiusCoreVoxels =
|
||||
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
||||
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
|
||||
FIntVector ColdStartCollisionCenter = FIntVector::ZeroValue;
|
||||
uint64 ExpectedEditDigest = 0;
|
||||
std::array<std::uint64_t, 4> InitialAggregates{};
|
||||
std::array<std::uint64_t, 4> TravelAggregates{};
|
||||
@@ -18463,17 +18208,10 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
BiomeProbeSpanVoxels);
|
||||
|
||||
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
|
||||
// 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);
|
||||
const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({});
|
||||
if (Desired.empty())
|
||||
{
|
||||
OutError = TEXT("Standalone Core returned an empty reference clipmap at the cold-start camera.");
|
||||
OutError = TEXT("Standalone Core returned an empty reference clipmap at the origin.");
|
||||
return false;
|
||||
}
|
||||
ExpectedInitialTileCount = Desired.size();
|
||||
@@ -18520,23 +18258,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
// 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});
|
||||
ReferenceWorld.retainTileRegions(Desired);
|
||||
|
||||
if (Arguments.bCoreHybridCliffOnly)
|
||||
{
|
||||
@@ -18610,47 +18332,6 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
||||
else
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
||||
|
||||
// Keep collision hashes outside ExpectedInitialDesiredSet; the collision cube is an
|
||||
// additional runtime request even where it overlaps the visual clipmap.
|
||||
// Garder les hachages de collision hors de l'ensemble visuel initial, meme s'ils se
|
||||
// recoupent avec le clipmap.
|
||||
std::vector<VoxelForgeCore::TileKey> RetainedReferenceTiles = Desired;
|
||||
RetainedReferenceTiles.insert(RetainedReferenceTiles.end(),
|
||||
ColdStartCollisionTiles.begin(), ColdStartCollisionTiles.end());
|
||||
ReferenceWorld.retainTileRegions(RetainedReferenceTiles);
|
||||
if (!ReferenceWorld.compileTileRegion(ColdStartCollisionTiles, 16, Scheduler, CoreError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Standalone collision reference compile failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
std::size_t CollisionBodyTiles = 0;
|
||||
for (const VoxelForgeCore::TileKey& Key : ColdStartCollisionTiles)
|
||||
{
|
||||
const std::optional<VoxelForgeCore::TileMesh> Mesh =
|
||||
ReferenceWorld.generateTile(Key, Options, CoreError);
|
||||
if (!Mesh)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Standalone collision tile generation failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
FVoxelForgeCoreTileHashRecord Record;
|
||||
Record.bEmpty = Mesh->indices.empty();
|
||||
Record.bHasHashes = Mesh->hasDensityHash && Mesh->hasMeshHash
|
||||
&& Mesh->hasPayloadHash && Mesh->hasTopologyHash;
|
||||
if (!Record.bEmpty) ++CollisionBodyTiles;
|
||||
ExpectedHashes.Add(FVoxelTileKey(
|
||||
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod), Record);
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] standalone_collision_reference=1 center=(%d,%d,%d) tiles=%d body=%llu no_body=%llu"),
|
||||
ColdStartCollisionCenter.X, ColdStartCollisionCenter.Y, ColdStartCollisionCenter.Z,
|
||||
static_cast<int32>(ColdStartCollisionTiles.size()),
|
||||
static_cast<unsigned long long>(CollisionBodyTiles),
|
||||
static_cast<unsigned long long>(ColdStartCollisionTiles.size() - CollisionBodyTiles));
|
||||
|
||||
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
|
||||
AggregateRows.reserve(Desired.size());
|
||||
bool bFoundCarveVertex = false;
|
||||
@@ -19686,7 +19367,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
||||
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
|
||||
{
|
||||
const FVoxelTileKey Key(ColdStartCollisionCenter + FIntVector(X, Y, Z), 0);
|
||||
const FVoxelTileKey Key(FIntVector(X, Y, Z), 0);
|
||||
ExpectedCollisionSet.Add(Key);
|
||||
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
|
||||
if (Record == nullptr)
|
||||
@@ -21680,44 +21361,6 @@ int32 RunBatch(const FString& Params)
|
||||
|
||||
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
{
|
||||
if (FParse::Param(*Params, TEXT("coreassetupgradeaudit")))
|
||||
{
|
||||
// Load the untouched BuildHost copies and emit their in-memory conversions; never save a package.
|
||||
// Charge les copies BuildHost intactes et affiche les conversions en memoire, sans sauver de package.
|
||||
UVoxelSettings* SettingsAsset = LoadObject<UVoxelSettings>(nullptr,
|
||||
TEXT("/Game/VoxelForge/DA_Settings.DA_Settings"));
|
||||
if (SettingsAsset == nullptr)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing DA_Settings copy"));
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_gap_chunks=%d new_gap_m=%.6f layout_chunk_m=%.6f"),
|
||||
*SettingsAsset->GetPathName(), SettingsAsset->InterStrateGapChunks,
|
||||
SettingsAsset->InterStrateGapMeters, SettingsAsset->StrateLayoutChunkSizeMeters);
|
||||
|
||||
const TCHAR* StratePaths[] = {
|
||||
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
|
||||
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
|
||||
};
|
||||
for (const TCHAR* Path : StratePaths)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = LoadObject<UVoxelStrateDefinition>(nullptr, Path);
|
||||
if (Definition == nullptr)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing strate copy %s"), Path);
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_height_chunks=%d new_height_m=%.6f old_transition_chunks=%d new_transition_m=%.6f"),
|
||||
*Definition->GetPathName(), Definition->StrateHeightInChunks,
|
||||
Definition->StrateHeightMeters, Definition->TransitionBlendChunks,
|
||||
Definition->TransitionWidthMeters);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (FParse::Param(*Params, TEXT("step2f")))
|
||||
{
|
||||
return RunStep2f(Params);
|
||||
|
||||
@@ -7,14 +7,10 @@ fi
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$SCRIPT_DIR"
|
||||
PROJECT_ROOT="$(cd -- "$ROOT/.." && pwd)"
|
||||
VERIFY_DIR="$ROOT/Tools/verify"
|
||||
COMPILER="clang++-19"
|
||||
VARIANT="clang19"
|
||||
FP_CONTRACT="off"
|
||||
JOBS=12
|
||||
SANITIZERS=""
|
||||
EXCLUDE_UNSIGNED_OVERFLOW=0
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
@@ -22,8 +18,6 @@ while (($#)); do
|
||||
--variant=*) VARIANT="${1#*=}" ;;
|
||||
--fp-contract=*) FP_CONTRACT="${1#*=}" ;;
|
||||
--jobs=*) JOBS="${1#*=}" ;;
|
||||
--sanitize=*) SANITIZERS="${1#*=}" ;;
|
||||
--exclude-unsigned-integer-overflow) EXCLUDE_UNSIGNED_OVERFLOW=1 ;;
|
||||
*) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
@@ -42,20 +36,10 @@ command -v ar >/dev/null || { printf 'ar not found\n' >&2; exit 2; }
|
||||
command -v pkg-config >/dev/null || { printf 'pkg-config not found\n' >&2; exit 2; }
|
||||
pkg-config --exists egl glesv2 libpng || { printf 'EGL, GLESv2, and libpng development files are required\n' >&2; exit 2; }
|
||||
|
||||
BUILD_ROOT="${VOXELFORGE_BUILD_ROOT:-$ROOT/Build}"
|
||||
BIN_DIR="${VOXELFORGE_BIN_DIR:-$ROOT/bin/linux}"
|
||||
BUILD_DIR="$BUILD_ROOT/linux-$VARIANT"
|
||||
BUILD_DIR="$ROOT/Build/linux-$VARIANT"
|
||||
BIN_DIR="$ROOT/bin/linux"
|
||||
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=(
|
||||
"-I$ROOT/Core/include"
|
||||
"-I$ROOT/Core/src"
|
||||
@@ -64,32 +48,13 @@ INCLUDES=(
|
||||
"-I$ROOT/Source/VoxelForge/Public"
|
||||
"-I$ROOT/Source/VoxelForge/Private"
|
||||
)
|
||||
OPT_LEVEL=-O2
|
||||
LINK_FLAGS=()
|
||||
if [[ -n "$SANITIZERS" ]]; then
|
||||
OPT_LEVEL=-O1
|
||||
LINK_FLAGS+=("-fsanitize=$SANITIZERS" -fno-sanitize-recover=undefined)
|
||||
if ((EXCLUDE_UNSIGNED_OVERFLOW)); then
|
||||
LINK_FLAGS+=(-fno-sanitize=unsigned-integer-overflow)
|
||||
fi
|
||||
fi
|
||||
|
||||
FLAGS=(
|
||||
-std=c++20 "$OPT_LEVEL" -mavx2 -mfma -fno-fast-math
|
||||
-std=c++20 -O2 -mavx2 -mfma -fno-fast-math
|
||||
-fexceptions -pthread "${INCLUDES[@]}"
|
||||
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\""
|
||||
)
|
||||
FLAGS+=("${LINK_FLAGS[@]}")
|
||||
if [[ "$FP_CONTRACT" != "default" ]]; then
|
||||
FLAGS+=("-ffp-contract=$FP_CONTRACT")
|
||||
fi
|
||||
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
|
||||
FLAGS+=(-Wno-c++11-narrowing)
|
||||
else
|
||||
@@ -146,11 +111,6 @@ done < "$ROOT/Core/core_sources.txt"
|
||||
headless_object="$BUILD_DIR/objects/StreamBenchmarkHeadless.o"
|
||||
compile_background "$ROOT/Source/StreamBenchmarkHeadless.cpp" "$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
|
||||
PIDS=()
|
||||
@@ -158,19 +118,11 @@ PIDS=()
|
||||
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
|
||||
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
|
||||
OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT"
|
||||
"$COMPILER" -pthread "${LINK_FLAGS[@]}" "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
|
||||
"$COMPILER" -pthread "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
|
||||
CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT"
|
||||
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
|
||||
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" -I"$ROOT/Source" \
|
||||
"$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \
|
||||
$(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 EGL offscreen capture: %s\n' "$CAPTURE_OUTPUT"
|
||||
printf 'Compiler: %s | variant: %s | fp-contract: %s | sanitizers: %s | jobs: %s\n' \
|
||||
"$COMPILER" "$VARIANT" "$FP_CONTRACT" "${SANITIZERS:-off}" "$JOBS"
|
||||
printf 'Compiler: %s | variant: %s | fp-contract: %s | jobs: %s\n' "$COMPILER" "$VARIANT" "$FP_CONTRACT" "$JOBS"
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <AssetRegistry/AssetData.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <AssetRegistry/AssetRegistryModule.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Async/ParallelFor.h>
|
||||
#endif
|
||||
@@ -1,10 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <CoreMinimal.h>
|
||||
#else
|
||||
|
||||
// Deliberately small UE-style surface used by the copied VoxelForge CPU sources.
|
||||
// This is a value/container compatibility layer; it does not start or link Unreal.
|
||||
#include <algorithm>
|
||||
@@ -50,9 +45,6 @@
|
||||
#define GENERATED_UCLASS_BODY()
|
||||
#define VOXELFORGE_API
|
||||
#define FORCEINLINE inline
|
||||
#ifdef WITH_EDITOR
|
||||
#undef WITH_EDITOR
|
||||
#endif
|
||||
#define WITH_EDITOR 0
|
||||
#define TRACE_CPUPROFILER_EVENT_SCOPE(...)
|
||||
#define PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS
|
||||
@@ -713,4 +705,3 @@ struct FParse
|
||||
struct FCrc { static uint32 MemCrc32(const void*Data,int32 Length,uint32 Crc=0){const auto*P=static_cast<const uint8*>(Data);uint32 V=~Crc;for(int32 I=0;I<Length;++I){V^=P[I];for(int J=0;J<8;++J)V=(V>>1)^(0xEDB88320u&static_cast<uint32>(-static_cast<int32>(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast<int32>(std::strlen(S)),Crc);} };
|
||||
template<class T> using TAtomic = std::atomic<T>;
|
||||
class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomic<int32>Value{0};};
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Dom/JsonObject.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Dom/JsonValue.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Engine/DataAsset.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <GameplayTagContainer.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/CriticalSection.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/Event.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/FileManager.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/IConsoleManager.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/PlatformProcess.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <HAL/PlatformTime.h>
|
||||
#endif
|
||||
@@ -1,10 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Json.h>
|
||||
#else
|
||||
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
@@ -167,4 +162,3 @@ namespace VFJson
|
||||
return Parser(std::move(Text)).Parse(Out, Error);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Math/RandomStream.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/CommandLine.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/Crc.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/FileHelper.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/Parse.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/Paths.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/ScopeLock.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/ScopeRWLock.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Misc/SecureHash.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Modules/ModuleManager.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <ProfilingDebugging/CpuProfilerTrace.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Serialization/CustomVersion.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Serialization/JsonSerializer.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Serialization/JsonWriter.h>
|
||||
#endif
|
||||
@@ -1,9 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Stats/Stats.h>
|
||||
#else
|
||||
#define STATCAT_Advanced 0
|
||||
#define DECLARE_STATS_GROUP(Name,Group,Category) constexpr int32 Group=0
|
||||
#define DECLARE_DWORD_COUNTER_STAT_EXTERN(Name,Stat,Group,Api) constexpr int32 Stat=0
|
||||
@@ -13,4 +8,3 @@
|
||||
#define SET_DWORD_STAT(Stat,Value) ((void)0)
|
||||
#define SCOPE_CYCLE_COUNTER(Stat) ((void)0)
|
||||
#define QUICK_SCOPE_CYCLE_COUNTER(Stat) ((void)0)
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Templates/SubclassOf.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <Templates/UniquePtr.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <UObject/EnumProperty.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <UObject/FieldIterator.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <UObject/Object.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <UObject/UObjectGlobals.h>
|
||||
#endif
|
||||
@@ -1,6 +1 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
|
||||
&& (defined(__clang__) || defined(__GNUC__))
|
||||
#include_next <UObject/UnrealType.h>
|
||||
#endif
|
||||
@@ -153,13 +153,8 @@ radius range, top flattening, taper, bounded contour warp, roughness, blend, con
|
||||
route constraints. `PlanFloatingIslandCluster` emits stable island and connector IDs, a connected
|
||||
cluster graph, conservative influence AABBs, and a centerline route. At least the primary chain is
|
||||
always planned; `islandDensityQ16` controls up to two optional side sites. Connectors carry a sharp-tag
|
||||
ready bit for a later hybrid-mesher adapter. The schema accepts geometry versions 1 through 3.
|
||||
Version 3 keeps the version-2 island form, removes the all-XY lower seal, and obtains its ceiling from
|
||||
the shared upper-strate boundary seal. The resolved upper bound comes from the strate's own chunk
|
||||
range; `boundarySealSubvoxels` sets the shared seal thickness and remains a planning margin for island
|
||||
features. It does not define a FloatingIslands-specific ceiling height. Versions 1 and 2 retain their
|
||||
recorded two-sided boundary seals. Manifests without a geometry version retain version-1 evaluation
|
||||
and canonical hashes.
|
||||
ready bit for a later hybrid-mesher adapter. The schema accepts geometry version 1 and 2. Manifests
|
||||
without a geometry version retain version-1 evaluation and canonical hashes.
|
||||
|
||||
`FloatingIslandFieldV1::densitySubvoxels` evaluates the canonical island and connector field.
|
||||
`interval` returns unknown for boxes overlapping any influence AABB and a certified all-air range
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
#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,253 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "VoxelForgeCore/CpuCycleCounter.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
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:
|
||||
// the same recipe/plan column has the same bits at every sampling step.
|
||||
struct FColumnCacheKey
|
||||
{
|
||||
std::uint64_t recipeIdentity = 0;
|
||||
std::uint64_t recipeContentHash = 0;
|
||||
std::uint64_t planIdentity = 0;
|
||||
std::uint64_t planContentHash = 0;
|
||||
std::int64_t xSubvoxels = 0;
|
||||
std::int64_t ySubvoxels = 0;
|
||||
// Optional exact sub-domain identity for recipes with several independent columns
|
||||
// at the same XY (for example, a floating-island anchor).
|
||||
std::uint64_t contextIdentity = 0;
|
||||
|
||||
std::array<std::uint64_t, 7> words() const noexcept
|
||||
{
|
||||
return {recipeIdentity, recipeContentHash, planIdentity, planContentHash,
|
||||
static_cast<std::uint64_t>(xSubvoxels), static_cast<std::uint64_t>(ySubvoxels),
|
||||
contextIdentity};
|
||||
}
|
||||
};
|
||||
|
||||
// A bounded, world-owned second-level cache. Existing thread-local caches stay first, so
|
||||
// their ordinary hit path remains private and lock-free. Shared hits read atomic words; only
|
||||
// a shared miss takes the set spin lock. Entries are four-way set associative, evict on
|
||||
// collision, and carry pointer identities plus content hashes to reject stale reuse.
|
||||
// Les caches TLS restent le premier niveau; seul un defaut partage prend le verrou du groupe.
|
||||
template <typename TValue, std::size_t SetCount, std::size_t Ways = 4>
|
||||
class TSharedColumnValueCache
|
||||
{
|
||||
static_assert(SetCount > 0 && (SetCount & (SetCount - 1)) == 0,
|
||||
"column cache set count must be a power of two");
|
||||
static_assert(Ways > 0 && Ways <= 16, "unsupported column cache associativity");
|
||||
static_assert(std::is_trivially_copyable_v<TValue>,
|
||||
"shared column values must be trivially copyable");
|
||||
|
||||
static constexpr std::size_t ValueWordCount = (sizeof(TValue) + 7) / 8;
|
||||
using FKeyWords = std::array<std::uint64_t, 7>;
|
||||
using FValueWords = std::array<std::uint64_t, ValueWordCount>;
|
||||
|
||||
struct FEntry
|
||||
{
|
||||
std::atomic<std::uint64_t> Version{0};
|
||||
std::atomic<std::uint64_t> Tag{0};
|
||||
std::array<std::atomic<std::uint64_t>, 7> Key{};
|
||||
std::array<std::atomic<std::uint64_t>, ValueWordCount> Value{};
|
||||
};
|
||||
|
||||
struct FSet
|
||||
{
|
||||
std::atomic_flag Writer = ATOMIC_FLAG_INIT;
|
||||
std::uint8_t NextWay = 0;
|
||||
std::array<FEntry, Ways> Entries{};
|
||||
};
|
||||
|
||||
struct FSpinGuard
|
||||
{
|
||||
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))
|
||||
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); }
|
||||
std::atomic_flag& Flag;
|
||||
FColumnCacheMetrics* Metrics;
|
||||
};
|
||||
|
||||
public:
|
||||
TSharedColumnValueCache() : Sets(std::make_unique<FSet[]>(SetCount)) {}
|
||||
TSharedColumnValueCache(const TSharedColumnValueCache&) = delete;
|
||||
TSharedColumnValueCache& operator=(const TSharedColumnValueCache&) = delete;
|
||||
|
||||
static TSharedColumnValueCache* setThreadActive(TSharedColumnValueCache* value) noexcept
|
||||
{
|
||||
TSharedColumnValueCache* previous = Active;
|
||||
Active = value;
|
||||
return previous;
|
||||
}
|
||||
|
||||
static TSharedColumnValueCache* threadActive() noexcept { return Active; }
|
||||
|
||||
template <typename TFactory>
|
||||
TValue getOrCompute(const FColumnCacheKey& key, TFactory&& factory,
|
||||
bool& computed, FColumnCacheMetrics* metrics = nullptr)
|
||||
{
|
||||
computed = false;
|
||||
const FKeyWords keyWords = key.words();
|
||||
const std::uint64_t tag = Hash(keyWords);
|
||||
const std::size_t setIndex = static_cast<std::size_t>(tag) & (SetCount - 1);
|
||||
FSet& set = Sets[setIndex];
|
||||
TValue 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, metrics,sampleCycles);
|
||||
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)();
|
||||
if(sampleCycles)
|
||||
{
|
||||
metrics->sampledBuildCycles+=AdjustedCpuCycleDelta(buildStart,ReadCpuCycles());
|
||||
++metrics->sampledBuilds;
|
||||
}
|
||||
computed = true;
|
||||
if (metrics) ++metrics->computes;
|
||||
FValueWords valueWords{};
|
||||
std::memcpy(valueWords.data(), &value, sizeof(TValue));
|
||||
|
||||
std::size_t replacement = Ways;
|
||||
for (std::size_t way = 0; way < Ways; ++way)
|
||||
if (set.Entries[way].Version.load(std::memory_order_seq_cst) == 0)
|
||||
{ replacement = way; break; }
|
||||
if (replacement == Ways)
|
||||
{
|
||||
if (metrics) ++metrics->conflictReplacements;
|
||||
replacement = set.NextWay;
|
||||
set.NextWay = static_cast<std::uint8_t>((set.NextWay + 1) % Ways);
|
||||
}
|
||||
FEntry& entry = set.Entries[replacement];
|
||||
std::uint64_t version = entry.Version.load(std::memory_order_seq_cst);
|
||||
if (version & 1u) ++version;
|
||||
entry.Version.store(version + 1, std::memory_order_seq_cst);
|
||||
for (std::size_t word = 0; word < keyWords.size(); ++word)
|
||||
entry.Key[word].store(keyWords[word], std::memory_order_seq_cst);
|
||||
for (std::size_t word = 0; word < valueWords.size(); ++word)
|
||||
entry.Value[word].store(valueWords[word], std::memory_order_seq_cst);
|
||||
entry.Tag.store(tag, std::memory_order_seq_cst);
|
||||
entry.Version.store(version + 2, std::memory_order_seq_cst);
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::uint64_t Hash(const FKeyWords& words) noexcept
|
||||
{
|
||||
std::uint64_t hash = 0x9E3779B185EBCA87ull;
|
||||
for (const std::uint64_t word : words)
|
||||
{
|
||||
hash ^= word + 0x9E3779B97F4A7C15ull + (hash << 6) + (hash >> 2);
|
||||
hash ^= hash >> 30;
|
||||
hash *= 0xBF58476D1CE4E5B9ull;
|
||||
hash ^= hash >> 27;
|
||||
hash *= 0x94D049BB133111EBull;
|
||||
hash ^= hash >> 31;
|
||||
}
|
||||
return hash == 0 ? 1 : hash;
|
||||
}
|
||||
|
||||
static bool TryRead(const FEntry& entry, const FKeyWords& keyWords,
|
||||
std::uint64_t tag, TValue& out)
|
||||
{
|
||||
const std::uint64_t before = entry.Version.load(std::memory_order_seq_cst);
|
||||
if (before == 0 || (before & 1u)
|
||||
|| entry.Tag.load(std::memory_order_seq_cst) != tag) return false;
|
||||
FKeyWords foundKey{};
|
||||
for (std::size_t word = 0; word < foundKey.size(); ++word)
|
||||
foundKey[word] = entry.Key[word].load(std::memory_order_seq_cst);
|
||||
if (foundKey != keyWords) return false;
|
||||
FValueWords foundValue{};
|
||||
for (std::size_t word = 0; word < foundValue.size(); ++word)
|
||||
foundValue[word] = entry.Value[word].load(std::memory_order_seq_cst);
|
||||
const std::uint64_t after = entry.Version.load(std::memory_order_seq_cst);
|
||||
if (before != after || (after & 1u)) return false;
|
||||
std::memcpy(&out, foundValue.data(), sizeof(TValue));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryReadSet(const FSet& set, const FKeyWords& keyWords,
|
||||
std::uint64_t tag, TValue& out)
|
||||
{
|
||||
for (const FEntry& entry : set.Entries)
|
||||
if (TryRead(entry, keyWords, tag, out)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<FSet[]> Sets;
|
||||
inline static thread_local TSharedColumnValueCache* Active = nullptr;
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
// Core implementation TUs opt into the standalone Unreal-shaped compatibility headers. UE's
|
||||
// Linux-generated module TUs leave this unset so those same header names forward to the engine.
|
||||
#define VFCORE_USE_COMPAT 1
|
||||
|
||||
// Every standard or platform header used by the copied implementation must be included before
|
||||
// its wrapper opens vfcore::detail. This prevents system declarations from being nested in the
|
||||
// private namespace when the implementation includes its Unreal-shaped compatibility headers.
|
||||
@@ -59,8 +55,6 @@
|
||||
#include "Json.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
||||
#include "VoxelForgeCore/SharedColumnCache.h"
|
||||
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -817,9 +817,8 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
||||
struct FFarMeshColumnIndex
|
||||
{
|
||||
FFarMeshColumnIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent,
|
||||
int32 InCells,double InStep,double InVoxelSizeCm)
|
||||
int32 InCells,double InStep)
|
||||
:Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(InCells),Step(InStep),
|
||||
VoxelSizeCm(InVoxelSizeCm),
|
||||
ByColumn(static_cast<size_t>(InCells)*InCells)
|
||||
{
|
||||
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
|
||||
@@ -865,98 +864,16 @@ struct FFarMeshColumnIndex
|
||||
return Hits;
|
||||
}
|
||||
|
||||
bool HasDownwardHit(double X,double Y,double Z,double Tolerance) const
|
||||
{
|
||||
const int32 CX=std::clamp(static_cast<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
|
||||
const auto& Candidates=ByColumn[static_cast<size_t>(CY)*Cells+CX];
|
||||
for(const int32 Triangle:Candidates)
|
||||
{
|
||||
const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],
|
||||
IC=Mesh.Triangles[Triangle*3+2];
|
||||
if(IA<0||IB<0||IC<0||IA>=Mesh.Normals.Num()||IB>=Mesh.Normals.Num()
|
||||
||IC>=Mesh.Normals.Num())continue;
|
||||
if(Mesh.Normals[IA].Z+Mesh.Normals[IB].Z+Mesh.Normals[IC].Z>=-1.5)continue;
|
||||
const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2);
|
||||
const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2);
|
||||
const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2);
|
||||
const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX);
|
||||
if(std::abs(Den)<1.0e-10)continue;
|
||||
const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den;
|
||||
const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den;
|
||||
const double W=1.0-U-V;
|
||||
if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue;
|
||||
const double Hit=AZ+U*(BZ-AZ)+V*(CZ-AZ);
|
||||
if(std::abs(Hit-Z)<=Tolerance)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HasDownwardCellCoverage(double X0,double Y0,double X1,double Y1,
|
||||
double Z,double Tolerance) const
|
||||
{
|
||||
// Probe the perimeter as well as the interior: sparse interior-only probes can
|
||||
// incorrectly hide a missing ceiling quad at an MC grid vertex.
|
||||
// Sonder le perimetre et l'interieur: des sondes seulement interieures peuvent
|
||||
// masquer un carre de plafond manquant sur un sommet de la grille MC.
|
||||
constexpr double Samples[5]={0.0,0.25,0.5,0.75,1.0};
|
||||
for(const double V:Samples)for(const double U:Samples)
|
||||
{
|
||||
const double X=X0+(X1-X0)*U;
|
||||
const double Y=Y0+(Y1-Y0)*V;
|
||||
if(!HasDownwardHit(X,Y,Z,Tolerance))return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HasUpwardHit(double X,double Y,double Z,double Tolerance) const
|
||||
{
|
||||
const int32 CX=std::clamp(static_cast<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
|
||||
const auto& Candidates=ByColumn[static_cast<size_t>(CY)*Cells+CX];
|
||||
for(const int32 Triangle:Candidates)
|
||||
{
|
||||
const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],
|
||||
IC=Mesh.Triangles[Triangle*3+2];
|
||||
if(IA<0||IB<0||IC<0||IA>=Mesh.Normals.Num()||IB>=Mesh.Normals.Num()
|
||||
||IC>=Mesh.Normals.Num())continue;
|
||||
if(Mesh.Normals[IA].Z+Mesh.Normals[IB].Z+Mesh.Normals[IC].Z<=1.5)continue;
|
||||
const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2);
|
||||
const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2);
|
||||
const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2);
|
||||
const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX);
|
||||
if(std::abs(Den)<1.0e-10)continue;
|
||||
const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den;
|
||||
const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den;
|
||||
const double W=1.0-U-V;
|
||||
if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue;
|
||||
const double Hit=AZ+U*(BZ-AZ)+V*(CZ-AZ);
|
||||
if(std::abs(Hit-Z)<=Tolerance)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HasUpwardCellCoverage(double X0,double Y0,double X1,double Y1,
|
||||
double Z,double Tolerance) const
|
||||
{
|
||||
constexpr double Samples[5]={0.0,0.25,0.5,0.75,1.0};
|
||||
for(const double V:Samples)for(const double U:Samples)
|
||||
if(!HasUpwardHit(X0+(X1-X0)*U,Y0+(Y1-Y0)*V,Z,Tolerance))return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
double PositionAxis(int32 Index,int32 Axis) const
|
||||
{
|
||||
const FVector& P=Mesh.Vertices[Index];
|
||||
const double WorldCentimetres=Axis==0?P.X:(Axis==1?P.Y:P.Z);
|
||||
return WorldCentimetres/VoxelSizeCm;
|
||||
return (Axis==0?P.X:(Axis==1?P.Y:P.Z))*0.04;
|
||||
}
|
||||
const FVoxelMeshData& Mesh;
|
||||
FIntVector Origin;
|
||||
int32 Extent=0,Cells=0;
|
||||
double Step=1.0;
|
||||
double VoxelSizeCm=25.0;
|
||||
std::vector<std::vector<int32>> ByColumn;
|
||||
};
|
||||
|
||||
@@ -1057,11 +974,11 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
||||
std::vector<FBoundary> Boundaries;
|
||||
for(const FStrateSlot& Slot:Layout)
|
||||
{
|
||||
FStandaloneShapeWorld::FResolvedStratumBounds Resolved;
|
||||
if(!Shape.GetResolvedStratumBounds(Slot,Resolved))continue;
|
||||
const double HalfWidth=std::max(8.0,Resolved.SealThickness+8.0);
|
||||
Boundaries.push_back({Resolved.BottomZ,HalfWidth});
|
||||
Boundaries.push_back({Resolved.TopZ,HalfWidth});
|
||||
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(
|
||||
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
|
||||
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
|
||||
Boundaries.push_back({static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth});
|
||||
Boundaries.push_back({static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth});
|
||||
}
|
||||
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z<B.Z;});
|
||||
Boundaries.erase(std::unique(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
|
||||
@@ -1079,8 +996,7 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
||||
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
|
||||
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
|
||||
const int32 Cells=std::max(1,World.CoarseTileCells);
|
||||
FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step,
|
||||
static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0);
|
||||
FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step);
|
||||
std::vector<FVector> NewVertices,NewNormals;
|
||||
std::vector<FVector2f> NewUVs;std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
@@ -1216,466 +1132,3 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
||||
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
|
||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||
}
|
||||
|
||||
// Fill only underside cells that marching cubes missed. Active transitions use the
|
||||
// same displaced face and passage-mouth fade as the signed-distance field.
|
||||
struct FTransitionFaceReplacement
|
||||
{
|
||||
double BaseZVoxels=0.0;
|
||||
double MaximumDisplacementVoxels=0.0;
|
||||
std::vector<uint8> Cells;
|
||||
};
|
||||
|
||||
inline int32 RemoveCoarseTransitionFaceTriangles(FVoxelMeshData& Mesh,
|
||||
const FIntVector& Origin,int32 Step,int32 GridSide,double VoxelSizeCm,
|
||||
const std::vector<FTransitionFaceReplacement>& Replacements,bool bUpward)
|
||||
{
|
||||
if(Step<=0||GridSide<2||VoxelSizeCm<=0.0||Replacements.empty())return 0;
|
||||
const int32 TriangleCount=Mesh.Triangles.Num()/3;
|
||||
const int32 OldCeilingStart=std::max(0,TriangleCount-Mesh.NumCeilingTriangles);
|
||||
TArray<uint8> OldMasks=Mesh.TriangleSkirtFaceMasks;
|
||||
OldMasks.SetNum(TriangleCount);
|
||||
TArray<int32> KeptTriangles;
|
||||
TArray<uint8> KeptMasks;
|
||||
KeptTriangles.Reserve(Mesh.Triangles.Num());
|
||||
KeptMasks.Reserve(TriangleCount);
|
||||
int32 Removed=0,RemovedCeiling=0;
|
||||
const int32 CellSide=GridSide-1;
|
||||
for(int32 Triangle=0;Triangle<TriangleCount;++Triangle)
|
||||
{
|
||||
const int32 IA=Mesh.Triangles[Triangle*3];
|
||||
const int32 IB=Mesh.Triangles[Triangle*3+1];
|
||||
const int32 IC=Mesh.Triangles[Triangle*3+2];
|
||||
bool bRemove=false;
|
||||
if(OldMasks[Triangle]==0&&IA>=0&&IB>=0&&IC>=0
|
||||
&&IA<Mesh.Vertices.Num()&&IB<Mesh.Vertices.Num()&&IC<Mesh.Vertices.Num())
|
||||
{
|
||||
const FVector& A=Mesh.Vertices[IA];
|
||||
const FVector& B=Mesh.Vertices[IB];
|
||||
const FVector& C=Mesh.Vertices[IC];
|
||||
const FVector GeometricNormal=FVector::CrossProduct(B-A,C-A);
|
||||
const double NormalLength=GeometricNormal.Size();
|
||||
const double Facing=NormalLength>1.0e-12?GeometricNormal.Z/NormalLength:0.0;
|
||||
if((bUpward&&Facing>0.35)||(!bUpward&&Facing< -0.35))
|
||||
{
|
||||
const double X=(A.X+B.X+C.X)/(3.0*VoxelSizeCm);
|
||||
const double Y=(A.Y+B.Y+C.Y)/(3.0*VoxelSizeCm);
|
||||
const double Z=(A.Z+B.Z+C.Z)/(3.0*VoxelSizeCm);
|
||||
const int32 CellX=static_cast<int32>(std::floor((X-Origin.X)/Step));
|
||||
const int32 CellY=static_cast<int32>(std::floor((Y-Origin.Y)/Step));
|
||||
if(CellX>=0&&CellX<CellSide&&CellY>=0&&CellY<CellSide)
|
||||
{
|
||||
const std::size_t Cell=static_cast<std::size_t>(CellY)*CellSide+CellX;
|
||||
for(const auto& Replacement:Replacements)
|
||||
{
|
||||
if(Cell>=Replacement.Cells.size()||!Replacement.Cells[Cell])continue;
|
||||
const double Margin=Replacement.MaximumDisplacementVoxels+4.0;
|
||||
if(Z>=Replacement.BaseZVoxels-Margin
|
||||
&&Z<=Replacement.BaseZVoxels+Margin)
|
||||
{bRemove=true;break;}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(bRemove)
|
||||
{
|
||||
++Removed;
|
||||
if(Triangle>=OldCeilingStart)++RemovedCeiling;
|
||||
continue;
|
||||
}
|
||||
KeptTriangles.Add(IA);KeptTriangles.Add(IB);KeptTriangles.Add(IC);
|
||||
KeptMasks.Add(OldMasks[Triangle]);
|
||||
}
|
||||
if(Removed==0)return 0;
|
||||
Mesh.Triangles=std::move(KeptTriangles);
|
||||
Mesh.TriangleSkirtFaceMasks=std::move(KeptMasks);
|
||||
Mesh.NumCeilingTriangles=std::max(0,Mesh.NumCeilingTriangles-RemovedCeiling);
|
||||
return Removed;
|
||||
}
|
||||
|
||||
inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
||||
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||
{
|
||||
if(Key.Level<0||Step<=0||!World.Manager)return;
|
||||
const FIntVector Origin=Key.OriginVoxels();
|
||||
const int32 Extent=Key.ExtentVoxels();
|
||||
const int32 GridStep=Step;
|
||||
if(Extent<=0||GridStep<=0||Extent%GridStep!=0)return;
|
||||
const int32 GridSide=Extent/GridStep+1;
|
||||
struct FCandidate
|
||||
{
|
||||
double FaceZ=0.0;
|
||||
double Thickness=0.0;
|
||||
double MaximumDisplacementVoxels=0.0;
|
||||
std::vector<double> FaceHeights;
|
||||
std::vector<uint8> SolidAbove;
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
|
||||
bool bTransition=false;
|
||||
};
|
||||
std::vector<FCandidate> Candidates;
|
||||
std::vector<FTransitionFaceReplacement> Replacements;
|
||||
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
||||
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
||||
{
|
||||
const auto* Stratum=Shape.FindResolvedLayoutStratum(Slot.StrateIndex);
|
||||
if(!Stratum||!Stratum->ceiling.presentInCurrentField
|
||||
||Stratum->ceiling.topMicrometres<=Stratum->ceiling.bottomMicrometres)continue;
|
||||
FCandidate Candidate;
|
||||
Candidate.FaceZ=static_cast<double>(Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
||||
Candidate.Thickness=static_cast<double>(Stratum->ceiling.topMicrometres
|
||||
-Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
||||
Candidate.Stratum=Stratum;
|
||||
const auto* transitionUpper=Shape.FindResolvedLayoutStratumByOrder(Stratum->order-1);
|
||||
const bool bTransition=transitionUpper
|
||||
&&Shape.IsResolvedLayoutTransitionActive(*transitionUpper);
|
||||
const double displacement=bTransition?static_cast<double>(
|
||||
Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(*transitionUpper,*Stratum))
|
||||
/CoreMicrons:0.0;
|
||||
if(Candidate.FaceZ<Origin.Z-displacement-1.0e-6
|
||||
||Candidate.FaceZ>Origin.Z+Extent+displacement+1.0e-6)continue;
|
||||
Candidate.bTransition=bTransition;
|
||||
Candidate.MaximumDisplacementVoxels=displacement;
|
||||
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
||||
if(Epsilon<=0.0)continue;
|
||||
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||
Candidate.FaceHeights.resize(GridCount,Candidate.FaceZ);
|
||||
Candidate.SolidAbove.resize(GridCount,0);
|
||||
bool AnySolidAbove=false;
|
||||
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
||||
{
|
||||
const double WorldX=Origin.X+X*GridStep;
|
||||
const double WorldY=Origin.Y+Y*GridStep;
|
||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||
Candidate.bTransition=Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,
|
||||
WorldX,WorldY,Candidate.FaceHeights[GridIndex])||Candidate.bTransition;
|
||||
const float Above=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||
static_cast<float>(Candidate.FaceHeights[GridIndex]+Epsilon));
|
||||
const bool bSolidAbove=std::isfinite(Above)&&Above<0.0f;
|
||||
Candidate.SolidAbove[GridIndex]=bSolidAbove?1:0;
|
||||
AnySolidAbove=AnySolidAbove||bSolidAbove;
|
||||
}
|
||||
if(AnySolidAbove)Candidates.push_back(std::move(Candidate));
|
||||
}
|
||||
if(Candidates.empty())return;
|
||||
|
||||
const int32 Cells=std::max(1,World.CoarseTileCells);
|
||||
FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step,
|
||||
static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0);
|
||||
const int32 OldVertexCount=Mesh.Vertices.Num();
|
||||
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
|
||||
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
|
||||
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
|
||||
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
|
||||
const double VoxelSizeCm=static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0;
|
||||
std::vector<FVector> NewVertices,NewNormals;
|
||||
std::vector<FVector2f> NewUVs;
|
||||
std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
for(const FCandidate& Candidate:Candidates)
|
||||
{
|
||||
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
|
||||
FTransitionFaceReplacement Replacement;
|
||||
Replacement.BaseZVoxels=Candidate.FaceZ;
|
||||
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
|
||||
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
|
||||
const auto heightAt=[&](double X,double Y)
|
||||
{
|
||||
if(!Candidate.bTransition)return Candidate.FaceZ;
|
||||
double height=Candidate.FaceZ;
|
||||
Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,X,Y,height);
|
||||
return height;
|
||||
};
|
||||
const auto AddVertex=[&](int32 X,int32 Y)
|
||||
{
|
||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||
int32& Mapped=VertexMap[GridIndex];
|
||||
if(Mapped!=INDEX_NONE)return Mapped;
|
||||
Mapped=static_cast<int32>(NewVertices.size());
|
||||
const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep;
|
||||
const double FaceZ=Candidate.FaceHeights[GridIndex];
|
||||
FVector Normal(0.0,0.0,-1.0);
|
||||
if(Candidate.bTransition)
|
||||
{
|
||||
const double Delta=std::max(1.0,std::min<double>(GridStep,4.0));
|
||||
const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY))
|
||||
/(2.0*Delta);
|
||||
const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta))
|
||||
/(2.0*Delta);
|
||||
Normal=FVector(DX,DY,-1.0);
|
||||
Normal.Normalize();
|
||||
}
|
||||
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,FaceZ*VoxelSizeCm);
|
||||
NewNormals.push_back(Normal);
|
||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
||||
if(bHasColors)NewColors.push_back(Color);
|
||||
return Mapped;
|
||||
};
|
||||
for(int32 Y=0;Y+1<GridSide;++Y)for(int32 X=0;X+1<GridSide;++X)
|
||||
{
|
||||
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
||||
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
||||
if(!Candidate.SolidAbove[I00]||!Candidate.SolidAbove[I10]
|
||||
||!Candidate.SolidAbove[I01]||!Candidate.SolidAbove[I11])continue;
|
||||
const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep;
|
||||
const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep;
|
||||
bool fullyCovered=false;
|
||||
if(!Candidate.bTransition)
|
||||
fullyCovered=MeshIndex.HasDownwardCellCoverage(
|
||||
CellX0,CellY0,CellX1,CellY1,Candidate.FaceZ,0.001);
|
||||
else
|
||||
{
|
||||
fullyCovered=true;
|
||||
constexpr double CoverageSamples[5][2]={
|
||||
{0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}};
|
||||
for(const auto& Sample:CoverageSamples)
|
||||
{
|
||||
const double U=Sample[0],V=Sample[1];
|
||||
const double SampleX=CellX0+(CellX1-CellX0)*U;
|
||||
const double SampleY=CellY0+(CellY1-CellY0)*V;
|
||||
const double SampleZ=heightAt(SampleX,SampleY);
|
||||
if(!MeshIndex.HasDownwardHit(SampleX,SampleY,SampleZ,0.25))
|
||||
fullyCovered=false;
|
||||
}
|
||||
}
|
||||
if(fullyCovered)continue;
|
||||
if(Key.Level>=4&&Candidate.bTransition)
|
||||
Replacement.Cells[static_cast<std::size_t>(Y)*(GridSide-1)+X]=1;
|
||||
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
||||
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
||||
NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C});
|
||||
}
|
||||
if(Key.Level>=4&&Candidate.bTransition
|
||||
&&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(),
|
||||
[](uint8 Value){return Value!=0;}))
|
||||
Replacements.push_back(std::move(Replacement));
|
||||
}
|
||||
if(NewTriangles.empty())return;
|
||||
const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,GridStep,
|
||||
GridSide,VoxelSizeCm,Replacements,false);
|
||||
|
||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||
{
|
||||
using TArrayType=std::remove_reference_t<decltype(Existing)>;
|
||||
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
|
||||
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
|
||||
for(const auto& Value:Added)Combined.Add(Value);
|
||||
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
|
||||
Existing=std::move(Combined);
|
||||
};
|
||||
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
|
||||
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
|
||||
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
|
||||
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
|
||||
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
|
||||
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(
|
||||
Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
|
||||
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
|
||||
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
|
||||
TArray<uint8> ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks;
|
||||
ExistingFaceMasks.SetNum(OldTriangleCount);
|
||||
TArray<uint8> CombinedFaceMasks;CombinedFaceMasks.Reserve(
|
||||
OldTriangleCount+static_cast<int32>(NewTriangles.size()/3));
|
||||
for(int32 Triangle=0;Triangle<CeilingStart/3;++Triangle)
|
||||
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
|
||||
for(std::size_t Triangle=0;Triangle<NewTriangles.size()/3;++Triangle)
|
||||
CombinedFaceMasks.Add(0);
|
||||
for(int32 Triangle=CeilingStart/3;Triangle<OldTriangleCount;++Triangle)
|
||||
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
|
||||
Mesh.Triangles=std::move(CombinedTriangles);
|
||||
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
|
||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||
Mesh.NumCeilingTriangles+=static_cast<int32>(NewTriangles.size()/3);
|
||||
}
|
||||
|
||||
// Fill only top cells that marching cubes missed. Active transitions use the same
|
||||
// displaced face and passage-mouth fade as the signed-distance field.
|
||||
inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
||||
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||
{
|
||||
if(Key.Level<0||Step<=0||!World.Manager||!Shape.ResolvedLayout)return;
|
||||
const FIntVector Origin=Key.OriginVoxels();
|
||||
const int32 Extent=Key.ExtentVoxels();
|
||||
if(Extent<=0||Extent%Step!=0)return;
|
||||
const int32 GridSide=Extent/Step+1;
|
||||
struct FCandidate
|
||||
{
|
||||
double FloorZ=0.0;
|
||||
double Thickness=0.0;
|
||||
double MaximumDisplacementVoxels=0.0;
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
|
||||
bool bTransition=false;
|
||||
};
|
||||
std::vector<FCandidate> Candidates;
|
||||
std::vector<FTransitionFaceReplacement> Replacements;
|
||||
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
||||
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
||||
{
|
||||
const auto* Stratum=Shape.FindResolvedLayoutStratum(Slot.StrateIndex);
|
||||
if(!Stratum||!Stratum->bedrock.presentInCurrentField
|
||||
||Stratum->bedrock.topMicrometres<=Stratum->bedrock.bottomMicrometres)continue;
|
||||
const double FloorZ=static_cast<double>(Stratum->bedrock.topMicrometres)/CoreMicrons;
|
||||
const auto* transitionLower=Shape.FindResolvedLayoutStratumByOrder(Stratum->order+1);
|
||||
const bool bTransition=transitionLower
|
||||
&&Shape.IsResolvedLayoutTransitionActive(*Stratum);
|
||||
const double displacement=bTransition
|
||||
?static_cast<double>(Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
||||
*Stratum,*transitionLower))/CoreMicrons
|
||||
:0.0;
|
||||
if(FloorZ<Origin.Z-displacement-1.0e-6
|
||||
||FloorZ>Origin.Z+Extent+displacement+1.0e-6)continue;
|
||||
Candidates.push_back({FloorZ,static_cast<double>(Stratum->bedrock.topMicrometres
|
||||
-Stratum->bedrock.bottomMicrometres)/CoreMicrons,displacement,Stratum,bTransition});
|
||||
}
|
||||
if(Candidates.empty())return;
|
||||
|
||||
const int32 Cells=std::max(1,World.CoarseTileCells);
|
||||
FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step,
|
||||
static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0);
|
||||
const int32 OldVertexCount=Mesh.Vertices.Num();
|
||||
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
|
||||
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
|
||||
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
|
||||
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
|
||||
const double VoxelSizeCm=static_cast<double>(World.CoreVoxelSizeMicrons)/10000.0;
|
||||
std::vector<FVector> NewVertices,NewNormals;
|
||||
std::vector<FVector2f> NewUVs;
|
||||
std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
for(const FCandidate& Candidate:Candidates)
|
||||
{
|
||||
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
||||
if(Epsilon<=0.0)continue;
|
||||
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||
std::vector<double> faceHeights(GridCount,Candidate.FloorZ);
|
||||
std::vector<uint8> solidBelow(GridCount,0);
|
||||
FTransitionFaceReplacement Replacement;
|
||||
Replacement.BaseZVoxels=Candidate.FloorZ;
|
||||
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
|
||||
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
|
||||
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
||||
{
|
||||
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||
if(Candidate.bTransition)
|
||||
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,WorldX,WorldY,
|
||||
faceHeights[GridIndex]);
|
||||
const float Density=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||
static_cast<float>(faceHeights[GridIndex]-Epsilon));
|
||||
solidBelow[GridIndex]=
|
||||
std::isfinite(Density)&&Density<0.0f?1:0;
|
||||
}
|
||||
const auto heightAt=[&](double X,double Y)
|
||||
{
|
||||
if(!Candidate.bTransition)return Candidate.FloorZ;
|
||||
double height=Candidate.FloorZ;
|
||||
if(Candidate.bTransition)
|
||||
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,X,Y,height);
|
||||
return height;
|
||||
};
|
||||
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
|
||||
const auto AddVertex=[&](int32 X,int32 Y)
|
||||
{
|
||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||
int32& Mapped=VertexMap[GridIndex];
|
||||
if(Mapped!=INDEX_NONE)return Mapped;
|
||||
Mapped=static_cast<int32>(NewVertices.size());
|
||||
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
||||
FVector Normal(0.0,0.0,1.0);
|
||||
if(Candidate.bTransition)
|
||||
{
|
||||
const double Delta=std::max(1.0,std::min<double>(Step,4.0));
|
||||
const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY))
|
||||
/(2.0*Delta);
|
||||
const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta))
|
||||
/(2.0*Delta);
|
||||
Normal=FVector(-DX,-DY,1.0);
|
||||
Normal.Normalize();
|
||||
}
|
||||
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,
|
||||
faceHeights[GridIndex]*VoxelSizeCm);
|
||||
NewNormals.push_back(Normal);
|
||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
||||
if(bHasColors)NewColors.push_back(Color);
|
||||
return Mapped;
|
||||
};
|
||||
for(int32 Y=0;Y+1<GridSide;++Y)for(int32 X=0;X+1<GridSide;++X)
|
||||
{
|
||||
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
||||
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
||||
if(!solidBelow[I00]||!solidBelow[I10]||!solidBelow[I01]||!solidBelow[I11])continue;
|
||||
const double X0=Origin.X+X*Step,Y0=Origin.Y+Y*Step;
|
||||
const double X1=X0+Step,Y1=Y0+Step;
|
||||
bool fullyCovered=false;
|
||||
if(!Candidate.bTransition)
|
||||
fullyCovered=MeshIndex.HasUpwardCellCoverage(
|
||||
X0,Y0,X1,Y1,Candidate.FloorZ,0.001);
|
||||
else
|
||||
{
|
||||
fullyCovered=true;
|
||||
constexpr double CoverageSamples[5][2]={
|
||||
{0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}};
|
||||
for(const auto& Sample:CoverageSamples)
|
||||
{
|
||||
const double U=Sample[0],V=Sample[1];
|
||||
const double SampleX=X0+(X1-X0)*U;
|
||||
const double SampleY=Y0+(Y1-Y0)*V;
|
||||
const double SampleZ=heightAt(SampleX,SampleY);
|
||||
if(!MeshIndex.HasUpwardHit(SampleX,SampleY,SampleZ,0.25))
|
||||
fullyCovered=false;
|
||||
}
|
||||
}
|
||||
if(fullyCovered)continue;
|
||||
if(Key.Level>=4&&Candidate.bTransition)
|
||||
Replacement.Cells[static_cast<std::size_t>(Y)*(GridSide-1)+X]=1;
|
||||
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
||||
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
||||
NewTriangles.insert(NewTriangles.end(),{A,B,C,A,C,D});
|
||||
}
|
||||
if(Key.Level>=4&&Candidate.bTransition
|
||||
&&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(),
|
||||
[](uint8 Value){return Value!=0;}))
|
||||
Replacements.push_back(std::move(Replacement));
|
||||
}
|
||||
if(NewTriangles.empty())return;
|
||||
const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,Step,
|
||||
GridSide,VoxelSizeCm,Replacements,true);
|
||||
|
||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||
{
|
||||
using TArrayType=std::remove_reference_t<decltype(Existing)>;
|
||||
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
|
||||
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
|
||||
for(const auto& Value:Added)Combined.Add(Value);
|
||||
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
|
||||
Existing=std::move(Combined);
|
||||
};
|
||||
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
|
||||
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
|
||||
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
|
||||
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
|
||||
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
|
||||
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(
|
||||
Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
|
||||
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
|
||||
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
|
||||
TArray<uint8> ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks;
|
||||
ExistingFaceMasks.SetNum(OldTriangleCount);
|
||||
TArray<uint8> CombinedFaceMasks;CombinedFaceMasks.Reserve(
|
||||
OldTriangleCount+static_cast<int32>(NewTriangles.size()/3));
|
||||
for(int32 Triangle=0;Triangle<CeilingStart/3;++Triangle)
|
||||
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
|
||||
for(std::size_t Triangle=0;Triangle<NewTriangles.size()/3;++Triangle)
|
||||
CombinedFaceMasks.Add(0);
|
||||
for(int32 Triangle=CeilingStart/3;Triangle<OldTriangleCount;++Triangle)
|
||||
CombinedFaceMasks.Add(ExistingFaceMasks[Triangle]);
|
||||
Mesh.Triangles=std::move(CombinedTriangles);
|
||||
Mesh.TriangleSkirtFaceMasks=std::move(CombinedFaceMasks);
|
||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -331,28 +331,6 @@ VF_FIELD_INLINE float VF_FieldPerlin3D(float x,float y,float z)
|
||||
return VF_FieldPerlinLerp(y0,y1,sw);
|
||||
}
|
||||
|
||||
VF_FIELD_INLINE float VF_FieldPassageWallRelief(float x,float y,float z,VF_FieldU32 seed)
|
||||
{
|
||||
// Use long, non-commensurate rotated scales. The shared passage wall keeps broad,
|
||||
// uneven rock relief without a short wavelength that can read as repeated rings.
|
||||
const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
|
||||
seed^0xB5297A4Du);
|
||||
const float mid=VF_FieldValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
|
||||
seed^0x63D83595u);
|
||||
const float fine=VF_FieldValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
|
||||
seed^0xA511E9B3u);
|
||||
return 0.50f*broad+0.35f*mid+0.15f*fine;
|
||||
}
|
||||
|
||||
VF_FIELD_INLINE float VF_FieldPassageGroundGrain(float x,float y,VF_FieldU32 seed)
|
||||
{
|
||||
const float broad=VF_FieldValueNoise(x+3.25f,y-7.5f,0.0f,8,
|
||||
seed^0xC2B2AE35u);
|
||||
const float fine=VF_FieldValueNoise(y+11.75f,x-5.0f,0.0f,5,
|
||||
seed^0x27D4EB2Fu);
|
||||
return 0.65f*broad+0.35f*fine;
|
||||
}
|
||||
|
||||
VF_FIELD_INLINE float VF_FieldOwnerFractal(float x,float y,float z,VF_FieldI32 octaves,
|
||||
VF_FieldI32 mode)
|
||||
{
|
||||
@@ -394,9 +372,9 @@ VF_FIELD_INLINE float VF_FieldOwnerCellular(float x,float y,float z)
|
||||
{
|
||||
const VF_FieldI32 nx=cellX+dx,ny=cellY+dy,nz=cellZ+dz;
|
||||
const VF_FieldU32 h=VF_FieldVoxelHashMix(
|
||||
(VF_FIELD_CAST_U32(nx)+0x7FFFFFFFu)
|
||||
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(ny)+0x7FFFFFFFu)*2654435761u)
|
||||
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(nz)+0x7FFFFFFFu)*374761393u));
|
||||
VF_FIELD_CAST_U32(nx+0x7FFFFFFF)
|
||||
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(ny+0x7FFFFFFF)*2654435761u)
|
||||
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(nz+0x7FFFFFFF)*374761393u));
|
||||
const float fpx=VF_FIELD_CAST_F32(dx)
|
||||
+VF_FIELD_CAST_F32(h&0xFFFFFFu)*(1.0f/16777216.0f)-fracX;
|
||||
const VF_FieldU32 hy=VF_FieldVoxelHashMix(h^0x12345678u);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
@@ -122,9 +121,6 @@ WorldAssemblyFeatureColumnRangeV1 BuildWorldAssemblyFeatureColumnRangeV1(
|
||||
|
||||
namespace world_assembly_detail
|
||||
{
|
||||
using FSharedHeightfieldColumnCache =
|
||||
VoxelForgeCoreShared::TSharedColumnValueCache<HeightfieldPointSample,131072,4>;
|
||||
|
||||
// Preserve recipe order without allocating or looking up metadata in the point loop.
|
||||
// Garder l'ordre de recette sans allocation ni recherche de metadonnees par point.
|
||||
struct FeatureRange
|
||||
@@ -145,12 +141,6 @@ namespace world_assembly_detail
|
||||
};
|
||||
|
||||
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 MaxSerializedMagnitude = 1000000000000ll;
|
||||
constexpr std::size_t MaxBiomeCount = 8;
|
||||
@@ -234,114 +224,6 @@ namespace world_assembly_detail
|
||||
inline bool OutsideRange(std::int64_t value)
|
||||
{ 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)
|
||||
{
|
||||
if (recipe.schemaVersion != WorldAssemblySchemaVersion
|
||||
@@ -349,9 +231,6 @@ namespace world_assembly_detail
|
||||
|| recipe.evaluatorVersion > WorldAssemblyEvaluatorVersion
|
||||
|| recipe.unitsPerVoxel != WorldAssemblySubvoxelsPerVoxel)
|
||||
{ error = "world assembly recipe version, evaluator or fixed scale is unsupported"; return false; }
|
||||
if (recipe.layoutChunkWidthSubvoxels <= 0
|
||||
|| recipe.layoutChunkWidthSubvoxels > 256ll * 512ll)
|
||||
{ error = "resolved strate-layout chunk width is outside the supported physical range"; return false; }
|
||||
if (recipe.biomes.empty() || recipe.biomes.size() > MaxBiomeCount)
|
||||
{ error = "world assembly recipe must contain one to eight biome profiles"; return false; }
|
||||
if (recipe.biomeCellSizeSubvoxels <= 0 || recipe.biomeCellSizeSubvoxels > 100000000ll
|
||||
@@ -410,57 +289,15 @@ namespace world_assembly_detail
|
||||
|| recipe.biomeClimateMoistureWavelengthSubvoxels <= 0
|
||||
|| recipe.biomeClimateReliefContrastQ16 < 0))
|
||||
{ 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)
|
||||
inline bool ValidateStratum(const WorldAssemblyStratumPlanV1& stratum, 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;
|
||||
}
|
||||
|
||||
inline bool ValidateStratum(const WorldAssemblyStratumPlanV1& stratum,
|
||||
const WorldAssemblyRecipeV1& recipe, std::string& error)
|
||||
{
|
||||
const std::int64_t layoutChunkWidthSubvoxels = recipe.layoutChunkWidthSubvoxels;
|
||||
if (stratum.strateIndex < 0 || stratum.topChunkZ < stratum.bottomChunkZ
|
||||
|| stratum.capWavelengthSubvoxels <= 0 || stratum.capUndulationSubvoxels < 0
|
||||
|| stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount)
|
||||
{ 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)
|
||||
|| !ValidScalar(stratum.capWavelengthSubvoxels)
|
||||
|| !ValidScalar(stratum.capDetailAmplitudeSubvoxels)
|
||||
@@ -489,9 +326,9 @@ namespace world_assembly_detail
|
||||
if (feature.type == 2 || feature.type == 4)
|
||||
{
|
||||
const std::int64_t stratumMinZ = static_cast<std::int64_t>(stratum.bottomChunkZ)
|
||||
* layoutChunkWidthSubvoxels;
|
||||
* 32 * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t stratumMaxZ = (static_cast<std::int64_t>(stratum.topChunkZ) + 1)
|
||||
* layoutChunkWidthSubvoxels;
|
||||
* 32 * WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr std::int64_t maximumExceptionExtent = 1024
|
||||
* WorldAssemblySubvoxelsPerVoxel;
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
@@ -579,7 +416,6 @@ namespace world_assembly_detail
|
||||
std::uint64_t hash = StartHash();
|
||||
HashWord(hash, recipe.schemaVersion, 4); HashWord(hash, recipe.evaluatorVersion, 4);
|
||||
HashWord(hash, recipe.unitsPerVoxel, 4); HashWord(hash, recipe.seed, 4);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutChunkWidthSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeCellSizeSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeWarpSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeWarpWavelengthSubvoxels), 8);
|
||||
@@ -621,8 +457,6 @@ namespace world_assembly_detail
|
||||
HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
}
|
||||
}
|
||||
if (recipe.surfaceProfile.version != 0)
|
||||
HashSurfaceProfile(hash, recipe.surfaceProfile);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -682,8 +516,6 @@ namespace world_assembly_detail
|
||||
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);
|
||||
}
|
||||
if (stratum.surfaceProfile.version != 0)
|
||||
HashSurfaceProfile(hash, stratum.surfaceProfile);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -786,66 +618,12 @@ namespace world_assembly_detail
|
||||
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,
|
||||
std::string& error)
|
||||
{
|
||||
if (!value.IsObject() || !ReadU32(value.Get("schema_version"), recipe.schemaVersion)
|
||||
|| !ReadU32(value.Get("evaluator_version"), recipe.evaluatorVersion)
|
||||
|| !ReadU32(value.Get("units_per_voxel"), recipe.unitsPerVoxel)
|
||||
|| !ReadInteger(value.Get("layout_chunk_width_q"), recipe.layoutChunkWidthSubvoxels)
|
||||
|| !ReadU32(value.Get("seed"), recipe.seed)
|
||||
|| !ReadInteger(value.Get("biome_cell_size_q"), recipe.biomeCellSizeSubvoxels)
|
||||
|| !ReadInteger(value.Get("biome_warp_q"), recipe.biomeWarpSubvoxels)
|
||||
@@ -859,10 +637,6 @@ namespace world_assembly_detail
|
||||
|| !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels)
|
||||
|| !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16)))
|
||||
{ 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())
|
||||
{
|
||||
WorldAssemblyBiomeProfileV1 biome;
|
||||
@@ -915,7 +689,6 @@ namespace world_assembly_detail
|
||||
out << "{\"schema_version\":" << recipe.schemaVersion
|
||||
<< ",\"evaluator_version\":" << recipe.evaluatorVersion
|
||||
<< ",\"units_per_voxel\":" << recipe.unitsPerVoxel
|
||||
<< ",\"layout_chunk_width_q\":" << recipe.layoutChunkWidthSubvoxels
|
||||
<< ",\"seed\":" << recipe.seed
|
||||
<< ",\"biome_cell_size_q\":" << recipe.biomeCellSizeSubvoxels
|
||||
<< ",\"biome_warp_q\":" << recipe.biomeWarpSubvoxels
|
||||
@@ -925,8 +698,6 @@ namespace world_assembly_detail
|
||||
out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels
|
||||
<< ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels
|
||||
<< ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16;
|
||||
if (recipe.surfaceProfile.version != 0)
|
||||
out << ",\"surface_profile\":" << SurfaceProfileJson(recipe.surfaceProfile);
|
||||
out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":[";
|
||||
for (std::size_t i = 0; i < recipe.biomes.size(); ++i)
|
||||
{
|
||||
@@ -1015,10 +786,6 @@ namespace world_assembly_detail
|
||||
{ error = "world assembly stratum has malformed optional evaluator-2 fields"; return false; }
|
||||
if(hasWaterBoolean)stratum.waterEnabled=waterEnabled.Boolean(false);
|
||||
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");
|
||||
if (!std::holds_alternative<std::nullptr_t>(routeConstraintsValue.Data))
|
||||
{
|
||||
@@ -1090,8 +857,7 @@ namespace world_assembly_detail
|
||||
|| !ReadI32(islandValue.Get("bottom_chunk_z"), entry.bottomChunkZ))
|
||||
{ error = "world assembly floating-island slot is missing its resolved strate bounds"; return std::nullopt; }
|
||||
const std::string nestedJson = JsonText(islandValue.Get("manifest"));
|
||||
auto parsedIsland = FloatingIslandManifestV1::FromJson(
|
||||
nestedJson, error, manifest.recipe.layoutChunkWidthSubvoxels);
|
||||
auto parsedIsland = FloatingIslandManifestV1::FromJson(nestedJson, error);
|
||||
if (!parsedIsland) return std::nullopt;
|
||||
entry.manifest = std::move(*parsedIsland);
|
||||
manifest.floatingIslands.push_back(std::move(entry));
|
||||
@@ -1589,196 +1355,6 @@ namespace world_assembly_detail
|
||||
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
|
||||
{
|
||||
const WorldAssemblyRecipeV1* recipe = nullptr;
|
||||
@@ -1799,9 +1375,6 @@ namespace world_assembly_detail
|
||||
std::array<std::uint8_t, HeightfieldColumnCacheSetCount>> HeightfieldColumnCacheNextWay;
|
||||
inline thread_local HeightfieldColumnCacheStats HeightfieldColumnStats{};
|
||||
inline thread_local bool HeightfieldColumnCacheLegacyDirectMapped=false;
|
||||
inline thread_local std::array<std::int64_t,4> HeightfieldColumnCaptureBounds{};
|
||||
inline thread_local bool HeightfieldColumnCaptureBoundsValid=false;
|
||||
inline std::atomic<std::uint64_t> HeightfieldColumnCaptureSequence{0};
|
||||
|
||||
inline std::size_t HeightfieldColumnCacheSet(const WorldAssemblyRecipeV1& recipe,
|
||||
const WorldAssemblyStratumPlanV1& plan,std::int64_t x,std::int64_t y) noexcept
|
||||
@@ -1827,61 +1400,6 @@ namespace world_assembly_detail
|
||||
HeightfieldPointSample out;
|
||||
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 auto& primary=recipe.biomes[selection.primary];
|
||||
const auto& neighbor=recipe.biomes[selection.neighbor];
|
||||
@@ -2069,46 +1587,22 @@ namespace world_assembly_detail
|
||||
replacement=&cache[first+replacementWay];
|
||||
}
|
||||
auto& entry=*replacement;
|
||||
++HeightfieldColumnStats.columnMisses;
|
||||
const auto compute=[&]()
|
||||
{
|
||||
const std::uint64_t sequence=HeightfieldColumnStats.captureCoordinates
|
||||
?HeightfieldColumnCaptureSequence.fetch_add(1,std::memory_order_relaxed):0;
|
||||
const auto start=std::chrono::steady_clock::now();
|
||||
HeightfieldPointSample result=EvaluateColumn(recipe,plan,x,y);
|
||||
HeightfieldPointSample column=EvaluateColumn(recipe,plan,x,y);
|
||||
HeightfieldColumnStats.columnComputeMilliseconds+=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-start).count();
|
||||
++HeightfieldColumnStats.columnEvaluations;
|
||||
if(HeightfieldColumnStats.captureCoordinates
|
||||
&&HeightfieldColumnStats.coordinateCaptureComplete)
|
||||
++HeightfieldColumnStats.columnMisses;
|
||||
if(HeightfieldColumnStats.captureCoordinates&&HeightfieldColumnStats.coordinateCaptureComplete)
|
||||
{
|
||||
try
|
||||
{
|
||||
const bool outside=HeightfieldColumnCaptureBoundsValid
|
||||
&&(x<HeightfieldColumnCaptureBounds[0]||x>HeightfieldColumnCaptureBounds[2]
|
||||
||y<HeightfieldColumnCaptureBounds[1]||y>HeightfieldColumnCaptureBounds[3]);
|
||||
HeightfieldColumnStats.evaluatedCoordinates.push_back({x,y,sequence,outside});
|
||||
HeightfieldColumnStats.evaluatedCoordinates.push_back({x,y});
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
HeightfieldColumnStats.coordinateCaptureComplete=false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
HeightfieldPointSample column{};
|
||||
if(auto* shared=FSharedHeightfieldColumnCache::threadActive())
|
||||
{
|
||||
const VoxelForgeCoreShared::FColumnCacheKey key{
|
||||
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe)),
|
||||
recipe.contentHash,
|
||||
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
|
||||
plan.contentHash,x,y};
|
||||
bool computed=false;
|
||||
column=shared->getOrCompute(key,compute,computed);
|
||||
if(!computed)++HeightfieldColumnStats.sharedColumnHits;
|
||||
}
|
||||
else column=compute();
|
||||
entry={&recipe,&plan,recipe.contentHash,plan.contentHash,x,y,column};
|
||||
return column;
|
||||
}
|
||||
@@ -2209,16 +1703,6 @@ std::optional<WorldAssemblyManifest> WorldAssemblyManifest::FromJson(
|
||||
|
||||
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
|
||||
|| !world_assembly_detail::ValidateRecipe(recipe, error))
|
||||
{ if (error.empty()) error = "world assembly manifest or plan version is unsupported"; return false; }
|
||||
@@ -2236,15 +1720,15 @@ bool WorldAssemblyManifest::finalize(std::string& error)
|
||||
{ error = "SurfaceWorld stratum recipe seed does not match the world assembly season seed"; return false; }
|
||||
stratum.recipeOverride.contentHash = world_assembly_detail::HashRecipe(stratum.recipeOverride);
|
||||
}
|
||||
if (!world_assembly_detail::ValidateStratum(stratum, recipe, error)) return false;
|
||||
if (!world_assembly_detail::ValidateStratum(stratum, error)) return false;
|
||||
if (recipe.evaluatorVersion >= 4)
|
||||
{
|
||||
if (stratum.routeConstraints.version != 1 || stratum.route.size() < 2)
|
||||
{ error = "world assembly evaluator 4 requires a versioned entrance-to-exit route"; return false; }
|
||||
const std::int64_t bottomZ = static_cast<std::int64_t>(stratum.bottomChunkZ)
|
||||
* recipe.layoutChunkWidthSubvoxels;
|
||||
* 32 * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t topZ = (static_cast<std::int64_t>(stratum.topChunkZ) + 1)
|
||||
* recipe.layoutChunkWidthSubvoxels;
|
||||
* 32 * WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t halfHeight = stratum.routeConstraints.capsuleHeightSubvoxels / 2;
|
||||
for (const auto& point : stratum.route)
|
||||
{
|
||||
@@ -2274,12 +1758,14 @@ bool WorldAssemblyManifest::finalize(std::string& error)
|
||||
const auto& Scale = islands.manifest.recipe;
|
||||
if (Scale.ownerToCoreScaleNumerator == 0 || Scale.ownerToCoreScaleDenominator == 0)
|
||||
{ error = "floating-island owner-to-Core scale is invalid"; return false; }
|
||||
if(Scale.layoutChunkWidthSubvoxels<=0)
|
||||
{error="floating-island strate has no resolved layout chunk width";return false;}
|
||||
const std::int64_t bottomQ = static_cast<std::int64_t>(islands.bottomChunkZ)
|
||||
*Scale.layoutChunkWidthSubvoxels;
|
||||
const std::int64_t topQ = (static_cast<std::int64_t>(islands.topChunkZ)+1)
|
||||
*Scale.layoutChunkWidthSubvoxels;
|
||||
const double CoreVoxelsPerOwnerChunk = static_cast<double>(CoreTileVoxelCount)
|
||||
* Scale.ownerToCoreScaleNumerator / Scale.ownerToCoreScaleDenominator;
|
||||
const std::int64_t bottomQ = static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(islands.bottomChunkZ) * CoreVoxelsPerOwnerChunk
|
||||
* WorldAssemblySubvoxelsPerVoxel));
|
||||
const std::int64_t topQ = static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(static_cast<std::int64_t>(islands.topChunkZ) + 1)
|
||||
* CoreVoxelsPerOwnerChunk * WorldAssemblySubvoxelsPerVoxel));
|
||||
if (islands.manifest.recipe.layoutMinimumZSubvoxels < bottomQ
|
||||
|| islands.manifest.recipe.layoutMaximumZSubvoxels > topQ)
|
||||
{ error = "floating-island recipe layout escapes the resolved owner strate bounds"; return false; }
|
||||
@@ -2359,9 +1845,6 @@ std::string WorldAssemblyManifest::ToJson() const
|
||||
if (stratum.hasRecipeOverride)
|
||||
out << ",\"recipe_override\":"
|
||||
<< world_assembly_detail::RecipeJson(stratum.recipeOverride);
|
||||
if (stratum.surfaceProfile.version != 0)
|
||||
out << ",\"surface_profile\":"
|
||||
<< world_assembly_detail::SurfaceProfileJson(stratum.surfaceProfile);
|
||||
out
|
||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash)
|
||||
<< "\",\"features\":[";
|
||||
@@ -2437,8 +1920,6 @@ bool Heightfield2D::isValid() const noexcept
|
||||
|| recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount
|
||||
|| recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0
|
||||
|| recipe_->biomeWarpSubvoxels < 0 || recipe_->biomeBorderBlendSubvoxels < 0
|
||||
|| recipe_->surfaceProfile != plan_->surfaceProfile
|
||||
|| recipe_->surfaceProfile.version > 1
|
||||
|| plan_->strateIndex < 0 || plan_->topChunkZ < plan_->bottomChunkZ
|
||||
|| plan_->capWavelengthSubvoxels <= 0 || plan_->capUndulationSubvoxels < 0)
|
||||
return false;
|
||||
@@ -2455,32 +1936,6 @@ bool Heightfield2D::isValid() const noexcept
|
||||
std::array<std::int64_t,2> Heightfield2D::capHeightBounds() const noexcept
|
||||
{
|
||||
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 Maximum=-std::numeric_limits<long double>::infinity();
|
||||
const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome)
|
||||
@@ -2514,7 +1969,7 @@ std::array<std::int64_t,2> Heightfield2D::capHeightBounds() const noexcept
|
||||
if(recipe_->evaluatorVersion>=5)
|
||||
{
|
||||
const long double TopSubvoxels=(static_cast<long double>(plan_->topChunkZ)+1.0L)
|
||||
*recipe_->layoutChunkWidthSubvoxels;
|
||||
*CoreTileVoxelCount*WorldAssemblySubvoxelsPerVoxel;
|
||||
Minimum=std::min(Minimum,TopSubvoxels);
|
||||
Maximum=std::min(Maximum,TopSubvoxels);
|
||||
}
|
||||
@@ -2533,7 +1988,7 @@ std::int64_t Heightfield2D::farSurfaceCapHeightSubvoxels(std::int64_t x,
|
||||
// Clip only the far render surface. Keep the near density evaluator bit-identical.
|
||||
// Borner uniquement le plafond lointain. L'evaluateur de densite proche reste identique.
|
||||
const std::int64_t ownedUpperBoundary=(static_cast<std::int64_t>(plan_->topChunkZ)+1)
|
||||
*recipe_->layoutChunkWidthSubvoxels;
|
||||
*CoreTileVoxelCount*WorldAssemblySubvoxelsPerVoxel;
|
||||
return std::min(column.capHeightSubvoxels,ownedUpperBoundary);
|
||||
}
|
||||
|
||||
@@ -2632,22 +2087,10 @@ void Heightfield2D::resetThreadColumnCacheStats(bool captureCoordinates, bool cl
|
||||
Stats.columnHits=0;
|
||||
Stats.columnMisses=0;
|
||||
Stats.columnConflictMisses=0;
|
||||
Stats.sharedColumnHits=0;
|
||||
Stats.columnEvaluations=0;
|
||||
Stats.columnComputeMilliseconds=0.0;
|
||||
Stats.captureCoordinates=captureCoordinates;
|
||||
Stats.coordinateCaptureComplete=true;
|
||||
Stats.evaluatedCoordinates.clear();
|
||||
world_assembly_detail::HeightfieldColumnCaptureBoundsValid=false;
|
||||
}
|
||||
|
||||
void Heightfield2D::setThreadColumnCaptureBounds(std::int64_t minimumXSubvoxels,
|
||||
std::int64_t minimumYSubvoxels,std::int64_t maximumXSubvoxels,
|
||||
std::int64_t maximumYSubvoxels) noexcept
|
||||
{
|
||||
world_assembly_detail::HeightfieldColumnCaptureBounds={minimumXSubvoxels,
|
||||
minimumYSubvoxels,maximumXSubvoxels,maximumYSubvoxels};
|
||||
world_assembly_detail::HeightfieldColumnCaptureBoundsValid=true;
|
||||
}
|
||||
|
||||
void Heightfield2D::setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept
|
||||
@@ -2686,15 +2129,6 @@ HeightfieldBoxClass Heightfield2D::classifyLatticeBox(std::int64_t minimumX,
|
||||
&&feature.minimumSubvoxels[2]<=maximumZ&&feature.maximumSubvoxels[2]>=minimumZ)
|
||||
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)
|
||||
*WorldAssemblySubvoxelsPerVoxel;
|
||||
if((maximumX-minimumX)%stride!=0||(maximumY-minimumY)%stride!=0
|
||||
@@ -2737,29 +2171,7 @@ 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 capMin=plan_->capHeightSubvoxels-plan_->capUndulationSubvoxels;
|
||||
std::int64_t capMax=plan_->capHeightSubvoxels+plan_->capUndulationSubvoxels;
|
||||
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)
|
||||
if(recipe_->evaluatorVersion>=2)
|
||||
{
|
||||
for(const auto& biome:recipe_->biomes)
|
||||
{
|
||||
@@ -2843,8 +2255,8 @@ FieldInfluenceBounds Heightfield2D::influenceBounds() const noexcept
|
||||
{
|
||||
FieldInfluenceBounds out;
|
||||
if (!isValid()) return out;
|
||||
out.minimumZSubvoxels = static_cast<std::int64_t>(plan_->bottomChunkZ) * recipe_->layoutChunkWidthSubvoxels;
|
||||
out.maximumZSubvoxels = (static_cast<std::int64_t>(plan_->topChunkZ) + 1) * recipe_->layoutChunkWidthSubvoxels;
|
||||
out.minimumZSubvoxels = static_cast<std::int64_t>(plan_->bottomChunkZ) * 32 * WorldAssemblySubvoxelsPerVoxel;
|
||||
out.maximumZSubvoxels = (static_cast<std::int64_t>(plan_->topChunkZ) + 1) * 32 * WorldAssemblySubvoxelsPerVoxel;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -2852,24 +2264,6 @@ FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept
|
||||
{
|
||||
FieldDetailEnvelope 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
|
||||
+plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels;
|
||||
std::int64_t minimumFeature = std::numeric_limits<std::int64_t>::max();
|
||||
@@ -3005,7 +2399,6 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block
|
||||
&result.capBoxes,false,"cap");
|
||||
}
|
||||
bool hasCliffProfile=false;
|
||||
if(recipe->surfaceProfile.version==0)
|
||||
for(const auto& biome:recipe->biomes)hasCliffProfile|=biome.cliffStrengthQ16>0;
|
||||
if(hasCliffProfile)
|
||||
{
|
||||
@@ -3046,8 +2439,7 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block
|
||||
// Controle negatif : cette plage unilaterale exclut un echantillon connu; l'audit doit rejeter cette borne mensongere.
|
||||
const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2};
|
||||
result.negativeControlDetected = !world_assembly_detail::InInterval(deliberatelyWrong, controlPoint);
|
||||
const bool borderCoverage=recipe->surfaceProfile.version!=0
|
||||
||recipe->biomes.size()<2||recipe->biomeBorderBlendSubvoxels==0
|
||||
const bool borderCoverage=recipe->biomes.size()<2||recipe->biomeBorderBlendSubvoxels==0
|
||||
||result.biomeBorderBoxes>0;
|
||||
const bool cliffCoverage=!hasCliffProfile||result.cliffBoxes>0;
|
||||
const bool exceptionCoverage=std::none_of(plan->features.begin(),plan->features.end(),
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
# VoxelForge strata, units, and generation layers
|
||||
|
||||
**Design round L20 — 2026-10-20**
|
||||
|
||||
## Summary
|
||||
|
||||
Author strate layout in metres and give each strate a voxel size. Keep Core's internal tile at 32³
|
||||
voxels; its physical width follows that strate's voxel size and the tile's LOD. Use one director to
|
||||
resolve the season, layout, seed, generator recipe, resolution, and interfaces before workers start.
|
||||
Generators then evaluate an immutable, common field contract. A shared composition layer applies
|
||||
strata boundaries, gaps, seals, edits, caches, and proofs. The mesher consumes that composed field
|
||||
and generic surface hints; it should not branch on SurfaceWorld, FloatingIslands, or feature type IDs.
|
||||
|
||||
The code already has parts of this shape, but not the boundaries between them. In particular,
|
||||
`FStandaloneShapeWorld` is both a dispatch layer and a collection of field adapters, while
|
||||
`World::generateTile` contains generator-aware far-surface and feature refinement. The Unreal
|
||||
adapter currently translates two different meanings of “chunk” into Core coordinates.
|
||||
|
||||
The L19 ledges are not explained by a different density recipe per LOD. The ring audit found
|
||||
upward-facing terrain from the same lower SurfaceWorld field at different extracted heights on
|
||||
adjacent LODs. At the default 25 cm Core voxel size, LOD1, LOD2, and LOD3 sample at 0.5 m, 1 m,
|
||||
and 2 m. The maximum LOD1→2 ring difference is 1.49862 m; LOD2→3 reaches 7.73318 m, with 8
|
||||
no-hit samples among 308. An explicit shared surface contract can give all LODs common boundary
|
||||
geometry and a controlled transition band. A planar ceiling alone will not fix these ground ledges.
|
||||
|
||||
One conversion issue needs an explicit preservation target. The current Core path maps one
|
||||
SurfaceWorld slot chunk to 32 Core voxels (8 m at 25 cm), but one FloatingIslands slot chunk to
|
||||
32 owner voxels at 12.5 cm (4 m). Converting each current interpretation independently into metres
|
||||
can preserve each field's current positions, but it does not make shared chunk indices one common
|
||||
physical boundary. The converter must record the actual per-strate bounds and any resulting gap or
|
||||
overlap before choosing a unified interface. Do not promise a no-movement migration from a single
|
||||
global “metres per chunk” constant.
|
||||
|
||||
Metres are not a runtime cost concern. Resolution is. Halving voxel spacing in all three axes means
|
||||
about eight times as many density samples for the same physical volume, plus more tiles, queue work,
|
||||
and cache entries because each internal tile remains 32³ voxels. With 25 cm as the default for all
|
||||
strates, the proposed starting resolution matches today's Core sampling resolution.
|
||||
|
||||
## 1. Current code mapped to the three layers
|
||||
|
||||
There is no single class called “director” today. The director work is split between Unreal's
|
||||
season/layout code, the Core adapter, and Core's tile scheduler.
|
||||
|
||||
| Intended layer | Current location and symbols | Current responsibility and boundary issue |
|
||||
|---|---|---|
|
||||
| **Director / orchestrator** | `VoxelStrateComposer.cpp`; `VoxelSeasonManifest.cpp`; `VoxelStrateManager.cpp` (`Initialize`, `GetGeneratorTypeForChunk`, `GetGenerationParams`); `VoxelWorld.cpp` (`BuildDesiredTiles`, tile scheduling, epochs); `VoxelForgeCoreAdapter.cpp` (`BuildSurfaceStratumPlan`, `BuildFloatingIslandStratumPlan`, `AttachValidatedWorldAssembly`) | Composes season params and stacked slots, resolves generator kind and seed, and serializes the Core snapshot. `WorldAssemblyManifest`, `WorldAssemblyPlanV1`, and their `FromJson`/`finalize`/`validate` functions in `WorldAssemblerInternal.h` are the current plan/manifest format. Resolution and bounds are not fully resolved here: Core re-derives them using generator-specific conversions. `TopWorldZ`, `BottomWorldZ`, `HeightInChunks`, and `InterStrateGapChunks` remain chunk/voxel-coordinate based in owner-facing data. |
|
||||
| **Generators** | SurfaceWorld: `Heightfield2D` in `VoxelForgeCore.h` and `WorldAssemblerInternal.h`; plan construction in `VoxelForgeCoreAdapter.cpp`. FloatingIslands: `FloatingIslandFieldV1` in `VoxelForgeCore.h` and `FloatingIslandsInternal.h`; plan construction in the adapter. Legacy: `UVoxelGenerator::GetDensityAt` in `VoxelGenerator.cpp`, `VoxelDensityOpStack.cpp`, and `VoxelCaveMorphology.cpp`. | SurfaceWorld and FloatingIslands have distinct plan and evaluator APIs. Legacy generation still asks the manager for the current chunk's params and generator type. In Core, the “generator” boundary is obscured by `FStandaloneShapeWorld`, which selects native fields or falls back to the copied owner generator. |
|
||||
| **Shared field / composition library** | Noise and fixed-point evaluation: `WorldAssemblerInternal.h`, `FloatingIslandsInternal.h`. CPU/HLSL far-field math: `VoxelForgeFieldShared.h`. FP contract: `CoreNamespacePreamble.h`. Feature culling and caches: `BuildWorldAssemblyFeatureTileListV1`, `ScopedWorldAssemblyFeatureQueriesV1`, `SharedColumnCache.h`, `ThreadLocalStorage.h`. Field bounds/proofs: `Heightfield2D::interval`, FloatingIslands bounds, `FStandaloneShapeWorld::ClassifyBoxDetailed` and `BuildShapeDependencyProof`. Density seals: `VoxelDensityPrimitives.h`. | Useful shared pieces exist, but only for subsets of recipes. Noise is not one library used by every legacy and native generator. Bounds, density, and output identity still use subvoxels of a Core voxel. The canonical stratum bounds descriptor `FResolvedStratumBounds` is private to `ShapeWorldInternal.h`, not part of the plan contract. `AppendStratumBoundaryCeilings` is under far-shell/meshing code, even though the boundary meaning is shared composition policy. |
|
||||
| **Mesher** | `UVoxelMarchingCubesMesher::GenerateMesh` in `VoxelMarchingCubesMesher.cpp`; dispatch and mesh assembly in `World::generateTile` in `VoxelForgeCore.cpp`; `FarShellsInternal.h`; hybrid sharp support in `VoxelHybridSharpMesher.h` and `VoxelMarchingCubesMesher.cpp`. | MC, skirts, far shells, connector repair panels, LOD refinement, and cap repair are spread across the production mesher and Core tile builder. `World::generateTile` scans recipe feature type IDs, refines type-4 shelves only at LOD0–2, selects a native SurfaceWorld horizon, and invokes separate ground/cap repair. `FStandaloneShapeWorld::BuildFarFieldTileInput` constructs FloatingIslands connector panels. These are recipe-aware mesh paths. |
|
||||
|
||||
### What crosses a boundary today that should not
|
||||
|
||||
- `FStandaloneShapeWorld` reads `UVoxelStrateManager`, `FStrateSlot`, `WorldAssemblyManifest`, legacy
|
||||
generator params, and generator type. It selects a recipe, evaluates a field, resolves its bounds,
|
||||
applies a seal, and builds GPU far-field input. That combines director, generator adapter, shared
|
||||
field, and renderer responsibilities.
|
||||
- `World::generateTile` knows `WorldAssemblyPlanV1`, SurfaceWorld heightfields, feature numeric IDs,
|
||||
and FloatingIslands presence. The generic mesher should receive capabilities and surface data,
|
||||
not know those recipe identities.
|
||||
- The adapter exports one layout but encodes physical positions in `WorldAssemblySubvoxelsPerVoxel`.
|
||||
That makes a physical length's fixed integer depend on the selected Core voxel size.
|
||||
- Core `WorldAssemblyStratumPlanV1` carries `topChunkZ`/`bottomChunkZ`; validation in
|
||||
`WorldAssemblerInternal.h` multiplies those values by `32`. FloatingIslands additionally stores
|
||||
its own owner-to-Core size ratio and metric-equivalent bounds in Core-voxel units.
|
||||
- The Core boundary path calls `GetResolvedStratumBounds` differently for SurfaceWorld and
|
||||
FloatingIslands. The director should resolve a single physical boundary record once.
|
||||
- Legacy density still reaches into manager-owned params and operations. It must become a generator
|
||||
adapter that consumes the same resolved request as a native recipe, or remain explicitly marked as
|
||||
a compatibility route until it can be ported.
|
||||
|
||||
## 2. Violations, ranked by risk
|
||||
|
||||
Rank 1 is the highest risk to correct physical layout and cross-LOD output.
|
||||
|
||||
| Rank | Risk | Violation and evidence | Why it matters |
|
||||
|---:|---|---|---|
|
||||
| 1 | **Critical** | **No single physical bounds/interface contract.** `FResolvedStratumBounds` in `ShapeWorldInternal.h` derives SurfaceWorld bounds from `Slot.*ChunkZ × 32`, but derives native FloatingIslands bounds from `layout*ZSubvoxels` and an owner/Core scale ratio. `GetResolvedStratumBounds` is then reused by field classification, far-field protection, and far ceiling generation. | A shared logical slot boundary can map to different metre positions depending on generator. This can make an apparent seam a gap, overlap, or misplaced terrain surface before the mesher samples it. It also makes “preserve the old heights” ambiguous unless the old per-field physical bounds are recorded. |
|
||||
| 2 | **Critical** | **LOD extraction has no required shared surface representation.** `World::generateTile` uses normal MC sampling for LOD1–3; native SurfaceWorld ground refinement is only invoked at LOD4+. L19's strict ring probe excludes skirts and masked triangles and measured 149.862 cm max at LOD1→2 and 773.318 cm max at LOD2→3. | A coordinate-stable density function does not guarantee equal extracted surfaces when sample grids differ. Thin seals can be skipped, and a heightfield root can shift with coarse XY/Z samples. This is the measured ledge failure. |
|
||||
| 3 | **High** | **Chunk, owner-voxel, Core-voxel, and fixed-point units are mixed.** `WorldAssemblyFixed` in the Unreal adapter serializes lengths as 1/256 Core voxel. SurfaceWorld plan bounds use `Slot.BottomChunkZ * 32` and `(TopChunkZ-BottomChunkZ+1)*32`. FloatingIslands uses `32 × ownerToCoreScaleNumerator / denominator`. `ShapeWorldInternal.h` reverses Core Z to legacy chunk coordinates with `/ GetCoreVoxelsPerOwnerChunk()`. | A parameter's physical interpretation depends on generator and runtime Core size. `/32`, `*32`, and scale-ratio logic are reasonable only in a single boundary adapter or for the internal fixed tile; they are incorrect as the authored layout contract. |
|
||||
| 4 | **High** | **The Unreal adapter and manager both resolve the same layout.** `VoxelStrateManager.cpp` creates chunk slots and per-generator runtime params; `VoxelForgeCoreAdapter.cpp` re-reads those params and builds distinct native plans; `ShapeWorldInternal.h` maps Core samples back through the manager again. `WorldAssemblyStratumPlanV1::topChunkZ/bottomChunkZ` are checked against the manager in `AttachValidatedWorldAssembly`. | The runtime can validate matching integer slots without validating that the resulting physical bounds mean the same thing for both generators. The future adapter should serialize already-resolved metres and per-strate resolution. |
|
||||
| 5 | **High** | **Generators read director state.** Legacy `UVoxelGenerator::GetDensityAt` asks the manager for type/params and builds/refetches operator stacks. Core native lookup also asks the manager which generator applies at a Core sample. | The generator cannot be a pure evaluator of a resolved recipe. It is hard to cache, audit, or replay independently, and unit conversion is repeated in worker code. |
|
||||
| 6 | **High** | **Boundary behavior is split between recipe and shared paths.** FloatingIslands geometry v3 special-cases top seal evaluation in `NativeFloatingIslandDensityAt`; older geometry versions retain two-sided seals. `VF_ApplyBoundarySeal`/`VF_ApplyUpperBoundarySeal` provide shared density math, while `AppendStratumBoundaryCeilings` separately patches missing mesh coverage. SurfaceWorld also owns a heightfield cap. | Density closure, boundary ownership, and visible cap geometry can disagree. A shared operator alone does not guarantee a shared, LOD-stable surface. Preserve versioned historical fields during migration, then route new plans through one contract. |
|
||||
| 7 | **Medium-high** | **Mesher contains recipe policy.** `World::generateTile` has SurfaceWorld native-horizon selection, a SurfaceWorld-only far skip condition, type-2 sharp boxes, type-4 shelf refinement, separate native ground/cap refinement, and a `farHorizonVersion` identity. `ShapeWorldInternal.h` creates connector panel repairs from FloatingIslands. | A new recipe requires mesher edits and generator details leak into extraction. The mesher should consume explicit surfaces, generic feature constraints, and a requested error/refinement hint. |
|
||||
| 8 | **Medium-high** | **Legacy paths do not share the new generator format.** `UVoxelGenerator`, operator stacks, morphology caches, Core's native `Heightfield2D`, and `FloatingIslandFieldV1` have separate recipe/parameter routes. | “One generator format” is not yet true. A migration wrapper is needed so behavior can be ported in small, hash-checked steps instead of reimplementing all legacy archetypes at once. |
|
||||
| 9 | **Medium** | **Shared helpers and numeric policies are partial.** Core's FP preamble/self-test applies to Core translation units; fixed-point noise is implemented in assembler code, island noise is in a separate implementation, and far CPU/HLSL math has its own shared header. Feature bounds/caches are more reusable than the field interfaces. | Keep the tested Core determinism contract, but promote the shared numeric/noise primitives only where their exact semantics can be versioned. “Shared” code is not automatically cross-platform identical if separate float evaluators remain. |
|
||||
|
||||
### Unit measurements relevant to migration
|
||||
|
||||
`CHUNK_SIZE` is 32 and `VOXEL_SIZE` is 12.5 cm, so the owner's legacy chunk is 4 m
|
||||
([`VoxelTypes.h`](../Source/VoxelForge/Public/VoxelTypes.h), [`Round5`](Research/WorldAssemblerM3/Round5/REPORT.md)). The current default Core voxel is 25 cm. In the current native Core routes:
|
||||
|
||||
| Route | Current conversion | Physical chunk width at default | Evidence |
|
||||
|---|---|---:|---|
|
||||
| SurfaceWorld | `32` Core voxels per slot chunk | 8 m | `BuildSurfaceStratumPlan` uses chunk bounds directly in Core voxel units; L18 records the preserved `topChunkZ × 32` mapping. |
|
||||
| FloatingIslands | `32 × 125000/250000 = 16` Core voxels per owner chunk | 4 m | `BuildFloatingIslandStratumPlan` serializes the ratio and applies it to layout bounds; `GetCoreVoxelsPerOwnerChunk` uses the same ratio. |
|
||||
|
||||
This confirms the claimed factor of two. It does **not** prove that this conversion alone caused
|
||||
the L19 ring delta. L19 isolated the observed step to coarse extraction of lower SurfaceWorld
|
||||
terrain, and the reported LOD4+ horizon refiner cannot affect those LOD1→2 and LOD2→3 values.
|
||||
|
||||
## 3. Generator contract and one-time interface handling
|
||||
|
||||
### What the director resolves before generation
|
||||
|
||||
The director should publish an immutable `ResolvedWorld` (exact type/schema is an implementation
|
||||
choice) containing:
|
||||
|
||||
- a stable world/season identity and seed;
|
||||
- an ordered list of strates with a stable ID, generator kind and recipe version;
|
||||
- each strate's physical minimum and maximum bounds in metres, including height as their difference;
|
||||
- a voxel size in metres per strate (25 cm default for every strate);
|
||||
- route points, island anchors/connectors, and all top/bottom/ceiling positions in the same physical
|
||||
coordinate space;
|
||||
- one resolved interface record per adjacent pair, including any explicit void, bedrock gap,
|
||||
sealed contact, upper cap, passage, or open boundary;
|
||||
- any season-level values that affect world output, such as routes, islands, water datum, or biome
|
||||
selection, already attached to their owning strate or interface;
|
||||
- a canonical identity over the sorted resolved layout, seed, resolution map, recipes, and interface
|
||||
records.
|
||||
|
||||
Do not make generators ask the director for the active `FStrateSlot`, chunk number, season, or
|
||||
neighbour generator while evaluating a point.
|
||||
|
||||
### Request to every generator
|
||||
|
||||
A worker request should give the generator:
|
||||
|
||||
1. The immutable resolved recipe and stable stratum ID.
|
||||
2. Physical query/tile bounds in metres, including the exact halo requested by the mesher.
|
||||
3. The canonical sample coordinates or a clearly defined lattice origin and step. Voxel coordinates
|
||||
are useful as an internal derived lattice, but metres/physical fixed units are the identity space.
|
||||
4. The resolved per-strate voxel size and seed. LOD can be included as a sampling hint; the canonical
|
||||
field value itself must not change just because the caller chose another LOD.
|
||||
5. Read-only interface descriptors that touch the request bounds. Pass boundary records, not a
|
||||
pointer back to the neighbour's manager or mutable generator.
|
||||
|
||||
### Result from every generator
|
||||
|
||||
The common result should provide a deterministic field evaluator and its declared capabilities:
|
||||
|
||||
- signed density using one documented solid/air sign convention;
|
||||
- optional material or surface-class data;
|
||||
- conservative interval/classification proofs and conservative feature/influence bounds, or an
|
||||
explicit “unknown, evaluate” result;
|
||||
- optional semantic 2D surfaces such as a ground heightfield or a cap, in physical coordinates;
|
||||
- a detail envelope / maximum safe sample spacing hint. It is a hint to generic LOD/refinement
|
||||
policy, not permission for a recipe to choose its own mesh path;
|
||||
- generator/schema/evaluator versions for stable plan and output identity.
|
||||
|
||||
Keep the established fail-open behavior: an unknown interval or influence bound causes evaluation,
|
||||
not a solid/air skip. Existing feature candidate lists and worker caches fit this contract as
|
||||
optimizations beneath the evaluator.
|
||||
|
||||
### Interfaces, seals, caps, and voids belong to shared composition
|
||||
|
||||
The director resolves the meaning and exact physical location of each interface once. The shared
|
||||
composition layer then combines generator fields and applies boundary semantics in a fixed order:
|
||||
|
||||
1. Evaluate each generator only for its declared physical domain, retaining its ordinary
|
||||
field/feature behavior.
|
||||
2. Apply the interface contract once: for example, a physically measured seal band, a cap surface,
|
||||
open void, bedrock gap, or permitted passage. The contract declares ownership, sidedness, material
|
||||
class, collision/support policy, and whether player edits may cut it. A gap is data, not an
|
||||
accidental “no generator found” branch.
|
||||
3. Apply world edits through the canonical composed field, so both sides see the same edit in
|
||||
physical space.
|
||||
4. Give the resulting field, proofs, and any explicit surfaces to the mesher.
|
||||
|
||||
This centralizes the semantics currently spread across generator density wrappers,
|
||||
`FResolvedStratumBounds`, and `AppendStratumBoundaryCeilings`. Existing versioned manifests still
|
||||
need compatibility adapters: do not silently reinterpret old `boundarySealSubvoxels` values.
|
||||
|
||||
For two different generator kinds that meet at a plane, both should reference the same stable
|
||||
interface ID and exact physical boundary. Their density fields can remain independent; shared
|
||||
composition owns the boundary rule. If the plan intentionally has open space between them, the
|
||||
resolved layout records that metre-valued interval explicitly.
|
||||
|
||||
## 4. Why LOD changes thin features, and the interface surface contract
|
||||
|
||||
MC samples density at lattice corners. A thin seal or cap can be narrower than one coarse cell, so
|
||||
the coarse grid may see no sign change at all. If it does see a crossing, interpolation uses the
|
||||
coarse cell's endpoint values; its root can differ from the finer grid's root. A shared analytic
|
||||
density function guarantees the same values at the same physical coordinates, not the same triangles
|
||||
from different sample grids.
|
||||
|
||||
For a physical strate boundary, use an explicit surface descriptor with exact height/shape in metres
|
||||
and a globally stable sample phase. Every LOD must use common anchors along tile and LOD transition
|
||||
edges. The mesher can fill the interior at its chosen rate, then build a deterministic transition
|
||||
band from those common anchors into its coarser interior. A seal/cap should not rely on a sub-cell
|
||||
thin density strip surviving arbitrary MC sampling.
|
||||
|
||||
For the L19 lower-ground ledge, the same rule needs a SurfaceWorld ground profile available to the
|
||||
mesher as semantic surface data. A shared planar boundary cap would only close the strate ceiling;
|
||||
it would not align these upward-facing ground triangles. The surface can be band-limited by LOD, but
|
||||
the adjacent LODs must share their transition samples and be checked against a physical error
|
||||
tolerance. If that cannot be met with a transition band, refine only the affected boundary tiles.
|
||||
|
||||
The costs are extra boundary vertices/triangles and some CPU work to construct common transition
|
||||
strips. Collision/support must consume the same interface representation where that surface is
|
||||
collision-relevant. The owner would see caps and ledges hold one physical height as the camera moves
|
||||
between LODs, instead of steps or holes appearing at the ring. Authoring remains in metres; internal
|
||||
tile and transition details stay hidden.
|
||||
|
||||
This is not free geometry, but it is bounded to boundaries and LOD transition bands. L19 has already
|
||||
shown why “add a thin density seal and sample it normally” is not a sufficient all-LOD guarantee.
|
||||
|
||||
## 5. Keep the per-strate resolution door open
|
||||
|
||||
Yes. A 25 cm per-strate default preserves the current Core sample spacing, and the common physical
|
||||
field coordinate contract leaves room for later different sizes. Do not enable mixed resolutions
|
||||
until the Round5 boundary contract is met:
|
||||
|
||||
1. Canonical physical-space field queries and explicit ownership at a shared boundary.
|
||||
2. Deterministic transition meshes with common LOD ancestry on both resolutions.
|
||||
3. Physical-space edit rasterization/replay that reaches both grids exactly once, with no holes or
|
||||
double application.
|
||||
4. A stable, canonically sorted strate-ID-to-size map included in plan/season identity and border
|
||||
hashes.
|
||||
5. Streaming and collision rules for tiles that touch or span two resolutions.
|
||||
|
||||
At today's one-size setting, the size map has one value (250,000 microns) for every strate. It adds
|
||||
no mixed-resolution mesh, edit, or streaming cost. Tile width remains `32 × voxel size × 2^LOD`;
|
||||
different future sizes change physical tile width and workload, but never change the 32³ internal
|
||||
tile format.
|
||||
|
||||
## 6. Migration plan with “worlds don't move” checks
|
||||
|
||||
The checkpoints distinguish expected equality from a deliberately changed interface mesh. A plan
|
||||
or schema identity can change even when field output remains equal; record both identities and mesh
|
||||
hashes separately.
|
||||
|
||||
| Step | Work | Expected equality / check | Changes output? / Unreal PC? |
|
||||
|---:|---|---|---|
|
||||
| 0 | Freeze the L19 evidence as the baseline: W12, Clang with FP contraction off; retain the whole-view hashes, full-detail hashes, strict ceiling audit, captures, and FI/SW ring tables. | Baseline hashes recorded below; pin the protected SW LOD0–2 hashes too. Capture byte/pixel comparisons and ring samples must retain location, LOD, and skirt exclusion. | No. No new build needed for this design round. |
|
||||
| 1 | Add a versioned resolved-metre layout representation and a read-only conversion/audit path. The audit reports, per strate, old input units, resolved old physical bounds, new metre bounds, voxel size, interfaces, and identity. Keep the old evaluator active. | Exact old-vs-new resolved positions at all authored top, bottom, height, route, island, and cap anchors. Old Core density/mesh/payload/topology hashes remain equal where the conversion is exactly representable; otherwise list the precise quantization delta before accepting a new baseline. | No. Core parser/audit can be Linux-only; adapter export needs Unreal BuildHost on the owner's PC. |
|
||||
| 2 | Change owner-facing layout authoring and season serialization to metres. Convert each old slot using its **current resolved physical bounds**, not one global chunk multiplier. Represent any historical gap/overlap explicitly. Default each strate's voxel size to 25 cm. | Same physical top/bottom/height values and field anchors before/after. Run default W12 hashes and matched captures. The report must call out any interface whose two old generator bounds disagree instead of silently forcing them together. | Intended no. Touches `VoxelForgeCoreAdapter.cpp`, `VoxelStrateManager`, season manifest/composer and likely `VoxelWorld`; compile and actor validation require the owner's Unreal PC. |
|
||||
| 3 | Add the common generator request/result contract and adapt `Heightfield2D` and `FloatingIslandFieldV1`; wrap the legacy `UVoxelGenerator` route without moving it yet. Move manager lookups out of evaluator calls in stages. | Per-strate exact density queries, proof classifications, full-detail hashes, whole-view hashes, W1/W8/W12 determinism, and generator-to-legacy route parity. Existing current output hashes remain equal for adapters that preserve their exact physical recipe. | No intended visual change. Unreal adapter/legacy wrappers need owner-PC UHT/build and actor validation; native Core pieces can be audited on Linux. |
|
||||
| 4 | Resolve interface records once in the director; route new seal, cap, void, bedrock-gap, and passage behavior through shared composition. Keep old manifest versions on compatibility adapters. | Preserve density hashes where only a mesh closure was missing. Run the strict LOD0–8 ceiling interior/perimeter audit, edge pairing, same-LOD border hashes, matched captures, and field proofs. Any newly closed boundary is a named expected mesh/payload/topology delta. | Affected boundary mesh may change. Adapter serialization needs owner-PC validation; Core audit can run here. This step fixes thin seals/caps, not the L19 ground ledges. |
|
||||
| 5 | Add the generic explicit-surface and LOD-transition contract. Feed SurfaceWorld ground-profile data into common transition bands; prototype on the measured FI/SW interface. | Produce before/after matched readable captures and full ring tables. Require zero no-hit interface samples and a chosen physical seam-error limit; retain L19 hash equality for unaffected tiles/LODs. For intended changes, list changed mesh/payload/topology hashes and keep density unchanged if the field itself is unchanged. | **This is the step that fixes the L19 ledges.** It changes protected SurfaceWorld LOD2 output, so implement only after the owner approves that specific output change. Unreal capture/collision review is on the owner's PC. |
|
||||
| 6 | Enable a per-strate size map behind the already-resolved contract, initially set to 25 cm on all strates. Add one non-default diagnostic size case only after the default remains green. | At all-25-cm map, reproduce the approved default hashes and captures. Include sorted size-map identity in a negative control; boundary/transition hashes must change when one size changes. Re-run edit-across-boundary, collision, streaming, and W1/W8/W12 checks. | No default change; non-default diagnostic intentionally changes sampling. Touches Unreal reflected settings, snapshot adapter, and world identity; full compile/run requires the owner's PC. |
|
||||
|
||||
### L19 reference hashes
|
||||
|
||||
Representative W12 Clang `-ffp-contract=off`, density / mesh / payload / topology:
|
||||
|
||||
| Whole view | L19 hash |
|
||||
|---|---|
|
||||
| SurfaceWorld | `c10697a8f48380c7 / 4197fc1072f0ff58 / 7b3e616ec9459851 / c879e35592bf33e5` |
|
||||
| FloatingIslands | `00bc3607a7839854 / 4baa66127c2b0892 / e11ef05e851a1517 / 1278bc4b1fffb869` |
|
||||
|
||||
Protected SurfaceWorld LOD0–2 hashes are in the L19 report and should be retained through steps
|
||||
1–4 except for explicitly approved boundary geometry. The whole-view hash gate alone cannot prove
|
||||
that a ring improved; step 5 needs the strict ring table and readable captures as well.
|
||||
|
||||
### Owner decisions this design needs
|
||||
|
||||
1. **Preservation target:** confirm that migration snapshots each current generator's resolved
|
||||
physical bounds (including any current overlap or gap) before later tuning them into a single
|
||||
authored metre layout. If instead the initial migration should force currently different bounds
|
||||
to meet, that is a deliberate world change and needs its own baseline/capture review.
|
||||
2. **Ledge output:** approve the specific SurfaceWorld LOD2/transition change before step 5
|
||||
implementation, as required by the existing protected-output rule.
|
||||
|
||||
@@ -83,12 +83,6 @@ Costs below are design estimates, not timings. Low means bounded analytic featur
|
||||
|
||||
A passage planner must produce a traversable route or pair a non-walkable visual passage with another guaranteed route. A narrow crack or drop cannot be assumed walkable because its centerline is air.
|
||||
|
||||
For a SurfaceWorld strate, the supported arrival-to-departure route follows the sampled terrain
|
||||
ground and stays within the mouth placement bounds. The inter-strate departure passage owns the
|
||||
descent from that ground mouth through rock and bedrock. Treating the bottom of a tall SurfaceWorld
|
||||
strate as the route's departure height would ask the walk planner to cross open terrain air; it is
|
||||
the wrong endpoint for the intra-strate route.
|
||||
|
||||
| Passage type | Planner output | Bound and likely cost |
|
||||
|---|---|---|
|
||||
| SlopedTunnel | Straight-style path with switchbacks as needed, capsule clearance, supported floor, maximum grade and landing geometry. | Swept capsule bounds plus floor envelope; low–medium. |
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
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.
|
||||
@@ -1,133 +0,0 @@
|
||||
# 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].
|
||||
@@ -1,130 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,137 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,115 +0,0 @@
|
||||
# Linux10 — startup and region crossing
|
||||
|
||||
**Result:** no world-generation optimization was shipped. The dominant spawn gate is full shape compilation; the current Core API does not let tiles consume a partially built shape safely. L10 adds opt-in phase and crossing telemetry to the headless research path.
|
||||
|
||||
Runs used CPUs 0–11, normal priority, at most `-j12`, a 6 GiB address-space cap, and the unchanged `powersave` governor. Peak monitored RSS was 624,996 KiB (610.3 MiB). Final builds: Clang 19 with FP contraction off, SHA-256 `7d5360e2baa637f8b2b4679df9616580bffe689afd101470e311260f0ecd4975`; GCC 14 default, `5041cf15019766f453eb59fe8ff7fc4cd98ca6a904208b645e3d7ca4584e1195`. The L9 Clang timing/hash baseline is `Standalone/bin/linux/VoxelForgeCoreHeadless-linux9-clang`, SHA-256 `0cd3f0ef993ced2c615418c6e2b37b83f57eff6d024d7d1568c30ea51490384a`.
|
||||
|
||||
## 1. Startup phases
|
||||
|
||||
SurfaceWorld seed 0, W12, no hash; five runs on the optimized Clang build. Wall and process CPU are medians. Process CPU sums work across threads. First-tile time is from cold start; its CPU column is CPU since pool start.
|
||||
|
||||
| Phase | Wall ms | Process CPU ms | Threads used |
|
||||
|---|---:|---:|---|
|
||||
| Owner data load and resolve | 5.05 | 5.05 | 1 |
|
||||
| Assembly plan build | 51.04 | 51.03 | 1 |
|
||||
| Core snapshot | 0.53 | 0.54 | 1 |
|
||||
| First desired-set build, 2,553 tiles | 0.74 | 0.74 | 1 |
|
||||
| Shape request setup | 0.03 | 0.03 | 1 |
|
||||
| Shape compile | 222.59 | 856.79 | caller + up to 8 scheduler workers |
|
||||
| Streamer setup | 0.01 | 0.01 | 1 |
|
||||
| Worker-pool start | 0.41 | 1.27 | 12 requested; 8–11 had entered at print |
|
||||
| First tile starts at 299.62 ms | — | 1.59 since pool start | 12/12 entered |
|
||||
|
||||
Shape-compile wall ranged 215.89–324.88 ms; first tile started at 286.56–409.76 ms. Owner load, plan, local shape setup, and far-parent construction are serial. Before the pool starts, there are no sleeping stream workers, but most of the 12 allowed CPUs are unused. During the interval and detail-band phases the scheduler uses at most 8 workers, leaving up to 4 CPUs unused.
|
||||
|
||||
The phase-trace mode’s whole-view median was 1.436 s, versus 1.372 s with tracing off. The phase runs are for decomposition only; the performance ranking and A/B below use the optimized build with tracing disabled and hashing off. In that flag-off path, shape wall time was 0.222 s median, consistent with the phase trace.
|
||||
|
||||
## 2. Shape contents and first-tile dependencies
|
||||
|
||||
| Shape phase | Wall ms | Process CPU ms | Work / concurrency |
|
||||
|---|---:|---:|---|
|
||||
| Local owner shapes and spatial bins | 40.46 | 40.44 | Extracts owner primitives; 1 thread |
|
||||
| Rock intervals, signs, candidate bands | 40.20 | 187.14 | Packed column signs and candidate masks; up to 8 workers |
|
||||
| Detail band and sign-edge distances | 81.00 | 568.78 | Applies coherent detail to candidate nodes and builds distance proxies; up to 8 workers |
|
||||
| Route validation and shared-field setup | 1.12 | 1.13 | 5,642 passage samples; then shared primitive form |
|
||||
| Far parents and bins | 55.56 | 55.57 | 6,050 far rooms / 18,348 links; 1 thread |
|
||||
|
||||
The compile extracts 18,348 owner primitives (71 rooms, 74 tunnels, 2 passages), covers 138,188,413 sign nodes, allocates 36.71 MiB of packed words and 263.57 MiB of proxy address capacity, and visits 214,538 detail candidates (6,098 flips).
|
||||
|
||||
For this SurfaceWorld fixture, the first LOD0 tile’s density queries take the native `Heightfield2D` path before packed-shape lookup. Far-parent shapes are not needed by that first tile. However, `World::generateTile` rejects requests until the full base shape is marked built, and `compileTileRegion` also requires that base shape. So the current API gates the near tile on the interval, detail, validation, and far-parent work even when that tile does not sample those arrays. Other recipes and fallback/query paths do use the analytic primitives and packed signs/proxies.
|
||||
|
||||
There is no seed-independent owner shape payload to cache at build time. The assembly plan is built from owner data; region pages also re-run owner primitive extraction, spatial-bin construction, and shared-field packing. Those immutable owner-derived structures are a possible per-world reuse target, but the measured short crossings below built no pages, so this run does not price that work.
|
||||
|
||||
## 3. Moving crossing
|
||||
|
||||
Hashing off, W12, same camera and already-streamed initial view. Region compile was called, but all entering fine keys were covered by the initial shape bounds; it built zero regions.
|
||||
|
||||
| Camera step | New tiles | LOD breakdown | Region compile | Tile work wall | Crossing total |
|
||||
|---|---:|---|---:|---:|---:|
|
||||
| 8 m | 144 | LOD2: 128, LOD3: 16 | 0.010 ms | 12.61 ms | 12.81 ms |
|
||||
| 16 m | 288 | LOD0: 128, LOD1: 16, LOD2: 128, LOD3: 16 | 0.010 ms | 60.49 ms | 60.68 ms |
|
||||
|
||||
The 16 m crossing’s worker-time sum was 633 ms; its wall time was 60.5 ms. No plan or full shape compile repeated. A 24 m attempt terminated with `std::bad_alloc` after base shape compilation and before crossing metrics; the failing allocation site is unknown.
|
||||
|
||||
## 4. Ranked levers
|
||||
|
||||
| Rank | Lever | Upper bound from these measurements | Assessment |
|
||||
|---:|---|---|---|
|
||||
| 1 | Overlap near-tile work with shape compilation | 222.6 ms, 16.2% of 1.372 s whole-view-ready; first tile could start earlier by up to the compile time | Largest opportunity. Not built: tiles and region compilation currently require a fully built base shape. A safe version needs immutable partial-page publication and a wait before any tile reads an unfinished sign, proxy, primitive bin, or far field. |
|
||||
| 2 | Defer far-parent construction until far tiles need it | 55.6 ms, at most 4.1% of whole-view-ready and 18.6% of the first-tile delay | Could improve spawn, but far and near paths share mutable spatial-bin state today. Needs separate immutable bins and a synchronized readiness state; no crossing benefit was measured. |
|
||||
| 3 | Parallelize assembly-plan construction | 51.0 ms, at most 3.7% of whole-view-ready | Serial and seed/owner-specific. No safe deterministic parallel split was established. |
|
||||
| 4 | Raise shape scheduler cap from 8 to 12 | About 121 ms currently lies in the two worker phases; ideal linear scaling saves about 40 ms (2.9% of whole-view-ready) | Below the 3% bar before overhead, and no crossing benefit. Not built. |
|
||||
|
||||
For any future partial or deferred build, hashes can fail if a tile observes a partially written sign/proxy array, if region publication races with tile lookup, or if shared bins change while a tile is sampling. The page must be immutable before publication; worker-count and timing tests must compare W1 with W12 and repeat W12. Any asynchronous result must also follow the Epoch/MPSC shutdown-drain rules.
|
||||
|
||||
## 5. Final-build timing and hashes
|
||||
|
||||
Five interleaved W12 runs, no hash. Pair values are `whole_view_ready / stream_wall` in seconds.
|
||||
|
||||
| Pair | L9 `7650abb` Clang | L10 Clang | Delta, ms |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | 1.336 / 1.050 | 1.346 / 1.060 | +10 / +10 |
|
||||
| 2 | 1.351 / 1.062 | 1.365 / 1.073 | +14 / +11 |
|
||||
| 3 | 1.359 / 1.073 | 1.374 / 1.083 | +15 / +10 |
|
||||
| 4 | 1.363 / 1.077 | 1.375 / 1.081 | +12 / +4 |
|
||||
| 5 | 1.387 / 1.089 | 1.372 / 1.092 | −15 / +3 |
|
||||
| **Median** | **1.359 / 1.073** | **1.372 / 1.081** | **+13 / +8** |
|
||||
|
||||
The measurement-only candidate is +1.0% whole-view-ready and +0.7% stream median; this is no demonstrated speedup. Hashing was off for these timings.
|
||||
|
||||
Final rebuilt binaries passed SurfaceWorld and FloatingIslands seed-0 hash checks at W1, W8, and five W12 runs per compiler: Clang FP contraction off and GCC default. Every full-detail, whole-view, LOD0–8, and exact-query row matches the compiler-matched L9 baseline. Audit files: `Saved/Linux/l10_final_surface_hash_audit.txt` and `Saved/Linux/l10_islands_hash_audit.txt`.
|
||||
|
||||
| Recipe / scope | Density | Mesh | Payload | Topology |
|
||||
|---|---|---|---|---|
|
||||
| SurfaceWorld full detail | `0x0ee0579bd15cbbe4` | `0x32e5d4964d610fee` | `0x1932b96ee4b1d910` | `0x69743c6bad3447dc` |
|
||||
| SurfaceWorld whole view | `0xc10697a8f48380c7` | `0xe75c9119a1498cd5` | `0x49a9a8d1835a6c8c` | `0xedcb23288b5e8724` |
|
||||
| FloatingIslands full detail | `0x6d842d2878b08ef6` | `0x4b868a3184f08035` | `0x2cedf2236788752d` | `0xeac17d5e0cb5044a` |
|
||||
| FloatingIslands whole view | `0x5be4d8cde79b2a76` | `0x96f75041f6861b3a` | `0x3700a516cc89d50c` | `0x6b99948d6eab4f21` |
|
||||
|
||||
Exact-query hashes also match: SurfaceWorld density `0xe4d4a8eb95b35933`, detail `0xefcba20c0c70c415`, collision `0xe604823a249029bf`; FloatingIslands density `0x61daded9c303becb`, detail `0xefcba20c0c70c415`, collision `0x84d37463be9b0573`.
|
||||
|
||||
## 6. Failed or retried runs
|
||||
|
||||
- One phase smoke omitted the explicit owner-data path and resolved it under `Standalone/bin/data`; reran with `Standalone/data/owner_world.json`.
|
||||
- A no-camera phase smoke produced 2,370 tiles instead of the 2,553-tile fixture; discarded it and reran with the fixed camera.
|
||||
- A malformed `--workers=w8` command exited during argument parsing; reran with numeric `--workers=8`.
|
||||
- The first FloatingIslands matrix invocation omitted the three required streaming-mode switches. All 14 attempts exited before generation; corrected the command and all 14 final runs passed.
|
||||
- The 24 m crossing attempt failed with `std::bad_alloc` as described above.
|
||||
- The first hash-audit parser expected 13 rows; the actual audit contains 12 scopes. Corrected the parser and all final comparisons passed.
|
||||
|
||||
At spawn, the first tile starts about 300 ms after cold start, with roughly 223 ms in full shape compilation and 51 ms in the single-threaded plan; this round did not make that wait shorter. While moving, the 16 m crossing completed 288 entering tiles in about 61 ms and repeated no shape compile, so the measured delay was tile work rather than region setup.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- **Not committed:** the L10 telemetry code. It is +1.0% whole-view median with tracing off (4 of 5
|
||||
pairs slower), and the rule is zero cost when off. Saved as `Saved/Linux/claude/l10-telemetry.patch`
|
||||
(gitignored) for reuse; the tree is reset to `7650abb`.
|
||||
- **Key finding, for L11:** the first SurfaceWorld tiles take the native `Heightfield2D` path and don't
|
||||
read the cave shape, yet `World::generateTile` waits for the full base shape (~223 ms). A per-tile
|
||||
readiness check (start tiles whose recipe path provably never touches shape arrays) is lever #1 in
|
||||
safe form.
|
||||
- **Open bug, for L11:** the 24 m crossing died with `std::bad_alloc` under the 6 GiB **address-space**
|
||||
cap (`RLIMIT_AS`). That cap counts reservations, not resident memory; L10 also reports 263.57 MiB
|
||||
of proxy address *capacity*. It is not yet known whether this is a real memory blow-up on larger
|
||||
crossings or the cap counting reserved address space. L7's 32 m step also hit the 160M-node
|
||||
interval bound.
|
||||
@@ -1,74 +0,0 @@
|
||||
# VoxelForge L11 — Linux report
|
||||
|
||||
## 24 m crossing crash
|
||||
|
||||
The crash is real. The original binary threw `std::bad_alloc` after base shape compilation with both 6 GiB and 16 GiB `RLIMIT_AS`; the 16 GiB run peaked at 476,128 KiB RSS and 1,542,284 KiB virtual size. The allocation trace identifies a 432,345,551,342,665,824-byte resize in `BuildSpatialBins`, reached from `BuildAnalyticRegion` during `World::compileTileRegion`. The crossing code had coalesced disjoint entering tiles into a large LOD rectangle and tried to allocate dense per-axis bins that the analytic fallback never reads. L10's 263.57 MiB figure is the proxies' reserved virtual capacity; RSS samples record resident usage separately.
|
||||
|
||||
Analytic regions now skip those dense bins and compile in world-aligned 128-voxel pages. Pages are built privately and published together; coordinate, `bad_alloc`, and size failures return errors before partial pages are published. On the final L11 binary, 24 m and 32 m both passed (16 pages each); 1,000 m passed with 2,260 entering keys, 72 pages, 63 ms region-build wall time, 700,232 KiB peak RSS, and 2,578,588 KiB peak virtual size. The fixed 24 m run peaked at 533,420 KiB RSS and 2,578,588 KiB virtual size. Crossing pages build no packed interval nodes (`lattice_nodes=0`) and do not expand the immutable base sign/proxy arrays; they add analytic primitive/bin data. `retainTileRegions` prunes pages outside the desired set. The largest empirically verified step is 1,000 m. Work follows the destination's entering tile set; once the old and new views are disjoint, its page count stays bounded by the active view as travel distance increases. Keys outside Core's signed 32-bit voxel range are rejected with `tile region coordinate is out of range` before page construction. The earlier L7 32 m / 160-million-node failure was the same unbounded region geometry.
|
||||
|
||||
| Step | Entering keys | Pages | Added interval nodes | Base proxy nodes / virtual capacity |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 24 m | 432 | 16 | 0 | 185,024 / 263.57 MiB |
|
||||
| 1,000 m | 2,260 | 72 | 0 | 185,024 / 263.57 MiB |
|
||||
|
||||
## Shape readiness
|
||||
|
||||
`isTileShapeFree` accepts only LOD0 tiles whose padded mesh box maps to one valid SurfaceWorld plan, excludes FloatingIslands, plan features, routes, passages, edits, and overlaps with owner shape primitives or detail bounds. This covers the MC skirt and sample margin. Higher LODs are conservatively shape-dependent; the accepted path cannot use far parents. The native-only context is initialized without building sign arrays, proxies, owner primitive vectors, or spatial bins. Native sampling and classification paths check the padded bounds, reject unsupported fallbacks, and count any attempted shape-dependent read; tile generation then fails instead of returning a partial mesh.
|
||||
|
||||
The SurfaceWorld spawn view proved 27/27 near-ring tiles shape-free. A deliberately mis-marked distant tile `(0,0,100000,lod0)` reached the sampler and was rejected by the guard. After shape publication, native-only and full-shape generation matched all four hashes for all 27 tiles. Shape-dependent tiles waited for the release-published shape; tile jobs continue through the existing Epoch and MPSC result path, and published shape/page data is immutable. The FloatingIslands spawn proof accepted 0/27 tiles, so all waited for its shape. Slow-shape delay defaults to zero; audit and negative-control work is opt-in.
|
||||
|
||||
## No-hash timing pairs
|
||||
|
||||
Five interleaved W12 pairs, camera `0,-35,-487.94`, seed 0, Clang `-ffp-contract=off`, hashing off. Times are from process start. The L9 executable does not emit a first-tile completion event and starts tile workers only after shape compilation; its first-tile cell is therefore unavailable.
|
||||
|
||||
| Pair | First tile L9 / L11 | Near ring L9 / L11 | Whole view L9 / L11 |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | — / 153 ms | 343 / 166 ms | 1,439 / 1,429 ms |
|
||||
| 2 | — / 160 ms | 424 / 173 ms | 1,526 / 1,447 ms |
|
||||
| 3 | — / 145 ms | 347 / 163 ms | 1,447 / 1,443 ms |
|
||||
| 4 | — / 146 ms | 353 / 170 ms | 1,457 / 1,449 ms |
|
||||
| 5 | — / 153 ms | 376 / 173 ms | 1,478 / 1,464 ms |
|
||||
| Median | — / 153 ms | 353 / 170 ms | 1,457 / 1,447 ms |
|
||||
|
||||
The delayed maximum-overlap W12 run held shape compilation for 1.5 s: first tile 133 ms, near ring 166 ms, shape ready 1.696 s. Shape-free and full-shape near-ring hashes matched, and whole-view, full-detail, per-LOD, and exact-query hashes matched the baseline.
|
||||
|
||||
## Hashes
|
||||
|
||||
All 28 configurations passed against L10's committed `b370908` baseline: SurfaceWorld and FloatingIslands, seed 0, W1/W8/W12, Clang `-ffp-contract=off` and GCC default, with five W12 repeats per compiler and recipe. Each run matched all 12 rows: full-detail, whole-view, nine LODs, and exact queries. The delayed-overlap run also matched.
|
||||
|
||||
| Recipe | Full-detail density / mesh / payload / topology | Whole-view density / mesh / payload / topology | Exact-query density & unclamped / detail / collision |
|
||||
|---|---|---|---|
|
||||
| SurfaceWorld | `0x0ee0579bd15cbbe4` / `0x32e5d4964d610fee` / `0x1932b96ee4b1d910` / `0x69743c6bad3447dc` | `0xc10697a8f48380c7` / `0xe75c9119a1498cd5` / `0x49a9a8d1835a6c8c` / `0xedcb23288b5e8724` | `0xe4d4a8eb95b35933` / `0xefcba20c0c70c415` / `0xe604823a249029bf` |
|
||||
| FloatingIslands | `0x6d842d2878b08ef6` / `0x4b868a3184f08035` / `0x2cedf2236788752d` / `0xeac17d5e0cb5044a` | `0x5be4d8cde79b2a76` / `0x96f75041f6861b3a` / `0x3700a516cc89d50c` / `0x6b99948d6eab4f21` | `0x61daded9c303becb` / `0xefcba20c0c70c415` / `0x84d37463be9b0573` |
|
||||
|
||||
The per-LOD D/M/P/T rows match exactly in every configuration; the complete comparison is recorded in `Saved/Linux/l11_hash_audit.txt`.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- The 24 m reproduction failed with `std::bad_alloc` under both address-space caps (`l11-cross24-as6g.log`, `l11-cross24-as16g.log`); the allocation trace above isolated the oversized bin resize. Final-binary 24 m, 32 m, and 1,000 m reruns passed (`l11-final-cross-24m.log`, `l11-final-cross-32m.log`, `l11-final-cross-1000m.log`).
|
||||
- The first readiness proof admitted 512 LOD0 keys (`l11-readiness-audit.log`, `l11-readiness-audit2.log`). Although the 27 near-ring equivalence check passed, distant owner-shape tiles changed the whole-view density hash. Tightening the proof to test owner primitive bounds reduced early admission to the 27 safe near-ring tiles and restored every hash (`l11-readiness-audit-final.log`).
|
||||
- One rebuilt audit invocation omitted the owner JSON path and exited 2 before generation (`l11-readiness-audit-path-error.log`; `Standalone/bin/data/owner_world.json` was missing). It was rerun with `Standalone/data/owner_world.json` and passed.
|
||||
- The required hash matrix and five timing pairs had no failed runs. All monitored run processes stayed below the 6,000,000,000-byte RSS ceiling; governor remained `powersave`.
|
||||
|
||||
The standalone Core/headless builds passed with Clang and GCC. The Unreal adapter change was source-reviewed but could not be built here because UnrealBuildTool/UnrealEditor are not installed on this Linux machine.
|
||||
|
||||
At spawn on SurfaceWorld, the near ring is ready at a 170 ms median versus 353 ms on L9, and the first L11 tile is ready at 153 ms median before shape publication; L9 gates tile work on shape compilation and does not report that milestone. For fast movement, destination-page work stays bounded through the tested 1 km crossing, while invalid Core coordinates return an error before allocation.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- **Re-run by me, 5 interleaved W12 pairs, no hashing.** Near ring (`neighborhood_ready`) L9
|
||||
0.338/0.338/0.337/0.326/0.334 s vs L11 0.159/0.159/0.167/0.169/0.159 s (median 0.337 → 0.159 s,
|
||||
−53%). Whole view 1.337 → 1.365 s median (+2.1%, 4 of 5 pairs slower): a small cost, accepted for a
|
||||
2× faster spawn ring. W12 hash runs give density `c10697a8f48380c7`.
|
||||
- **Crossings re-run by me on L11** (`--region-crossing --camera-step=S,0,0`): 24 m gives 432 tiles,
|
||||
0.867 s wall, 10.05 worker-s, rc 0; 1,000 m gives 2,260 tiles, 5.90 s wall, 70.4 worker-s, rc 0. The L9
|
||||
binary lacks the option, so the original crash rests on Luna's reproduction and allocation trace.
|
||||
- **New finding, for L12:** the 24 m crossing costs ~23 ms of work per tile, against ~2 ms for L10's 16 m
|
||||
crossing (288 tiles, 0.633 worker-s). Tiles outside the base shape bounds, served from analytic
|
||||
region pages, look about 10× more expensive. That is a moving-player cost.
|
||||
- **Concurrency read:** `World::compile` builds the shape privately, then sets `impl_->shape` and
|
||||
`buildRequest` before a release store of `shapeReady`. Every shape reader acquire-checks
|
||||
`shapeReady` first (short-circuit), and shape-free tiles use a private native-only context. Sound.
|
||||
- **Unreal:** `Source/VoxelForge/Private/VoxelWorld.cpp` changed (compile outside the writer lock;
|
||||
proven shape-free tiles start before `bReady`). It is NOT compiled here, so it must be built on the
|
||||
owner's PC before it is trusted in game.
|
||||
@@ -1,62 +0,0 @@
|
||||
# VoxelForge L12 — Linux report
|
||||
|
||||
Governor stayed `powersave`. Builds and runs used CPUs 0–11, at most 12 jobs, a 6 GiB address-space cap, and RSS monitors. Clang and GCC builds passed; peak sampled RSS was 881 MiB for one GCC compiler process and 679 MiB for the 1 km crossing. The GCC build process group peaked at 4.35 GiB. No Unreal adapter files changed.
|
||||
|
||||
## LOD cost and change
|
||||
|
||||
The 24 m profile is like-for-like at LOD1 in the same SurfaceWorld crossing. L11 generated 160 LOD1 tiles: 32 base-shape tiles averaged 2.73 ms each, while 128 analytic-page tiles averaged 71.72 ms. Of the LOD1 worker time, 9.06 of 9.27 seconds went to the marching-cubes density grid; the page compiler added 15 ms and built no packed lattice nodes. The page route lacked the manifest input needed for the exact LOD1 block proofs. Across all 160 LOD1 tiles, the sample counter reported 5.64 million density samples, or 35,271 per tile.
|
||||
|
||||
Analytic pages now retain the immutable shared manifest for block classification while continuing to sample the legacy density field. The proof audit checked 27,479 blocks and 36,574,549 samples, with zero violations. On the plain 24 m profile, page-backed LOD1 fell to 5.60 ms per tile; all 160 LOD1 tiles reported 432,000 samples (2,700 per tile) and 435 ms in density-grid work. Base LOD1 tiles averaged 2.51 ms, so page tiles are now about 2.2× their base cost. No base-shape bounds changed; pages remain privately built, immutably published, and bounded, with 16 pages at 24/32 m and 72 at 1 km.
|
||||
|
||||
## Plain W12 crossings
|
||||
|
||||
Worker time is the sum across tile workers; per-tile values use that sum. L11 values are from the L11 executable and logs. L12 values include region compilation in wall time.
|
||||
|
||||
| Step | Tiles | L11 wall / worker-s / ms per tile | L12 wall / worker-s / ms per tile |
|
||||
|---:|---:|---:|---:|
|
||||
| 8 m | 144 | 0.038 s / 0.381 / 2.65 | 0.035 s / 0.360 / 2.50 |
|
||||
| 16 m | 288 | 0.831 s / 9.476 / 32.90 | 0.125 s / 1.096 / 3.80 |
|
||||
| 24 m | 432 | 0.869 s / 9.912 / 22.95 | 0.164 s / 1.596 / 3.69 |
|
||||
| 32 m | 576 | 0.898 s / 10.261 / 17.81 | 0.172 s / 1.691 / 2.94 |
|
||||
| 1 km | 2,260 | 5.892 s / 69.534 / 30.77 | 1.247 s / 14.003 / 6.20 |
|
||||
|
||||
All five L12 steps passed; peak crossing RSS was 679 MiB. The 8 m L11 comparison is `l12-baseline-8m-page.log`; 16 m is `l12-baseline-16m-page.log`; 24 m, 32 m, and 1 km are the L11 final crossing logs.
|
||||
|
||||
## Spawn and hashes
|
||||
|
||||
Three plain W12 spawn runs had medians of 146 ms first tile, 161 ms near ring, and 1.361 s whole view. L11 medians were 153 ms, 170 ms, and 1.447 s.
|
||||
|
||||
All 28 requested L1 hash configurations matched L11: SurfaceWorld and FloatingIslands seed 0, Clang `-ffp-contract=off` and GCC default, W1/W8/W12, with five W12 repeats. Every run matched all 12 rows: full detail, whole view, nine LODs, and exact queries. Clang/GCC full-detail and whole-view D/M/P/T plus exact-query values are unchanged from the L11 report. The L3 FP-contract preamble/self-test remains in place.
|
||||
|
||||
| Recipe | Full-detail density / mesh / payload / topology | Whole-view density / mesh / payload / topology | Exact density+unclamped / detail / collision |
|
||||
|---|---|---|---|
|
||||
| SurfaceWorld | `0x0ee0579bd15cbbe4` / `0x32e5d4964d610fee` / `0x1932b96ee4b1d910` / `0x69743c6bad3447dc` | `0xc10697a8f48380c7` / `0xe75c9119a1498cd5` / `0x49a9a8d1835a6c8c` / `0xedcb23288b5e8724` | `0xe4d4a8eb95b35933` / `0xefcba20c0c70c415` / `0xe604823a249029bf` |
|
||||
| FloatingIslands | `0x6d842d2878b08ef6` / `0x4b868a3184f08035` / `0x2cedf2236788752d` / `0xeac17d5e0cb5044a` | `0x5be4d8cde79b2a76` / `0x96f75041f6861b3a` / `0x3700a516cc89d50c` / `0x6b99948d6eab4f21` | `0x61daded9c303becb` / `0xefcba20c0c70c415` / `0x84d37463be9b0573` |
|
||||
|
||||
The 24 m crossing hash also matches L11 at W1 and W12 (432 rows each): density `0x174588abdc8c0d2c`, mesh `0x4c401e27d9d05dea`, payload `0xeb10d9801c5ef125`, topology `0xf59d12304d0dcb4a`.
|
||||
|
||||
The route-verified base/page audit does **not** pass the requested exact-tile invariant. For `(0,-5,-31,lod1)`, the base density hash is `0xe0672dae7528f8d7` and page density hash is `0x882280f67bfddd77`; mesh, payload, and topology match. All 48 base-covered LOD1 candidates had density mismatches; 10 also differed in mesh/payload/topology. Across eight route-verified LOD0/LOD1 pairs, all eight density hashes differed. The base SurfaceWorld route uses manifest density while L11 pages use the legacy density evaluator; switching page samples to the base field would change the L11 crossing hashes. I kept the legacy page field to preserve the required crossing output, so this pre-existing parity conflict remains unresolved.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- An attempted 24 m base-shape expansion exited 2 at the 160-million-node bound (`l12-baseline-24m-base.log`). A separate per-tile native-only proof promotion failed with `region OOM` and was reverted (`l12-probe-cross24-profile-w12.log`).
|
||||
- The first plain step command passed `32` as the camera distance while labeling it 8 m; output identified it as 32 m. It was retained as the 32 m result, and 8 m was rerun with the correct distance.
|
||||
- The first FloatingIslands matrix omitted `--auto-surface-camera`, so it hashed the wrong 2,553-tile view. It was rerun with the L11 seed-0 island placement; all 28 corrected matrix configurations passed. The comparison helper also needed two small script fixes before it reported the final 28/28 result.
|
||||
- One base/page audit initially treated a LOD2 base-route fallback (`page_keys=0`) as a match. The audit now verifies the generated route; that apparent pass was rejected and the route-verified parity failure above is the final result.
|
||||
|
||||
Walking into new ground should feel much better: the plain 24 m crossing fell from 0.869 s to 0.164 s, and the 1 km crossing from 5.892 s to 1.247 s. Page-backed LOD1 tiles are down from 71.7 to 5.6 ms each while the L11 crossing hashes stay exact; base/page density-hash parity remains a separate unresolved constraint.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Re-run by me (W12, no hashing, 3 interleaved pairs): 24 m crossing L11 0.874/0.914/0.898 s vs L12
|
||||
0.154/0.166/0.166 s (median 0.898 → 0.166 s, 5.4×). Spawn is unchanged (near ring 0.160–0.184 s).
|
||||
The 24 m crossing hash (density `0x174588abdc8c0d2c`, mesh `0x4c401e27d9d05dea`) matches L11 at W1
|
||||
and W12. Hash runs refill skipped samples, so time crossings with hashing off.
|
||||
- **Soundness of page-tile skipping:** the proof audit figures (27,479 blocks / 36,574,549 samples)
|
||||
are the base-shape audit from L9, not page tiles. The evidence for pages is the crossing hash
|
||||
equalling L11 (which filled every sample). That is necessary but narrow: one crossing, one seed.
|
||||
- ⚠️ **Route-dependent output (a pre-existing multiplayer bug, now measured):** the same tile key
|
||||
gives different density from the base shape (manifest field) than from an analytic page (legacy
|
||||
field). All 48 base-covered LOD1 candidates differed in density, 10 also in mesh/payload/topology.
|
||||
A tile's geometry then depends on streaming history (spawned near it vs walked to it), which
|
||||
breaks peer determinism. Top priority for L13.
|
||||
@@ -1,140 +0,0 @@
|
||||
# VoxelForge M3 Linux13 — route-invariant density
|
||||
|
||||
## Field decision and fix
|
||||
|
||||
The approved reference is the Core base shape built from the reviewed owner manifest: `Heightfield2D`
|
||||
for SurfaceWorld and `FloatingIslandFieldV1` for FloatingIslands. L12's base route sampled that
|
||||
field. Its analytic pages instead fell through to the copied legacy owner `GetDensityAt` evaluator,
|
||||
so the same key could sample different values after the player crossed into a page. In the L12
|
||||
SurfaceWorld audit, all 48 base-covered LOD1 candidates had different density; 10 also differed in
|
||||
mesh, payload, and topology. A 214,375-sample L12 negative-control run measured max/mean density
|
||||
delta `0.076171875 / 0.0133467594`, 29 sign changes, 170 edge-crossing changes, and up to 1.7778
|
||||
voxels of surface displacement. The manifest field is the approved Core world and is the reference
|
||||
kept here.
|
||||
|
||||
Changing the page manifest flag alone did not fix FloatingIslands: 8/8 page pairs still failed,
|
||||
with max density delta `433.925781`, 262,676 sign changes, and 17,188 edge-crossing changes across
|
||||
343,000 samples. The source fix binds each compiled page to its world's base `FStandaloneShapeWorld`
|
||||
before publication. Page density, lattice, detail, classification, and solid queries now delegate
|
||||
to that one evaluator; the legacy page-local evaluator is no longer an independent output route.
|
||||
`WorldAssemblyEvaluatorVersion` is bumped from 5 to 6.
|
||||
|
||||
## Route parity
|
||||
|
||||
The L12 evaluator-v5 negative control fails as required: 5/5 selected base/page pairs mismatch. After the fix,
|
||||
four audits passed: SurfaceWorld seeds 0, 1, and 17, plus FloatingIslands seed 0.
|
||||
|
||||
| Compared routes | Pairs | Mismatches |
|
||||
|---|---:|---:|
|
||||
| Base vs page (28 clipmap-requested pages; 26 base-covered page controls) | 54 | 0 |
|
||||
| Base vs native-only shape-free route (SurfaceWorld LOD0) | 27 | 0 |
|
||||
| Edited base vs edited page | 4 | 0 |
|
||||
| Same key built in initial and moved-world history | 2 | 0 |
|
||||
| Tile samples vs exact Core queries at LOD2–8 | 56 | 0 |
|
||||
|
||||
All 87 tile-generation pairs match density, mesh, payload, and topology hashes. The 56 higher-LOD
|
||||
pairs compare the sampled tile lattice to direct `densityAt` / `densityAtLattice` queries; these
|
||||
also match all sampled densities and solid/air signs. The owner fixture has
|
||||
`full_res_clip_levels=2`, so analytic page routes exist only at LOD0–1. In total, page comparisons
|
||||
covered 2,315,250 density samples with zero delta, sign, edge-crossing, or surface-root differences;
|
||||
6,172,229 exact-query samples had zero mismatches. The audit exits nonzero on any output-hash,
|
||||
sample, route-verification, exact-query, or proof-violation mismatch.
|
||||
|
||||
The history probe builds the same LOD0 key in a second Core world after a 96 m camera move. Both
|
||||
SurfaceWorld and FloatingIslands match all four hashes across those two build histories. The edit
|
||||
probe applies a sequence-1 density offset centered on a sampled sign crossing; it changes the field,
|
||||
and edited base/page results still match.
|
||||
|
||||
### Page proof audit
|
||||
|
||||
The fill-and-check audit generated page tiles with full density capture and uniform-block proof
|
||||
verification, so it checked the field those pages actually sample. Across the four audits it checked
|
||||
1,274 blocks / 1,695,694 samples: 518 blocks used the exact fallback, 6 were unknown, and there were
|
||||
zero false proofs or violations. SurfaceWorld walkability passed 5,642/5,642, 6,080/6,080, and
|
||||
4,785/4,785 samples for seeds 0, 1, and 17; FloatingIslands passed 4,000/4,000. Shape readiness
|
||||
passed 27/27 near-ring tiles where eligible. FloatingIslands has no eligible native-only tiles.
|
||||
|
||||
## Cost, hashing off
|
||||
|
||||
W12, seed 0, `--force-generate`, camera `0,-35,-487.94`, with the same crossing setup as L12:
|
||||
|
||||
| Move | Entering tiles | L12 crossing | L13 crossing | L13 peak RSS |
|
||||
|---|---:|---:|---:|---:|
|
||||
| 8 m | 144 | 0.0347 s | 0.0399 s | 468 MiB |
|
||||
| 16 m | 288 | 0.1249 s | 0.0856 s | 513 MiB |
|
||||
| 24 m | 432 | 0.1635 s | 0.1242 s | 512 MiB |
|
||||
| 32 m | 576 | 0.1717 s | 0.1692 s | 511 MiB |
|
||||
| 1 km | 2,260 | 1.2474 s | 0.7696 s | 673 MiB |
|
||||
|
||||
Three W12 spawn runs had medians of 157 ms first tile, 180 ms near ring, and 1.499 s whole view;
|
||||
L12 medians were 146 ms, 161 ms, and 1.361 s. Each spawn run generated the same 2,553-tile
|
||||
benchmark view. The final timings are in `Saved/Linux/l13-crossing-final-*.log` and
|
||||
`Saved/Linux/l13-spawn-final-*.log`.
|
||||
|
||||
## Determinism and invariants
|
||||
|
||||
All 28 required configurations passed: SurfaceWorld and FloatingIslands seed 0; Clang
|
||||
`-ffp-contract=off` and GCC default; W1/W8/W12, with five W12 repeats per compiler and recipe. Each
|
||||
run matched all 12 rows (full detail, whole view, LOD0–8, exact queries) against L12/L11.
|
||||
|
||||
| Recipe | Full detail D/M/P/T | Whole view D/M/P/T | Exact density / unclamped / detail / collision |
|
||||
|---|---|---|---|
|
||||
| SurfaceWorld | `0x0ee0579bd15cbbe4 / 0x32e5d4964d610fee / 0x1932b96ee4b1d910 / 0x69743c6bad3447dc` | `0xc10697a8f48380c7 / 0xe75c9119a1498cd5 / 0x49a9a8d1835a6c8c / 0xedcb23288b5e8724` | `0xe4d4a8eb95b35933 / 0xe4d4a8eb95b35933 / 0xefcba20c0c70c415 / 0xe604823a249029bf` |
|
||||
| FloatingIslands | `0x6d842d2878b08ef6 / 0x4b868a3184f08035 / 0x2cedf2236788752d / 0xeac17d5e0cb5044a` | `0x5be4d8cde79b2a76 / 0x96f75041f6861b3a / 0x3700a516cc89d50c / 0x6b99948d6eab4f21` | `0x61daded9c303becb / 0x61daded9c303becb / 0xefcba20c0c70c415 / 0x84d37463be9b0573` |
|
||||
|
||||
The crossing output changes as intended. The 24 m W1 and W12 moved-tile hashes match each other:
|
||||
`0x337ca62131576728 / 0x7612f48e2ddbbaab / 0x69a67ed4add83b87 / 0xf11ac1ebd0f7ddb8` (D/M/P/T),
|
||||
replacing L12's `0x174588abdc8c0d2c / 0x4c401e27d9d05dea / 0xeb10d9801c5ef125 / 0xf59d12304d0dcb4a`.
|
||||
|
||||
The L3 FP-contract preamble and runtime self-test, L4 `FLazy`, L7 shared column cache, L9/L12
|
||||
proofs, and L11 native-only readiness/crash fixes were preserved. The L6 screen-space controls
|
||||
still pass their six same-LOD checks and reject the missing/floating-surface, global-octave, and
|
||||
stride-2 negative controls as required. Its whole gate still rejects the known far-field error over
|
||||
the 1 px bound (exit 6); the route binding does not affect those LOD3–8 base-field comparisons.
|
||||
The asynchronous MPSC/Epoch path and `ARCHITECTURE.md` §8.10 invariants are unchanged. No Unreal
|
||||
adapter files were touched; the Core exact-query and collision-sign routes were checked, but Unreal
|
||||
collision cooking was not built here. `ARCHITECTURE.md` §8.36 and `CODEMAP.md` §3.22 document the new
|
||||
route law.
|
||||
|
||||
Builds and runs used CPUs 0–11, at most 12 workers, a 6 GiB address-space cap, and a 6,000,000,000
|
||||
byte RSS monitor. The governor remained `powersave`. Peak sampled RSS was 904 MiB across runs
|
||||
(673 MiB in the no-hash cost table); the GCC build process group peaked at 4.27 GiB. Clang and GCC
|
||||
builds passed. No system changes or Unreal builds were made.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- `l13-route-baseline-s0-w12.log` exited 2 because the inferred binary-relative owner JSON path did
|
||||
not exist; reruns used the explicit repository JSON. The first baseline capture used the wrong
|
||||
camera, then an audit pass had empty proof-skipped density captures; the final L12 negative
|
||||
control is `l13-route-baseline-s0-w12-r4.log`.
|
||||
- `l13-route-fi-diagnostic.log` and `l13-route-fi-diagnostic-r2.log` intentionally exposed the
|
||||
remaining FloatingIslands split before the shared-source binding; both exit 9 on 8/8 mismatches.
|
||||
- `l13-route-edit-s0.log` exited 9 because its brush did not change the selected tile. Centering the
|
||||
brush on a sampled sign crossing fixed the audit in `l13-route-edit-s0-r2.log`.
|
||||
- `l13-route-history-sw-s0.log` exited 9 because the second-world probe exceeded the 4,096-node
|
||||
region compiler envelope. The retry compiles one matching LOD0 anchor tile and passes.
|
||||
- The first `l13-cost-*` measurements omitted `--force-generate` and used the owner JSON start;
|
||||
those are not used above. The final crossing and spawn runs use L12's camera and force-generation
|
||||
setting. The screen-space run's exit 6 is its expected far-field rejection, not a retried run.
|
||||
- All final builds, the 28 hash runs, eight no-hash cost runs, and two crossing-hash runs completed
|
||||
within their limits. No commit, push, or stash was made.
|
||||
|
||||
## Multiplayer note for the owner
|
||||
|
||||
Peers with the same manifest, seed, evaluator version 6, and tile key now reconstruct the same Core
|
||||
density and tile output through each available base, page, or native-only route, regardless of
|
||||
arrival history; terrain geometry still does not need to cross the network. Player edits remain an
|
||||
explicit versioned diff that must be ordered and synchronized between peers.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- **Spawn cost re-run by me, 5 interleaved W12 pairs, no hashing:** whole view L12
|
||||
1.385/1.405/1.419/1.443/1.460 s vs L13 1.401/1.448/1.430/1.435/1.451 s (median 1.419 → 1.435 s,
|
||||
+1.1%, within noise); near ring 0.168 → 0.171 s. The +10% in the cost table above compares L13 runs
|
||||
with L12 numbers from another session; interleaved, there is no measurable spawn regression.
|
||||
- The fix is structural: pages are bound to their world's single base `FStandaloneShapeWorld`
|
||||
evaluator before publication, so routes can't diverge by construction. The negative control fails
|
||||
on L12 (5/5), and the 87 tile pairs, 56 exact-query pairs and 2.3M page samples show 0 mismatches
|
||||
after. The page lifetime is safe because the base shape is owned by the `World` and outlives its pages.
|
||||
- Crossing output changed, as intended (evaluator version 5 → 6); the spawn and island worlds are
|
||||
unchanged.
|
||||
@@ -1,157 +0,0 @@
|
||||
# Linux14 — far spawn work
|
||||
|
||||
No mesh or density code changed. The source changes add opt-in profiling for far-shell column
|
||||
lookups; counter snapshots are behind the existing profile guard, with no added timing or counter
|
||||
increments when profiling is off. The final plain build has the same output as L13.
|
||||
|
||||
## Far LOD cost and ranking
|
||||
|
||||
The first table is from five interleaved, W12, hashing-off pairs: L13 binary before and L14 binary
|
||||
after. Values are median worker-sums across that LOD; per-tile time is the L14 median divided by
|
||||
tile count. The ranking is L6, L8, L4, L5, L7 on both binaries.
|
||||
|
||||
| LOD | Tiles | L13 → L14 median worker-s (s) | L14 worker-ms/tile |
|
||||
|---:|---:|---:|---:|
|
||||
| 4 | 320 | 1.600 → 1.572 | 4.91 |
|
||||
| 5 | 144 | 1.421 → 1.411 | 9.80 |
|
||||
| 6 | 96 | 2.624 → 2.585 | 26.93 |
|
||||
| 7 | 48 | 1.152 → 1.158 | 24.13 |
|
||||
| 8 | 153 | 2.044 → 2.046 | 13.37 |
|
||||
|
||||
Phase detail below is one W12 `--profile-lod-work` run on the final Clang binary. Cache counters
|
||||
include all column work for that LOD, not just refinement. A “shared hit” is a hit from the
|
||||
world-shared cache; formula evaluations equal local misses minus shared hits.
|
||||
|
||||
| LOD | Calls | Local hits / misses | Shared hits | Formula evaluations (per tile) | Column compute (ms) | MC non-column (ms) | Ground/cap non-column (ms) | Connector shell non-column (ms) | Skirt (ms) | Assembly (ms) |
|
||||
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 4 | 1,845,787 | 1,579,868 / 265,919 | 175,805 | 90,114 (282) | 521 | 472 | 385 | 0 | 186 | 3 |
|
||||
| 5 | 877,542 | 701,124 / 176,418 | 105,477 | 70,941 (493) | 408 | 135 | 705 | 0 | 159 | 2 |
|
||||
| 6 | 760,952 | 418,443 / 342,509 | 63,209 | 279,300 (2,909) | 1,594 | 90 | 474 | 210 | 158 | 2 |
|
||||
| 7 | 337,328 | 257,969 / 79,359 | 8,878 | 70,481 (1,468) | 395 | 59 | 514 | 0 | 145 | 4 |
|
||||
| 8 | 644,565 | 508,799 / 135,766 | 29,545 | 106,221 (694) | 608 | 374 | 783 | 0 | 278 | 6 |
|
||||
|
||||
MC and refinement column-compute / non-column times (ms) were L4 `221/472` MC and `281/385`
|
||||
ground/cap; L5 `199/135` MC and `209/705` ground/cap; L6 `196/90` MC and `205/474` ground/cap;
|
||||
L7 `184/59` MC and `211/514` ground/cap; L8 `413/374` MC and `195/783` ground/cap. The column
|
||||
compute column overlaps those column portions. Timers are worker sums and may differ by a few ms
|
||||
from their subphase sums.
|
||||
|
||||
Stratum shells made zero field calls at LOD4–8 and took under 0.03 ms total. The profiler run
|
||||
measured about 1.408 s whole-view-ready; the unprofiled interleaved runs below are the timing result.
|
||||
|
||||
## Connector-shell repair
|
||||
|
||||
`AppendFarConnectorShells` is guarded to LOD6 and above. Coarse marching cubes can leave gaps in a
|
||||
route connector wall, so the repair checks four axial rays at connector stations and adds only the
|
||||
panels where the nearest coarse-mesh hit misses the source-field radius tolerance. LOD6 checks at
|
||||
128-voxel stations; LOD7–8 use 256-voxel stations. Only LOD6 had repair candidates in this spawn
|
||||
view: 5,066 triangles were added. The shell took 1.403 worker-s, including 1.193 s of column
|
||||
evaluation (about 0.210 s shell work outside those evaluations).
|
||||
|
||||
The LOD6 repair made 251,475 source/column calls: 44,596 worker-local hits, 206,879 local misses,
|
||||
30 shared-cache hits, and 206,849 formula evaluations. Thus 82.3% of calls evaluated a formula;
|
||||
the work is mostly uncached. A cache miss can include a point evicted by the four-way worker cache,
|
||||
so these counters do not prove every evaluated XY is unique. The L13-bound base evaluator already
|
||||
feeds the shared exact-XY cache; neighboring LOD samples mostly use different XY coordinates, and
|
||||
only 30 repair samples hit the shared cache. Reducing stations or rays would change the repair
|
||||
geometry, and no bit-identical reduction was proved.
|
||||
|
||||
## Ground and cap refinement
|
||||
|
||||
Ground refinement starts at LOD5 (32-voxel grid there, 64 at LOD6, then 128 at LOD7–8). Cap
|
||||
refinement starts at LOD4 (16-voxel grid there, then 128 at LOD5–7; LOD8 may use its configured
|
||||
interior override). Both query exact `Heightfield2D` columns and use those values for heights,
|
||||
normals, materials, clipping, and replacing nearby coarse faces. The existing shared cache is keyed
|
||||
by recipe/plan and exact XY, so aligned samples can already be reused across tiles and levels. The
|
||||
LOD-wide shared-hit counts are in the table above; they are not refinement-only counts.
|
||||
|
||||
Current safe skips are the plan Z-range check for ground and the cap-height-bound check for caps.
|
||||
No additional tile/cell skip was shown to preserve every sample that affects output; a grid point
|
||||
can also supply a neighboring normal or clipped cell. No new refinement sharing or skip was built.
|
||||
|
||||
## Ranked exact-output work
|
||||
|
||||
No exact-output change with a measured saving above 3% was proved, so no geometry optimization was
|
||||
built. The ranked work buckets are:
|
||||
|
||||
1. Ground/cap refinement across LOD4–8: about 3.95 worker-s including its column work. Removing all
|
||||
of it would be an unrealistic idealized 23% of 1.419 s at 12-way division; the sampled values
|
||||
currently affect geometry/materials/normals.
|
||||
2. LOD6 connector repair: 1.403 worker-s inclusive, an idealized upper bound near 8.2% at 12-way
|
||||
division. Most calls are uncached, and the missing panels are required for the current mesh.
|
||||
3. Far-LOD column formula work: 3.53 worker-s across LOD4–8, overlapping the two buckets above.
|
||||
Exact-XY sharing is already active; no redundant set large enough to remove was demonstrated.
|
||||
4. Marching-cubes non-column work is 1.13 worker-s and skirts 0.93 worker-s. Assembly is only
|
||||
0.017 worker-s; stratum shells are negligible. No exact reduction was established.
|
||||
|
||||
These idealized conversions divide worker-s by 12 and are not predicted wall-time gains. The L6
|
||||
screen-space gate was not used because this round made no geometry change.
|
||||
|
||||
## Interleaved cost, builds, and hashes
|
||||
|
||||
Five hashing-off W12 pairs ran in one session, alternating
|
||||
`Standalone/bin/linux/VoxelForgeCoreHeadless-linux13-clang` and
|
||||
`Standalone/bin/linux/VoxelForgeCoreHeadless-linux14-clang`. Every run generated 2,553 tiles at
|
||||
seed 0 and camera `0,-35,-487.94,0,-22`.
|
||||
|
||||
| Pair | L13 whole-view-ready (s) | L14 whole-view-ready (s) |
|
||||
|---:|---:|---:|
|
||||
| 1 | 1.416 | 1.410 |
|
||||
| 2 | 1.436 | 1.388 |
|
||||
| 3 | 1.416 | 1.424 |
|
||||
| 4 | 1.429 | 1.419 |
|
||||
| 5 | 1.430 | 1.426 |
|
||||
|
||||
Median was 1.429 s before and 1.419 s after (−0.7%, within this run spread). The telemetry is
|
||||
opt-in; the plain run shows no measurable cost when off.
|
||||
|
||||
Final binaries `Standalone/bin/linux/VoxelForgeCoreHeadless-linux14-clang` and
|
||||
`Standalone/bin/linux/VoxelForgeCoreHeadless-linux14-gcc` were rebuilt with `Standalone/BuildLinux.sh`,
|
||||
`-j12`, CPUs 0–11, and a 6 GiB address-space cap: Clang 19 with FP contraction off and GCC 14 with
|
||||
default contraction. Builds passed. Runtime peak RSS was 926,520 KiB; largest individual
|
||||
build-process RSS was 901,968 KiB (GCC), with a 4,557,196 KiB process-group peak. Governor remained
|
||||
`powersave`.
|
||||
|
||||
The final hash matrix passed 28/28: SurfaceWorld seed 0 and FloatingIslands seed 0, Clang/GCC,
|
||||
W1/W8/W12, with five W12 repeats per recipe/compiler. Full-detail, whole-view, all nine per-LOD,
|
||||
and exact-query rows match L13 exactly. Route parity also passed for SurfaceWorld seeds 0,
|
||||
1, and 17 and FloatingIslands seed 0: page/native-only routes, edits, history, higher-LOD exact
|
||||
queries, and proof checks had zero mismatches. The final 24 m crossing hash matched at W1/W12 on
|
||||
both compilers: `0x337ca62131576728 / 0x7612f48e2ddbbaab / 0x69a67ed4add83b87 / 0xf11ac1ebd0f7ddb8`
|
||||
(density/mesh/payload/topology).
|
||||
|
||||
| Recipe | Full-detail D/M/P/T | Whole-view D/M/P/T | Exact density / unclamped / detail / collision |
|
||||
|---|---|---|---|
|
||||
| SurfaceWorld | `0x0ee0579bd15cbbe4 / 0x32e5d4964d610fee / 0x1932b96ee4b1d910 / 0x69743c6bad3447dc` | `0xc10697a8f48380c7 / 0xe75c9119a1498cd5 / 0x49a9a8d1835a6c8c / 0xedcb23288b5e8724` | `0xe4d4a8eb95b35933 / 0xe4d4a8eb95b35933 / 0xefcba20c0c70c415 / 0xe604823a249029bf` |
|
||||
| FloatingIslands | `0x6d842d2878b08ef6 / 0x4b868a3184f08035 / 0x2cedf2236788752d / 0xeac17d5e0cb5044a` | `0x5be4d8cde79b2a76 / 0x96f75041f6861b3a / 0x3700a516cc89d50c / 0x6b99948d6eab4f21` | `0x61daded9c303becb / 0x61daded9c303becb / 0xefcba20c0c70c415 / 0x84d37463be9b0573` |
|
||||
|
||||
Hash logs and the 28-row comparator are in `Saved/Linux/l14-final-hash-*.log` and
|
||||
`Saved/Linux/l14-final-hash-audit.txt`. Final route logs are `Saved/Linux/l14-final-route-*.log`;
|
||||
crossing and pair logs use the `l14-final-crossing-*` and `l14-final-cost-*` names.
|
||||
|
||||
## Failed or retried runs
|
||||
|
||||
- The first SurfaceWorld hash matrix and first crossing checks omitted the spawn camera and used the
|
||||
headless origin. Their comparisons differed from L13; all 14 SurfaceWorld configurations and all
|
||||
four crossing configurations were rerun with the spawn camera and passed. FloatingIslands used
|
||||
its auto-surface camera and matched on the first run.
|
||||
- The first route probes also used the wrong camera selection. A later seed-1 probe used the fixed
|
||||
seed-0 camera, exited 9 on two LOD2 exact-query checks, and was rerun with `--auto-surface-camera`;
|
||||
the corrected seed-1 and seed-17 audits passed. The origin-camera route probes returned success
|
||||
but were not used as reference results.
|
||||
- Clang was rebuilt after the connector counters were added and again after folding aggregation
|
||||
into the existing opt-in guard. Both Clang builds and the final GCC build succeeded. No run hit
|
||||
the RSS, 15-minute stuck-step, or 30-minute limits.
|
||||
|
||||
Owner note: the highest measured far cost is still LOD6 connector repair, but its formula work is
|
||||
mostly uncached and its panels preserve the coarse route wall. The final interleaved plain runs show
|
||||
no measurable diagnostic overhead; no geometry change or L6 budget claim is made.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Accepted as a counting round. Interleaved W12, no hashing: L13 1.429 s vs L14 1.419 s median, so the
|
||||
opt-in counters cost nothing measurable when off; committed with the report.
|
||||
- Conclusion: exact-output far work has no remaining lever above 3%. The big buckets
|
||||
(ground/cap refinement ~3.95 worker-s, LOD6 connector repair ~1.40 worker-s) produce visible
|
||||
geometry. Further far savings need geometry changes, judged against today's same-LOD mesh within
|
||||
the owner's budget (1 px at 1440p, at the LOD's closest display distance, display only).
|
||||
@@ -1,195 +0,0 @@
|
||||
# Linux15 report
|
||||
|
||||
## Result
|
||||
|
||||
Null result. No far-mesh shortcut passed the full 1 px, shading, swap, and topology gates, so no
|
||||
recipe enables one by default. Evaluator version 6→7 adds the optional `far_display_mesh` declaration;
|
||||
omitting it keeps the L14 output path. The controls are in the shared Core layer and do not touch
|
||||
LOD0–2, exact queries, collision, edits, or server state.
|
||||
|
||||
Candidates measured:
|
||||
|
||||
- Refinement grid scale ×2 at LOD4, 5, 6, 7, and 8. The all-tile LOD4 and LOD5 runs reject; the
|
||||
remaining per-LOD probes exceed 1 px or change shading, and the LOD6–8 candidates worsen at
|
||||
least one adjacent swap.
|
||||
- Reusing connector station radii leaves geometry p99 at 1.07 px but reaches 15.23 px maximum;
|
||||
shading delta reaches 0.667 and the normal changes by 73.3°.
|
||||
- Coarsening refinement at LOD4–8 is the largest measured saving, but exceeds the display budget.
|
||||
Halving connector station density also saves time but opens gaps. Removing
|
||||
any one ray direction also opens gaps, and the removed-panel control finds 99 new holes.
|
||||
|
||||
## Screen-space gate
|
||||
|
||||
The gate samples 65×65 surface grids across all 1,145 displayed LOD3–8 tiles (5,208,134 samples;
|
||||
skirts excluded). Geometry is projected normal displacement against today's same-LOD mesh at its
|
||||
closest display distance, using 90° horizontal FOV and 2,560 px (0.000613592 rad/px). Shading delta
|
||||
is the maximum of fixed-light normalized Lambert change and material selector/blend-channel change;
|
||||
the limit is 1/255. Each number below is p50/p99/max. Exact-path baseline values are all zero.
|
||||
The gate rejects geometry max above 1 px, shading max above 1/255, any worse per-sample swap,
|
||||
missing surface, or new hole/crack/floater. Its own validation fails if S13, stride-2, missing
|
||||
surface, floating surface, missing fine-ring, or removed-panel controls are not rejected; the
|
||||
panel control must expose at least one new connector hole.
|
||||
|
||||
| LOD | Tiles | Geometry px: baseline → refine×2 all | Shading delta: baseline → refine×2 all | Refine×2 holes / cracks / floating |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 3 | 384 | 0/0/0 → 0/0/0 | 0/0/0 → 0/0/0 | 0 / 0 / 0 |
|
||||
| 4 | 320 | 0/0/0 → 0/2.370/606.562 | 0/0/0 → 0/0.023529/1 | 254 / 400 / 254 |
|
||||
| 5 | 144 | 0/0/0 → 0/12.728/95.453 | 0/0/0 → 0/1/1 | 1,183 / 76 / 138 |
|
||||
| 6 | 96 | 0/0/0 → 0.023/13.408/799.596 | 0/0/0 → 0/1/1 | 476 / 0 / 201 |
|
||||
| 7 | 48 | 0/0/0 → 0/13.242/248.905 | 0/0/0 → 0/1/1 | 30 / 0 / 1 |
|
||||
| 8 | 153 | 0/0/0 → 0.158/19.250/187.465 | 0/0/0 → 0.003922/1/1 | 45 / 0 / 946 |
|
||||
|
||||
The selected-patch swap report compares each baseline with refine×2 all. Values are p50/p99/max px.
|
||||
|
||||
| Swap (distance) | Before | After refine×2 all |
|
||||
|---|---:|---:|
|
||||
| 3→2 (96 m) | 0.611/3.767/6.045 | 0.611/3.767/6.045 |
|
||||
| 4→3 (192 m) | 0.752/4.630/7.513 | 0.752/4.630/7.513 |
|
||||
| 5→4 (384 m) | 0/9.580/31.783 | 2.435/32.321/86.615 |
|
||||
| 6→5 (768 m) | 0/9.754/70.340 | 0.343/19.142/78.077 |
|
||||
| 7→6 (1,536 m) | 0/9.040/42.355 | 0.200/15.109/50.994 |
|
||||
| 8→7 (3,072 m) | 0.826/18.030/47.182 | 1.528/24.547/64.449 |
|
||||
|
||||
The station×2 LOD6 screen scan measures 0/2/813.571 px, shading 0/0/1, with 1,349 holes and 1,690
|
||||
floaters. Its connector audit counts missing ray stations by stable tile/ray/station ID: holes 8→221
|
||||
(216 new), floating classifications 464→405 (111 new IDs). In the refine×2 all row above, the
|
||||
screen scan reports those all-tile holes, cracks, and floaters. Station×2 leaves all six selected-patch
|
||||
swap p50/p99/max rows at baseline values, including 6→5 at 0/9.754/70.340 px. All four ray directions are required:
|
||||
the four one-ray-removed
|
||||
controls create 1,008, 1,022, 1,171, and 1,158 new holes. The explicit panel-removal control creates
|
||||
99 new holes. The exact-path audit has zero new holes, cracks, or floating pieces; its raw connector
|
||||
audit counts remain 8 holes and 464 pre-existing floating classifications.
|
||||
|
||||
The LOD4×2 and LOD5×2 candidates also fail in isolation. Reusing station radii misses the LOD6
|
||||
geometry and shading limits. The four one-ray-removed masks add 1,008, 1,022, 1,171, and 1,158
|
||||
connector holes, respectively.
|
||||
|
||||
## Negative controls
|
||||
|
||||
- The S13 global-octave-drop and stride-2 column-interpolation controls are rejected at every LOD3–8.
|
||||
- Missing displayed surface, a floating surface, and missing fine-ring surface controls are rejected.
|
||||
- Removing connector panels is rejected with 99 new holes. The exact connector path has zero new
|
||||
station IDs classified as holes, cracks, or floaters.
|
||||
|
||||
## Speed
|
||||
|
||||
Five interleaved W12 pairs per candidate, hashing off, same 2,553-tile L1 view.
|
||||
`whole_view_ready` is the metric. Both candidates fail the quality gate.
|
||||
|
||||
Station×2 connector stations:
|
||||
|
||||
| Pair | L14 baseline | Station×2 | Change |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | 1.320 s | 1.304 s | −1.2% |
|
||||
| 2 | 1.363 s | 1.330 s | −2.4% |
|
||||
| 3 | 1.371 s | 1.332 s | −2.8% |
|
||||
| 4 | 1.393 s | 1.341 s | −3.7% |
|
||||
| 5 | 1.391 s | 1.345 s | −3.3% |
|
||||
| Median | 1.371 s | 1.332 s | −2.8% (3.3% median paired gain) |
|
||||
|
||||
Refinement grid scale ×2 at LOD4–8:
|
||||
|
||||
| Pair | L14 baseline | Refine×2 all | Change |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | 1.305 s | 1.164 s | −10.8% |
|
||||
| 2 | 1.336 s | 1.177 s | −11.9% |
|
||||
| 3 | 1.362 s | 1.169 s | −14.2% |
|
||||
| 4 | 1.378 s | 1.178 s | −14.5% |
|
||||
| 5 | 1.384 s | 1.190 s | −14.0% |
|
||||
| Median | 1.362 s | 1.177 s | −13.6% (14.0% median paired gain) |
|
||||
|
||||
## Hashes and invariants
|
||||
|
||||
The undeclared/default recipe path matches L14. LOD0–2 density/mesh/payload/topology hashes:
|
||||
|
||||
| LOD | Density | Mesh | Payload | Topology |
|
||||
|---:|---|---|---|---|
|
||||
| 0 | `769f2a706d585a72` | `33eeeb39fa443816` | `645621a1d968983f` | `c2457ed905036c9d` |
|
||||
| 1 | `69cc5a5c0a2c60af` | `b48335f5410c57d5` | `d15c5480162ab8ba` | `feca1bd025823604` |
|
||||
| 2 | `85deed5683a77202` | `53469895987aeea2` | `e55f8ae7e1532033` | `d5feb12a0e85c09b` |
|
||||
|
||||
Exact queries (density / unclamped / detail / collision) remain
|
||||
`e4d4a8eb95b35933 / e4d4a8eb95b35933 / efcba20c0c70c415 / e604823a249029bf`. The 24 m crossing's
|
||||
432 moved tiles match L14 at W1/W12 with Clang off and GCC default:
|
||||
`337ca62131576728 / 7612f48e2ddbbaab / 69a67ed4add83b87 / f11ac1ebd0f7ddb8` (D/M/P/T).
|
||||
Its LOD0–2 per-LOD hashes also match the three rows above.
|
||||
|
||||
Determinism passed 28/28 configurations: SurfaceWorld seed 0 and FloatingIslands seed 0, Clang 19
|
||||
FP-contract off and GCC 14 default, W1/W8/W12 and five W12 repeats. All per-LOD and whole-view
|
||||
hashes match L14. L13 route parity passed for SurfaceWorld seeds 0, 1, and 17 and FloatingIslands
|
||||
seed 0; page, native-only, higher-LOD, edit, and history mismatch counts are zero.
|
||||
|
||||
The L3 FP-contract preamble/self-test, L4 `FLazy`, L7 shared cache, L9/L12 proofs, L11 readiness and
|
||||
crash fix, L13 route binding, and CLAUDE.md async rules remain in place. The evaluator and optional
|
||||
recipe declaration are shared by all recipes. Builds and runs used CPUs 0–11, up to W12, normal
|
||||
priority, 6 GiB address-space limits, and the unchanged `powersave` governor. Peak RSS was 904,400 KB
|
||||
(GCC build); peak runtime RSS was 736,432 KB. No run exceeded the time or memory limits.
|
||||
|
||||
## Capture pairs
|
||||
|
||||
JPEGs are 1280×720 and each is under 74 KB. Before is L14; after is the rejected refinement×2
|
||||
candidate applied at LOD4–8.
|
||||
The L1 view uses `(0,-35,-487.94) m`, yaw 0°, pitch −22°. Horizon uses the same location at pitch −2°.
|
||||
The mesa frame aims from the L1 location at the L6 mesa band (`(-472,-1432,-463.151) m` in the
|
||||
gate) with yaw −161° and pitch 0.9°.
|
||||
|
||||
| View | Before | After refine×2 all |
|
||||
|---|---|---|
|
||||
| L1 camera | [l1-camera-before.jpg](captures/l1-camera-before.jpg) (59 KB) | [l1-camera-after-refineall.jpg](captures/l1-camera-after-refineall.jpg) (57 KB) |
|
||||
| Horizon | [horizon-before.jpg](captures/horizon-before.jpg) (63 KB) | [horizon-after-refineall.jpg](captures/horizon-after-refineall.jpg) (60 KB) |
|
||||
| LOD6 mesa rim | [lod6-mesa-rim-before.jpg](captures/lod6-mesa-rim-before.jpg) (74 KB) | [lod6-mesa-rim-after-refineall.jpg](captures/lod6-mesa-rim-after-refineall.jpg) (67 KB) |
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- The first Clang build wrapper exited 127 because `/usr/bin/time` is absent; the direct Clang
|
||||
`-ffp-contract=off` build succeeded. GCC 14 default also built successfully.
|
||||
- A later sequential-build monitor was interrupted after Clang completed and before its GCC step
|
||||
launched. Clang and GCC were rebuilt separately with the same compiler settings and 6 GiB cap.
|
||||
- The first hash matrix omitted `--seed=0` and was discarded. A second FloatingIslands matrix used
|
||||
the SurfaceWorld spawn camera; it was rerun with the L14 auto-surface camera. The corrected matrix
|
||||
passed all 28 configurations.
|
||||
- Initial SurfaceWorld route probes used no camera for seed 0 (the 2,370-tile bound), omitted
|
||||
`--region-crossing` for seed 0 (shape compiler safety bound), and used the fixed seed-0 camera for
|
||||
seed 1 (LOD2/exact-query parity mismatch; seed 17 was not run with that setup). They were rerun
|
||||
with L14's camera selection and region-crossing option; all four final route audits passed. The
|
||||
first 24 m crossing also omitted `--region-crossing` and exceeded the shape-build safety bound;
|
||||
all four corrected compiler/worker combinations passed.
|
||||
- The first connector audit compared aggregate counts. It was replaced with stable per-ray station
|
||||
ID comparison before the final gate. The initial L4 selected-tile gate missed a changed tile; the
|
||||
all-desired-tile coverage gate found and rejected it. The full-view gate is used in the table above.
|
||||
- The first JPEG encoding pass did not set CPU affinity or an address-space cap. All six files were
|
||||
re-encoded under CPUs 0–11 and the 6 GiB cap; sizes and pixels stayed the same.
|
||||
- The first five speed pairs included screen-gate color capture on ordinary streaming tiles. That
|
||||
diagnostic copy is now enabled only for gate runs; all five cost pairs were rerun and the table
|
||||
above uses only the final logs. The final exact gate passed and station×2 remained rejected.
|
||||
- No builds, captures, final audits, or cost runs timed out or breached their memory limits. The
|
||||
intentional candidate rejections are listed above, not treated as infrastructure failures.
|
||||
|
||||
Evidence logs are in `Saved/Linux`: the final baseline and station×2 all-tile gates are
|
||||
`l15-final-gatecolors-exact.log` and `l15-final-gatecolors-station2.log`; the best-payoff
|
||||
refinement×2 all-tile gate is `l15-final-gatecolors-refineall.log`. Single-LOD refinement coverage is in
|
||||
`l15-gate-alltiles-refine-l4x2.log` and `l15-gate-alltiles-refine-l5x2.log`. Single-LOD refinement
|
||||
and connector-ray probes are `l15-gate-refine-l{x}x2.log`, `l15-gate-reuse.log`, and
|
||||
`l15-gate-rays{14,13,11,7}.log`. Hash, route, and crossing evidence is in
|
||||
`l15-final-hash-audit.txt`, `l15-final-route-*.log`, and `l15-final-crossing-24m-*.log`; final
|
||||
station×2 speed pairs are `l15-final-speed-pair{1..5}-{before,after}.log`; refinement×2 pairs are
|
||||
`l15-final-speed-refineall-pair{1..5}-{before,after}.log`.
|
||||
|
||||
## Note for the owner
|
||||
|
||||
The default far terrain stays the same because every measured reduction exceeded the 1 px geometry
|
||||
or shading limit, worsened a swap, or opened holes. Refinement×2 across LOD4–8 saves 13.6% by the
|
||||
median whole-view ratio, but creates hundreds-pixel errors and visible gaps, so the captures show it
|
||||
for review only and it is not enabled.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- I looked at the LOD6 mesa-rim pair. After refine×2, the jagged crests are visibly smoothed, black
|
||||
holes appear in the cap/ceiling, and a dark sliver shows on the left horizon. That confirms the
|
||||
gate's rejection: the instrument is right this time, not the world.
|
||||
- **Not merged into `assembler/world`:** the candidate knobs and the evaluator 6→7 bump add no default
|
||||
output and would invalidate version stamps for nothing. The full L15 code (the extended gate with
|
||||
shading, swap and connector-ID audits, and the candidate controls) is kept on side branch
|
||||
`wip/l15-far-budget`. `assembler/world` stays at L14 code plus this report.
|
||||
- Conclusion of L14+L15: far terrain has no exact or within-budget saving left with today's design. The
|
||||
remaining far cost is the price of the current refinement and connector-repair geometry.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
Loaded 100 of 662 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user