Author SHA1 Message Date
Fr0zkaandClaude Opus 5.5 ec20994a8e streaming: near LODs first (walkable ring, LOD0, LOD1, LOD2, then far); exact passage trims (L78)
- Headless and Unreal schedulers drain the collision-critical ring, then LOD0-2, then far LODs
  at low priority. LOD0-2 ready (W8 medians): SW 4.36->1.76, FI 6.89->5.26, TN 4.88->3.22,
  VS 2.71->1.56 s. Whole view roughly unchanged (FI/VS slightly later).
  KNOWN RISK: far tiles wait until every near tile is ready, so far LODs may starve while
  moving fast (to address next).
- Exact composer trims: tile-level upper-mouth candidate list, segment-index min/max rejection
  before lower_bound, 64-entry thread-local value-noise corner cache. Passage share 65 -> 63%.
- World exact: 24/24 hash matrix + pinned L75 owner hash. Fast tier PASS. Linux UE host builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 19:13:45 +01:00
Fr0zkaandClaude Opus 5.5 5e091d254c profile: in-process SIGPROF sampler + exclusive route/phase census (L77); closes to 0.1%
Opt-in (L77 build + --linux77-profile; compiled out by default, 0.02% handler cost).
FI/TN cost account closes against direct grid-fill timing within 0.15% (LOD0-2 bins within 1.4%).
Passage carving = 66% of LOD0-2 CPU (52-54% whole view), passage support 10-14%, base packed
+ analytic field ~3%. Hotspots: value noise 17%, lower_bound in VisitNearestPassageSpatialNode
14%, ApplyResolvedStratePassageCarves 6.5%. LOD0/1 complete LAST (4.1/4.6 s) while LOD2 is
ready at 2.2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 17:55:39 +01:00
Fr0zkaandClaude Opus 5.5 68cba5a51d research: Astra pass 2, whole generation chain cost map (read-only)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 16:49:49 +01:00
Fr0zkaandClaude Opus 5.5 2dfe599148 research: Astra design review of the ~1 s whole-view goal (read-only)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 16:39:34 +01:00
Fr0zkaandClaude Opus 5.5 90378a585f profile: per-term legacy FI/TN sample profiler (L76); passage/support is ~75% of field time
Opt-in at build time (VOXELFORGE_PROFILE_LEGACY_TERMS=1; compiled out by default, ~2% when on).
Per legacy sample: passage/support composer ~1.05 us vs primitive scan ~0.19 us, boundary
band ~0.06 us, noise ~0.01 us. Resolved-passage constants are cached at field binding
(arithmetic order unchanged); whole-view effect within noise. Per-tile hashes identical to L75
in all 24 compiler x worker x view comparisons; fast tier PASS; Linux UE host builds.
R40 researcher notes attached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 16:25:19 +01:00
Fr0zkaandClaude Opus 5.5 9d5455ec68 skip: passage proof bounds include wall/floor relief (L75 soundness fix); FI -7.6%
L74's passage bound omitted the 0.21 m wall relief and 0.20 m floor relief, so its whole-tile
proof was unsound in principle (no tested view hit it). Passage support is now bounded per route
segment by four parameter-subinterval boxes using Catmull-Rom derivative extrema, local max
radius and relief padding. Stratum slicing only at LOD0-2 in FI/VS. Legacy sub-tile block
certificates are audit-only (runtime version was slower). TN regression (-80,-20,-3218) kept.

Exact full-grid vs skip hashes on every tile, 4 views, Clang/GCC, W1/W8/W12; control caught;
80-camera sweep 0 violations. Whole view vs c67b200: SW -1.3%, FI -7.6%, TN -2.3%, VS +0.2%.
Compiles in the Linux UE host (VFHostEditor, 27/27 actions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 14:34:49 +01:00
Fr0zkaandClaude Opus 5.5 c67b2006aa skip: sound whole-tile proofs for legacy FI/TN/VS fields (L74); FI -30%, TN -22%
The compiled-analytic stratum interval now certifies a legacy tile uniform only when it stays
beyond the detail envelope (and smooth-min slack for solid) with no intersecting primitive,
edit, native field, composer candidate, passage or active transition; everything else fails
closed. The stream layer uses the proof for whole-tile skips; legacy sub-tile skips stay off
(two block proofs failed the falsifier in TN).

Falsifier: full-grid reference vs skip-enabled run, identical mesh/payload/topology hashes on
every tile of all four owner views, Clang and GCC, W1/W8/W12; 20-camera sweep per strate,
0 violations; a too-tight control changed 3/184/233/238 tiles and was caught.
Whole view (8 workers): SW 2.61->2.48, FI 7.18->5.05, TN 5.97->4.63, VS 2.70->2.40 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 10:19:31 +01:00
Fr0zkaandClaude Opus 5.5 bd1b2a8f1d profile: per-LOD whole-view breakdown for the four-strate owner world (L73, no world change)
FI/TN/VS (legacy compiled-analytic) skip ZERO tiles at every LOD and evaluate ~44k density
points per LOD0/1 tile; SW skips 359/512 at LOD0. The whole-tile skip requires a native
tile with a uniform verdict, and the legacy paths return Mixed/unknown (R39 notes attached).
Opt-in stage timers, sampled field timer, per-LOD classification and composer counters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 08:48:57 +01:00
Fr0zkaandClaude Opus 5.5 d970d75be8 streaming: deeper ready queue + startup apply cap; tile-local mouth-support culling (L72)
- Unreal: ready queue depth 8 -> 16 (cap 4 worker-sets / 64), startup mesh-apply cap
  12 -> 24 within the unchanged 14 ms budget. Owner SW arrival, same 2,850 keys:
  initial set ready 4.78 -> 3.74 s, queue p95 123 -> 35 ms, request-to-apply p95
  139 -> 45 ms; walkable gate 1.07 -> 1.11 s; per-frame apply p95 0.73 ms.
- Core composer: mouth-support segments filtered by tile bounds, mouth relief result reused
  (route checks -80/-87% in passage tiles). Hashes identical W1/W8/W12, Clang and GCC.
- Opt-in per-tile composer counters. Verify control meshes stored as .xz (78.5 -> 18.4 MB).
- Fast tier 269 PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 08:25:14 +01:00
Fr0zkaandClaude Opus 5.5 971206c2ea trace: Unreal timing traces run without hash capture (L71); verify baselines tracked
The L70 "2x SurfaceWorld GenerateTile in Unreal" was the instrument: the trace enabled
bCaptureVoxelForgeCoreHashesForValidation, which disables the native heightfield-band
shortcut and whole-tile skips (W1 same keys: 9.00 s hash-off vs 28.46 s hash-on). With
hashing off, Unreal and headless match at W8 within 1.2-2.5% (Core 14.42 s vs 14.26 s) on
identical tile keys. LOD0 p95 tail = real cost of non-empty passage tiles near the spawn mouth
(p50/p95 18.8/105 ms); most LOD0 tiles are empty.

verify.py reads its baselines from Standalone/Tools/verify/baselines/ instead of untracked
Saved/Linux. The 11 .vfmesh control meshes (~78 MB) are not committed yet (to be stored
compressed). Fast tier green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 06:55:46 +01:00
Fr0zkaandClaude Opus 5.5 018c0e1ec8 verify: track the pinned owner hash baselines (L67/L68/L70); .gitignore had dropped them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 06:23:13 +01:00
Fr0zkaandClaude Opus 5.5 bd68aee1ca parity: Unreal and headless resolve the same four-strate plan (L70, v214)
- Export commandlet writes the live Core adapter's world_assembly + resolved layout pair;
  the headless owner path imports that pair (manifest/layout equal, layout_chunk_width_q
  validated). owner_world.json re-exported from the owner's staged assets on the Linux host.
- Adapter SW route: Core clearance/radius floors, 0.5 m support search, outward-facing
  tie-break; passage mouth/mid radius floors match Core (36/24 owner voxels).
- FI in Unreal now uses the legacy (compiled analytic) generator unless a plan is imported:
  the auto native-FI plan failed the passage audit on the owner plan (1843 blocked samples,
  945 open-air violations). Owner decides whether native FI returns.
- In-engine spawn traces per strate (3 each): SW GenerateTile 42.7 worker-s vs 20.1 headless
  for equal tile counts; LOD0 p95 45-169 ms in UE.
- Fast tier green; all three passages pass on the shared plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 06:22:48 +01:00
Fr0zkaandClaude Opus 5.5 81e2452a3d captures: readable player-view captures with a coverage gate (L69); R36 native-generator survey
- Capture exports now cover a 90-degree player frustum (radius 3, LOD 0-8, overlap patches);
  a 17x9 ray coverage probe aborts before rendering if tiles are missing (truncated-mesh
  control: 480 uncovered samples, rejected). Clear colour is sky blue, so un-meshed space can't
  pass as dark rock. 16 views of the owner's four-strate plan.
- Explore commandlet: finite XYZ spawn override for traces (Source/, Linux-compiled).
- In-engine four-strate trace NOT obtained: the engine-generated plan fails passage validation
  at a solid centerline voxel (~(-43,-34.5,-448.5) m) while the headless owner_world.json
  plan passes. Engine and headless plans diverge.
- R36 (read-only): order TN -> VS -> FI for native migration; native FI is reachable from the
  adapter but not selected in the owner run, cause unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 04:00:49 +01:00
Fr0zkaandClaude Opus 5.5 bd7bba994c passages: smooth 12 m lower-mouth radius taper; all four-strate passages pass (L68, v213)
The FI->TN wall beat (4.77 cm at 7.06 m) was the lower-mouth flare: the full mouth radius
was assigned to the penultimate control point, a one-segment radius jump. The shared route
builder now blends to the mouth radius over 12 m with a smoothstep. Wall ripple 1-2:
7.27 -> 1.62 cm. All three passages pass support/blocked/grade/ripple/naturalness; controls
detected. Fast tier 269/269 incl. the new-plan stage; W1/W8/W12 equal, Clang and GCC.
Hash pins: FI style 2 re-pinned at v213.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 03:17:27 +01:00
Fr0zkaandClaude Opus 5.5 087333bf8a passages: four-strate passage floors supported (L67, evaluator v212); 1-2 wall beat still red
- Shared composer: a passage's restored support is applied after a physically overlapping
  later bore, instead of being carved away by it (0-1: 59 -> 0 unsupported).
- Capsule clearance for support restoration is tested against nearby route segments only
  (1-2: 3 -> 0, 2-3: 2 -> 0 unsupported).
- L66 index switches replaced by a physical criterion (both mouths inside their strata bands,
  neither a SurfaceWorld landing).
- Audit: floor zero-crossing refined by bisection (the 0.2498 m "step" was the gauge);
  wall ripple keeps the authored radius, cubic detrend per 32 m window. Controls detected:
  L35 ribs, beads, synthetic 0.25 m step, L36 washboard, L23 terrace.
- Result: 0-1 and 2-3 pass all audits; 1-2 fails wall ripple 7.27 cm (limit 3.5) from an
  authored-radius beat at 7.06 m. Fast tier was green before the final gauge change and was
  not re-run after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 02:27:40 +01:00
Fr0zkaandClaude Opus 5.5 5e2fc53d39 planner: four-strate owner plan spawns (L66); pinned 2-strate regression fixture
- Core caps each passage mouth reach to its own strate's usable content band (owner
  DA_Strate1 exports ReachMax=5000 owner voxels, which put the SW->FI lower mouth 79.5 m
  below FI, inside TN).
- Unreal adapter: SW route search keeps the old 64 m candidates first, then a bounded
  single-mouth search with shorter lengths; logs candidate/rejection counts.
- Internal-chain passage floor/support tweaks (only with >2 strates): ripple 1-2 4.78->2.90 cm,
  2-3 3.57->0.10 cm. Interfaces still 0/3 walkable (59/3/2 unsupported).
- verify.py: legacy tiers use Standalone/data/regression/owner_world_2strate_l61.json; new
  four-strate stage reads its expectations from owner_world.json (currently red on interfaces).
- Unreal trace L14 vs HEAD, same assets (16-chunk FlatPlain): 12.20 vs 12.56 worker-s; no x2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-26 22:13:22 +01:00
Fr0zkaandClaude Opus 5.5 47148e0a0d build: VoxelForge compiles in Unreal 5.7 on Linux (L65); Linux-only Unreal trace timing fields
Linux UE builds failed on the Core wrapper module: the Linux-only PerModuleInline.gen.cpp saw Standalone/Compat
shadow headers instead of the engine's, and WITH_EDITOR was redefined under -Werror. Compat headers now forward
to the engine headers with include_next for generated Linux module TUs (canonical Core TUs opt into the Compat
definitions via CoreNamespacePreamble.h); CoreMinimal.h undefines WITH_EDITOR first. VoxelForgeCore.Build.cs
enables that forwarding and AVX2 on Linux only; VoxelForge.Build.cs disables Unity on Linux only. Portability:
std::int64_t consistently in VoxelContentManager.cpp; grouped &&/|| in VoxelRedefinedDensityField.cpp. Linux-only
trace fields split Core generateTile / mesh copy / RMC stream build. Standalone Clang and GCC whole-view hashes
are unchanged vs 3202193. The Unreal spawn trace is blocked by the owner's new assets (SW route sweep fails).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-26 19:19:07 +01:00
Fr0zkaandClaude Opus 5.5 e157847509 research(L64, R33): regression bisect -- Core +16% worker time L14->L61, not 2x; four-strate plan baseline
Per-round bisect of a176d07..3202193 (56 builds; timings inflated by concurrent UE builds) plus a clean
quiet-machine endpoint re-measure: L14 12.62 vs L61 14.65 worker-s (+16%), LOD0 3.99 vs 4.99 ms/tile. The
owner's Windows x2 is therefore mostly on the Unreal side or not like-for-like. Four-strate U2 plan: all
strates generate and are deterministic; passage 0-1 lower mouth is out of FI's band, 0/3 interfaces pass,
FI/TN/VS use legacy evaluators. R33: ranked Unreal-side suspects and Windows experiments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-26 18:38:27 +01:00
Fr0zkaandClaude Opus 5.5 9136f5f0e7 chore(unreal): U2 -- owner's four-generator plan staged; owner_world.json re-exported
Owner re-saved strates 2026-10-26: DA_Strate1 SurfaceWorld, 2 FloatingIslands,
3 TunnelNetwork, 4 VerticalShafts (default params). BuildHost copies and the
enforced SHA-256 tables updated (old U1 set kept in the report); the export
commandlet handles the four-strate plan; Standalone/data/owner_world.json
regenerated (SHA-256 F42F96AD...). One normal BuildHost gate passed (64/64
tiles) but its top-strate window produced 0 triangles, and a missing
NewDataAsset reference warns -- both to check. No measurements this round.
Report: Standalone/Research/WorldAssemblerM3/Unreal2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:18:19 +01:00
Fr0zkaandClaude Opus 5.5 fc9227f9e0 fix(unreal): U1 -- Unreal compiles again on L61; CoreGeometry asset section; versioned layout-unit upgrade
Windows MSVC catch-up after abyss L15-L61 (no Unreal there): 64-bit clipmap
shift (C4334), qualified VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, UHT
metadata and duplicate-signature fixes. Optional CoreGeometry section on
UVoxelStrateDefinition (native caves, lattice, profiled heightfields, named
stamps, rooms/tunnels; off/empty by default), resolved after passage planning.
VoxelForgeAssetVersion 2 converts strate height/transition/gap to metres on
load (exact, 0 m change on the owner's 5 assets; packages never saved).
Gates pass (SurfaceWorld, FloatingIslands, Core-off, M2, W1/W8/W16).
KNOWN REGRESSION vs L14 (2026-10-20, same command, same PC): full set 3.9 ->
5.37 s, worker-seconds 15.8 -> 30, LOD0 1.11 -> 1.69 s, ring 0.50 -> 0.80 s;
not yet attributed (Core L15-L61 vs Unreal side).
Report: Standalone/Research/WorldAssemblerM3/Unreal1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:12:08 +01:00
Fr0zkaandClaude Opus 5.5 32021930d2 research(R32, self-checked): first-principles work budget for ~1 s and the active-cell/sparse-brick path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-26 09:48:37 +01:00
Fr0zkaandClaude Opus 5.5 135957831b fix: sanitizer sweep across all generators; bound signed shifts/division/overflow at coordinate limits (L61); R31
New verify.py sanitize tier (Clang address,undefined,float-cast-overflow; GCC address,undefined;
-fno-sanitize-recover) runs 19 cases per compiler at LOD0-8 across every generator, passages and the
crossing: 0 runtime reports; a planted signed-overflow control is caught by both. A static scan fixed
INT_MIN negation in floor division, unbounded signed LOD shifts and coordinate-times-extent overflow in
VoxelClipmapDesiredTiles.h and VoxelTypes.h (plugin and standalone), and VoxelWorld.cpp, VoxelContentManager.cpp
and VoxelDensityVolume.cpp (plugin only, uncompiled here). Supported-input output unchanged (version 211).
fast/medium/full PASS. Research R31 (self-checked): far-LOD 4-8 cut plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-26 05:19:18 +01:00
Fr0zkaandClaude Opus 5.5 c0c204ba50 fix: signed-overflow UB in VoxelHash made cave placement compiler-dependent; all generators LOD0-8; speed matrix (L60)
VoxelHash::Cell/Pair, legacy Cellular3D and the shared Core cellular hash added 0x7FFFFFFF to a signed
int32 before casting to uint32; overflow is undefined, so Clang and GCC placed cave passage 1 differently
(first differing sample at LOD1 tile (0,64,-512)). The bias is now done in unsigned arithmetic, in both the
standalone copy and the Unreal plugin header (Source/VoxelForge/Public/VoxelCaveMorphology.h). Native
caves, motifs and lattice now stream LOD0-8 with skirts and cross-level transitions. New verify.py
--speed-matrix runs every generator on one asserted-identical view (5 interleaved runs): motifs 1.33,
profiles 1.30-1.32, caves 1.41, natural SW 1.67, lattice 1.70, FI 4.93 s whole view. fast/medium/full PASS,
Clang/GCC peer hashes equal. Research R29 (open-edge ledge) and R30 (compiler-divergence suspects).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-26 01:50:15 +01:00
Fr0zkaandClaude Opus 5.5 fbae82e024 research(R28, self-checked): per-generator cost ledger; caves/motifs/lattice timings were LOD0-only views, not comparable
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-25 20:27:56 +01:00
264 changed files with 17519 additions and 1770 deletions

No files matched your search

+45 -25
View File
@@ -2265,36 +2265,56 @@ mouth and off the walk route. It stays disabled by default. Active records carry
L43's report records the off/on hashes, walk and interface audits, outside-stamp timing, tier
results, captures and any failed runs.
### 8.54 Opt-in native lattice field — World Assembler M3 R26 slices 1–2 (Linux L58–L59)
### 8.54 Opt-in native lattice field — World Assembler M3 R26 slice 1 (Linux L58)
`Core.Lattice.PrimitiveField` is an opt-in recipe selected only for one Maze or VerticalShafts
`Core.Lattice.PrimitiveField` is a version-1 recipe selected only for one Maze or VerticalShafts
strate by `--recipe=lattice --lattice-strate=N`. The legacy generator assets and their default
dispatch stay unchanged. Version 2 binds the actual incoming lower mouth and outgoing upper mouth,
then creates arrival/exit overlap corridors, an open vertical shaft and the maze spine; two
deterministic dead ends are added after that route. Resolved room and edge seeds use
`(world seed, strate, recipe version, stable ID)`. The orphan control translates the shaft 20 m
away from both route junctions and must fail reachability.
dispatch stay unchanged. Planning first binds the actual incoming lower mouth and outgoing upper
mouth from the resolved passage records, then creates arrival/exit overlap corridors, a mandatory
four-turn helix and the maze spine; two deterministic dead ends are added after that route. The
resolved room and edge seeds use `(world seed, strate, recipe version, stable ID)`. An audit control
translates the helix 20 m away from both route junctions to make the shaft network orphaned.
The version-2 shaft core is a vertical cylinder of 4.5 m radius. Its air continues 8 m above the
top tread and 2 m below the bottom tread. The ramp winds around the core at a 6 m centreline radius,
with 3 m clear width, 5 m pitch and four turns. The open annular shaft air joins the core air, while
one-metre support slabs follow the helix under each tread. This leaves a 4 m vertical gap between
one tread and the underside of the next. The ledge's inner edge is open to the well; the route
centreline stays 1.5 m from the drop and leaves 1.16 m beyond the 0.34 m capsule radius. Its inner
edge grade is about 10.0 degrees. The maze retains 2.6 m room headroom. The shaft's padded outer
design envelope is 21 m. The 1.9 m floor-wall smooth-max gives an isolated ideal fillet radius of
0.6718 m; the separate 0.05 m feature-union blend keeps route junctions connected.
The prototype helix uses a 6 m centreline radius, 3 m clear width, 5 m pitch, four turns and 2.6 m
finished floor-to-soffit height. Its centreline descends 20 m over roughly 151 m; the inner-edge
grade is about 10.0 degrees. The 2.4 m floor-wall smooth-max gives an isolated ideal fillet radius
of 0.8485 m. A separate 0.4 m feature-union blend prevents adjoining ramps from erasing the tread.
The swept-clear diameter is 15 m; the conservative padded feature envelope is about 20.6 m (the
approved approximately 21 m site envelope).
The layout reuses the resolved cavern graph vocabulary. A per-strate opt-in selects the native
negative-solid/positive-air field; canonical feature order is stable-ID order. The ring-shaped shaft
air and its support layers use an analytic helix lookup instead of testing every polyline segment
per density sample. The core cylinder and annular wall have conservative interval bounds; false
all-air and all-solid controls remain rejected, and `isTileShapeFree` refuses occupied tiles. The
L59 pipeline checks the composed field from actual mouth to mouth with a 0.34 m by 1.76 m capsule,
support at every station, 2.2 m clearance and a 15 degree grade limit. It also checks the orphan
control, whole-surface ramp lip/wall-foot/underside, brute-force interval samples and the composed
wall-foot fillet. See the Linux58 and Linux59 reports for evidence. This code path is not promoted
to legacy Maze/VerticalShafts generation.
negative-solid/positive-air field; canonical feature order is stable-ID order. Room/corridor bounds
are indexed by the graph BVH and polyline segments have a nested deterministic BVH for density and
interval queries. Box classification stays conservative, and `isTileShapeFree` refuses any tile
that intersects the graph. The L58 pipeline audits the composed field from actual mouth to mouth
with a 0.34 m by 1.76 m capsule, per-station support, 2.2 m clearance and a 15 degree grade limit;
it also checks the orphan control, brute-force interval samples and the composed wall-foot fillet.
The Linux58 report contains the measured reachability, surface, peer-determinism, timing and capture
results; the code path is not promoted to legacy Maze/VerticalShafts generation.
### 8.55 Unreal CoreGeometry asset bridge and U1 asset version
`UVoxelStrateDefinition::CoreGeometry` is optional and defaults to recipe `None` with no stamps,
rooms, or tunnels. The Unreal adapter preserves the established passage planner, then resolves
authored stamps and cavern graph records, then finalizes the layout. Stable names are required and
are scoped by strate in Core; landmarks may target a stamp, room, or tunnel by that stable name.
Core profile heightfields are restricted to SurfaceWorld. Native caves require the TunnelNetwork or
Underwater archetype; native lattice requires Maze or VerticalShafts. Core recipe/version fields
enter the resolved layout identity when selected. An imported manifest cannot be mixed with a
per-strate profiled heightfield.
The checked-out Core API contains stamp vocabularies V1, V2, and V3. Unreal uses V1 for the original
stamp kinds and V3 for monolith/platform motifs, then combines and strictly validates the result.
Research17 names V2, but the source API and motif vocabulary require V3; source was followed.
Research27 water remains out of scope for U1.
The shared VoxelForge custom asset version is 2: version 1 records the tunnel-floor slope migration;
version 2 adds load-time conversion for inter-strate gap, strate height, and transition width. The
saved strate-layout width is used, currently 8 m. Old packages are converted in memory and marked
dirty, but U1 does not save them. This does not yet migrate the rest of Research8's reflected
spatial-property inventory: legacy chunk distances, owner/Core voxel lengths and frequencies,
placement vectors, and other centimetre fields remain in their prior units. The U1 report records
the five BuildHost asset values, hashes, and remaining work.
## 9. Multiplayer model (listen-server first, dedicated-friendly)
+48 -20
View File
@@ -88,19 +88,22 @@ therefore **128 m**, and a 64-chunk strate is **512 m**. Core still stores work
at 25 cm those tiles happen to span 8 m, but they are not the layout unit. The gameplay capsule remains
**0.34 m radius / 1.76 m tall** (**2.72 / 14.08 legacy voxels**). Research-field dimensions are authored
in metres and converted at runtime. The live
strate generator continues to read voxel-valued parameters from its assets, so those authored
dimensions become half as large in metres unless their asset values are doubled.
strate generator continues to read voxel-valued shape parameters from its assets, so those authored
dimensions become half as large in metres unless their asset values are doubled. U1 adds a
versioned in-memory conversion for strate height, transition width, and inter-strate gap only; the
broader Research8 voxel/chunk/centimetre inventory is not yet migrated.
The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells
**32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m**
radius; **3.0625 m** worst-case usable floor after roughness), slab open span **22.4 m**, shaft
diameters **4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. Source
new fixed physical dimensions are authored in metres and converted to voxel counts at the boundary.
The owner's voxel-valued live strate assets are not migrated by C++: unchanged saved values therefore
produce half-size rooms, tunnels, features and noise periods in metres. See the step-3 migration list
in the handoff report for the exact `DA_Settings` fields and values.
The owner's voxel-valued live strate shape settings are not migrated by U1: unchanged saved values
therefore produce half-size rooms, tunnels, features and noise periods in metres. See the U1 report
for the conversion coverage and remaining Research8 fields.
**Owner asset migration (no packages were edited):** preserve `DA_Settings` distances by changing
**Prior reauthoring recommendation (not applied in U1; no packages were edited):** preserve
`DA_Settings` distances by changing
`ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`,
`StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`,
`EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region
@@ -110,8 +113,9 @@ size `8→16 cells`, spacing/far spacing `8→16 voxels`, march step `4→8 voxe
`12→24 cells`; it is currently disabled. Keep `MaxClipLevel=6` and zero-valued disabled/unbounded
limits unchanged; double finite world radii and gaps, and multiply finite voxel-volume caps by 8.
The loaded `Crystal Cave` slot in `DA_Strate3` is 64 chunks tall; use 128 chunks to preserve its
former 512 m span. Its voxel-authored live-generator lengths remain half-size until that asset is
re-authored; the editor-only research field is the step-3 target.
former 512 m span. U1 automatically converts only strate height and transition widths and the
settings gap at load; the remaining recommendations above are pending. Its voxel-authored
live-generator lengths remain half-size until their asset fields are migrated.
Step-3 six-worker scale benchmark (idle machine, `Saved/Research/step3/scale/step3_scale_cacheperf_v1`):
the 519³ field took **7.133834–7.825292 s** across three runs (median **7.347496 s**); the 259³
@@ -170,10 +174,11 @@ remains `0`, and lateral regions remain gated off.
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. |
| `NativeLatticeFieldRecipeVersion`, `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit Maze/VerticalShafts opt-in at lattice recipe version 2; stable feature seeds include world seed, strate, recipe version and feature ID. §8.54. |
| `FCavernRuntimeFeature::openShaftRamp`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | Analytic open shaft ring, helical support lookup, exact cylinder core bounds and conservative interval proofs; the outer graph BVH supplies tile candidates and protects `isTileShapeFree`. §8.54. |
| `ResolvedLayoutStratePassageCapabilitiesV1::{hasNativeCaveField,hasNativeLatticeField}`, `IsNativeLatticeFieldStratum`, `HasNativeGraphField` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit per-strate native graph capability; `Core.Lattice.PrimitiveField` is restricted to Maze/VerticalShafts recipe version 1. Lattice seeds include world seed, strate, recipe version and stable feature ID; explicit polyline records keep their endpoints and receive conservative bounds. §8.54. |
| `FCavernRuntimeFeature::FSegmentNode`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | A deterministic nested segment BVH accelerates native graph distance and conservative interval evaluation; the outer graph BVH still supplies tile candidates and protects `isTileShapeFree`. The 0.4 m feature-union width is separate from the floor fillet width. §8.54. |
| `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
| `ApplyOwnerFourStratePlan` | `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` | BuildHost-only transient fixed-slot plan from the four staged owner strate assets; leaves the authored settings and strate packages untouched. |
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|standalone-export|compile-only|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, allows the owner's interactive editor while refusing overlapping `UnrealEditor-Cmd` launches, runs owned processes at Normal priority with a 12-logical-CPU cap, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. |
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; schema-v1 owner-asset export check and BuildHost Core actor tests. `-coreworldassembly=PATH` supplies a reviewed manifest transiently to `-coreworldtest=1`; it does not save a data asset. `FStandaloneCoreAdapterScheduler` runs Core work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. |
@@ -1310,19 +1315,42 @@ See ARCHITECTURE §8.53 and
[`Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md)
for layout identities, owner hashes, outside-box timing, audits and captures.
### 3.36 Native lattice field — World Assembler M3 R26 slices 1–2 (Linux L58–L59)
### 3.36 Native lattice field — World Assembler M3 R26 slice 1 (Linux L58)
| Symbol | Canonical file | Role |
|---|---|---|
| `NativeLatticeFieldRecipeVersion`, `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Version-2 Maze/VerticalShafts opt-in and deterministic seed from `(seed, strate, recipe version, stable ID)`. |
| `MakeNativeLatticeSpine`, `--recipe=lattice`, `--lattice-strate=N` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Resolves actual adjacent mouths, builds the radius-4.5 m open shaft core, supported four-turn wall ledge and maze spine, then adds two seeded dead ends; the orphan control translates the shaft away from both junctions. |
| `AuditNativeLatticeField` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the final field along the mouth-to-mouth capsule route, checking every-station support, headroom, grade, orphan rejection, composed fillet, shaft air/wall intervals and tile exclusion. |
| `BuildLatticeFixtureL59.py`, `CaptureNativeLatticeL59.py`, `AuditLatticeSurfaceL59.py` | `Standalone/Research/WorldAssemblerM3/Linux59/` | Isolated Maze fixture, four JPEG views of the open shaft and maze, and L54 whole-surface audit extended to include the ramp underside. |
| `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed`, `ResolvedCavernRoomV1::unionWidthMicrometres`, `ResolvedCavernEdgeV1::unionWidthMicrometres` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Versioned Maze/VerticalShafts opt-in and deterministic seed from `(seed, strate, recipe version, stable ID)`; optional union width preserves prior cave JSON when unset. |
| `MakeNativeLatticeSpine`, `--recipe=lattice`, `--lattice-strate=N` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Resolves actual adjacent mouths, builds a mandatory four-turn helix and maze spine, then adds two seeded dead ends; `--lattice-orphan-control` moves the helix away from both junctions. |
| `AuditNativeLatticeField` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the final field along the mouth-to-mouth capsule route, checking support, headroom, grade, orphan rejection, composed fillet, interval signs and feature-tile exclusion. |
| `BuildLatticeFixtureL58.py`, `CaptureNativeLatticeL58.py`, `AuditLatticeSurfaceL58.py` | `Standalone/Research/WorldAssemblerM3/Linux58/` | Isolated Maze strate fixture, four reviewed captures and the L54 whole-surface triangle audit adapted to explicit native polylines. |
See ARCHITECTURE §8.54 and the
[`Linux59 report`](Standalone/Research/WorldAssemblerM3/Linux59/REPORT.md) for the open shaft's
measured geometry, final-field reachability, controls, timing, hashes and captures. The L58 report
retains the first-slice evidence.
See ARCHITECTURE §8.54 and
[`Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md)
for resolved records, final-field reachability, controls, timing, hashes and captures.
### 3.37 Unreal CoreGeometry assets and partial metre upgrade — World Assembler U1
`Public/VoxelStrateTypes.h` declares optional `FVoxelForgeCoreGeometry`. Its default recipe is Off
and its stamp, room, and tunnel arrays are empty. `Private/VoxelForgeCoreAdapter.cpp` maps authored
metres to Core layout requests after passage planning and before finalizing the layout. Stamps use
the V1 resolver for original stamp kinds and V3 for monolith/platform motifs because the checked-out
Core API exposes V3; authored cavern records use `ResolveResolvedCavernGraphV1`. Stable feature
names are namespaced by strate in the resolved layout. `FStrateLandmark::CoreGeometryStableName`
binds one landmark to such a resolved record, and `VoxelContentManager.cpp` builds its lookup from
the finalized snapshot. `VoxelWorld.cpp` retains resolved layout identity in content hashing.
`Private/VoxelForgeAssetVersion.h` sets the shared custom version to 2. `UVoxelSettings::Serialize`
and `UVoxelStrateDefinition::Serialize`/`PostLoad` upgrade only inter-strate gap, strate height,
and transition width using the saved 8 m layout chunk. These conversions are version-guarded and
idempotent. No package save is performed by the audit. Other Research8 chunks, owner-voxel lengths,
Core-voxel lengths/frequencies, and centimetre fields remain on their existing units and need a
separate migration.
Likely compile/UHT touchpoints are the reflected nested fields in `VoxelStrateTypes.h`, Core snapshot
and resolver signatures in `VoxelForgeCoreAdapter.cpp`, `FCustomVersionRegistration` and the old
tunnel-slope version in `VoxelStrateDefinition.cpp`, plus the standalone `VoxelForgeExplore` audit
in `VoxelForgeExploreCommandlet.cpp`. `GetStrateForChunk` still receives layout-chunk Z, not a Core
tile index. See `Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md` for BuildHost evidence.
## 4. The density pipeline (most-edited hot path)
@@ -399,6 +399,7 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
LegacySettings->WorldRadiusVoxels = 987.0f;
LegacySettings->TotalStrates = 1;
LegacyDefinition->StrateHeightInChunks = 7;
LegacyDefinition->StrateHeightMeters = 7.0 * 8.0;
LegacySettings->StratePool.Add(
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
@@ -525,6 +525,7 @@ bool FVoxelForgeZeroTransitionBlendTest::RunTest(const FString& Parameters)
{
UVoxelStrateDefinition* Definition = World.Definitions[Index].Get();
Definition->TransitionBlendChunks = 0;
Definition->TransitionWidthMeters = 0.0;
Definition->TransitionType = (Index & 1) == 0
? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved;
}
@@ -44,6 +44,7 @@ namespace
Settings->Seed = LayoutSeed;
Settings->TotalStrates = NumPoolDefinitions;
Settings->InterStrateGapChunks = 1;
Settings->InterStrateGapMeters = Settings->StrateLayoutChunkSizeMeters;
Settings->bOpenSurfaceEntry = true;
static const ECaveGeneratorType GeneratorTypes[] = {
@@ -69,6 +70,8 @@ namespace
FString::Printf(TEXT("Layout order test strate %d"), Index));
Def->GeneratorType = GeneratorTypes[Index];
Def->StrateHeightInChunks = 3 + Index;
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
* Settings->StrateLayoutChunkSizeMeters;
Def->TransitionType = EVoxelStrateTransition::Hard;
// Non-trivial, distinct passage configs exercise the connection index and the
@@ -79,6 +79,8 @@ namespace VoxelForgeTest
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
Settings->Seed = InSeed;
Settings->InterStrateGapChunks = InGapChunks;
Settings->InterStrateGapMeters = static_cast<double>(InGapChunks)
* Settings->StrateLayoutChunkSizeMeters;
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
// WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0.
@@ -110,6 +112,8 @@ namespace VoxelForgeTest
// refinement and roll tests use the smaller synthetic volume as a bounded memory
// control; the owner-facing showcase opts into 8 chunks below.
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
* Settings->StrateLayoutChunkSizeMeters;
// Hard transitions: param blending across a boundary would make "which archetype
// owns this chunk" ambiguous, and these tests want an unambiguous mapping.
Def->TransitionType = EVoxelStrateTransition::Hard;
@@ -4,6 +4,27 @@
namespace VoxelClipmapDesiredTiles
{
FORCEINLINE int64 FloorDivWide(int64 Value, int64 Divisor)
{
if (Divisor <= 0) { return 0; }
const int64 Quotient = Value / Divisor;
const int64 Remainder = Value % Divisor;
return Remainder < 0 ? Quotient - 1 : Quotient;
}
FORCEINLINE int64 CeilDivWide(int64 Value, int64 Divisor)
{
if (Divisor <= 0) { return 0; }
const int64 Quotient = Value / Divisor;
const int64 Remainder = Value % Divisor;
return Remainder > 0 ? Quotient + 1 : Quotient;
}
FORCEINLINE int32 ClampInt32(int64 Value)
{
return static_cast<int32>(FMath::Clamp<int64>(Value, MIN_int32, MAX_int32));
}
struct FParameters
{
int32 ClipRadius = 3;
@@ -13,7 +34,10 @@ namespace VoxelClipmapDesiredTiles
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
{
return Value >= 0 ? (Value / Divisor) : -(((-Value) + Divisor - 1) / Divisor);
if (Divisor <= 0) { return 0; }
const int32 Quotient = Value / Divisor;
const int32 Remainder = Value % Divisor;
return Remainder < 0 ? Quotient - 1 : Quotient;
}
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
@@ -24,7 +48,10 @@ namespace VoxelClipmapDesiredTiles
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
{
return -FloorDiv(-Value, Divisor);
if (Divisor <= 0) { return 0; }
const int32 Quotient = Value / Divisor;
const int32 Remainder = Value % Divisor;
return Remainder > 0 ? Quotient + 1 : Quotient;
}
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
@@ -33,10 +60,12 @@ namespace VoxelClipmapDesiredTiles
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
int32& OutAxisMin, int32& OutAxisMax)
{
const int32 BaseMin = CenterAxis - Radius;
const int32 BaseEnd = CenterAxis + Radius + 1;
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
const int64 SafeRadius = FMath::Max<int64>(0, Radius);
const int64 SafeAlignment = FMath::Max<int64>(1, Alignment);
const int64 BaseMin = static_cast<int64>(CenterAxis) - SafeRadius;
const int64 BaseEnd = static_cast<int64>(CenterAxis) + SafeRadius + 1;
OutAxisMin = ClampInt32(FloorDivWide(BaseMin, SafeAlignment) * SafeAlignment);
OutAxisMax = ClampInt32(CeilDivWide(BaseEnd, SafeAlignment) * SafeAlignment - 1);
};
AlignAxis(Center.X, OutMin.X, OutMax.X);
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
@@ -49,8 +78,13 @@ namespace VoxelClipmapDesiredTiles
{
return Radius;
}
return FMath::Max(Radius,
(Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel);
const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8);
const int64 Scale = int64{1} << static_cast<uint32>(SafeLevel);
const int64 Numerator = static_cast<int64>(Parameters.RenderDistanceChunks)
+ Scale - 1;
const int64 DesiredRadius = Numerator / Scale;
return static_cast<int32>(FMath::Clamp<int64>(
FMath::Max<int64>(Radius, DesiredRadius), 0, MAX_int32));
}
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
@@ -73,12 +107,12 @@ namespace VoxelClipmapDesiredTiles
return;
}
OutMinChunkZ = CenterChunkZ - ViewDistanceDown;
OutMaxChunkZ = CenterChunkZ + ViewDistanceUp;
OutMinChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) - ViewDistanceDown);
OutMaxChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) + ViewDistanceUp);
if (bHasStrate)
{
OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks;
OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks;
OutMinChunkZ = ClampInt32(static_cast<int64>(StrateBottomChunkZ) - StrateViewMarginChunks);
OutMaxChunkZ = ClampInt32(static_cast<int64>(StrateTopChunkZ) + StrateViewMarginChunks);
}
}
@@ -94,7 +128,7 @@ namespace VoxelClipmapDesiredTiles
for (int32 Level = 0; Level <= MaxLevel; ++Level)
{
const int32 Pow = 1 << Level;
const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
const FIntVector CenterLevel = FloorDiv(Center, Pow);
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
@@ -105,24 +139,23 @@ namespace VoxelClipmapDesiredTiles
FIntVector RingMin, RingMax;
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
int64 TileMinZ = RingMin.Z;
int64 TileMaxZ = RingMax.Z;
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
{
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
TileMinZ = FMath::Max<int64>(TileMinZ, FloorDiv(MinChunkZ, Pow));
TileMaxZ = FMath::Min<int64>(TileMaxZ, FloorDiv(MaxChunkZ, Pow));
}
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
for (int32 DeltaX = RingMin.X - CenterLevel.X;
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
for (int64 Z = TileMinZ; Z <= TileMaxZ; ++Z)
for (int64 Y = RingMin.Y; Y <= RingMax.Y; ++Y)
for (int64 X = RingMin.X; X <= RingMax.X; ++X)
{
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
const int32 TileMinZ = Tile.Z << Level;
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
const FIntVector Tile(static_cast<int32>(X), static_cast<int32>(Y),
static_cast<int32>(Z));
const int64 TileBottomChunkZ = static_cast<int64>(Tile.Z) * Pow;
const int64 TileTopChunkZ = (static_cast<int64>(Tile.Z) + 1) * Pow - 1;
if (TileTopChunkZ < MinChunkZ || TileBottomChunkZ > MaxChunkZ)
{
continue;
}
@@ -130,12 +163,12 @@ namespace VoxelClipmapDesiredTiles
if (Level > 0)
{
const bool bCovered =
(2 * Tile.X >= PreviousRingMin.X)
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
&& (2 * Tile.Y >= PreviousRingMin.Y)
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
&& (2 * Tile.Z >= PreviousRingMin.Z)
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
(2 * static_cast<int64>(Tile.X) >= PreviousRingMin.X)
&& (2 * static_cast<int64>(Tile.X) + 1 <= PreviousRingMax.X)
&& (2 * static_cast<int64>(Tile.Y) >= PreviousRingMin.Y)
&& (2 * static_cast<int64>(Tile.Y) + 1 <= PreviousRingMax.Y)
&& (2 * static_cast<int64>(Tile.Z) >= PreviousRingMin.Z)
&& (2 * static_cast<int64>(Tile.Z) + 1 <= PreviousRingMax.Z);
if (bCovered)
{
continue;
+124 -24
View File
@@ -18,6 +18,7 @@
#include "Materials/MaterialInterface.h"
#include "Tasks/Task.h"
#include "HAL/PlatformProcess.h"
#include "Misc/Crc.h"
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
#include <VoxelForgeCore/VoxelForgeCore.h>
#include <cmath>
@@ -240,10 +241,10 @@ static std::vector<VoxelForgeCore::WorldAssemblyFeatureQueryV1> VF_ContentFeatur
{
using namespace VoxelForgeCore;
constexpr double Q = WorldAssemblySubvoxelsPerVoxel;
const std::array<int64,3> Min{static_cast<int64>(std::floor(MinX*Q)),
static_cast<int64>(std::floor(MinY*Q)),static_cast<int64>(std::floor(MinZ*Q))};
const std::array<int64,3> Max{static_cast<int64>(std::ceil(MaxX*Q)),
static_cast<int64>(std::ceil(MaxY*Q)),static_cast<int64>(std::ceil(MaxZ*Q))};
const std::array<std::int64_t,3> Min{static_cast<std::int64_t>(std::floor(MinX*Q)),
static_cast<std::int64_t>(std::floor(MinY*Q)),static_cast<std::int64_t>(std::floor(MinZ*Q))};
const std::array<std::int64_t,3> Max{static_cast<std::int64_t>(std::ceil(MaxX*Q)),
static_cast<std::int64_t>(std::ceil(MaxY*Q)),static_cast<std::int64_t>(std::ceil(MaxZ*Q))};
std::vector<WorldAssemblyFeatureQueryV1> Queries;
if (Surface)
if (const auto* Plan = Surface->FindPlan(StrateIndex))
@@ -322,11 +323,54 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
Seed = InSeed;
NativeSurfaceSnapshot.Reset();
NativeFloatingIslandSnapshot.Reset();
CoreGeometryAnchors.Reset();
if (Settings)
{
FString ManifestJson, ManifestHash, ManifestError;
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
FString ResolvedLayoutJson, ResolvedLayoutIdentity;
if (VoxelWorld != nullptr
&& VoxelWorld->GetVoxelForgeCoreResolvedLayout(ResolvedLayoutJson, ResolvedLayoutIdentity))
{
FTCHARToUTF8 Utf8Layout(*ResolvedLayoutJson);
std::string LayoutError;
const std::optional<VoxelForgeCore::ResolvedLayoutSnapshotV3> ResolvedLayout =
VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(
std::string_view(Utf8Layout.Get(), static_cast<std::size_t>(Utf8Layout.Length())), LayoutError);
if (ResolvedLayout)
{
const auto AddAnchor = [this](const std::string& StableId, int32 StrateIndex,
const std::array<std::int64_t, 3>& PositionMicrometres)
{
const FVector PositionCm(
static_cast<double>(PositionMicrometres[0]) / 10000.0,
static_cast<double>(PositionMicrometres[1]) / 10000.0,
static_cast<double>(PositionMicrometres[2]) / 10000.0);
const FString FullName = UTF8_TO_TCHAR(StableId.c_str());
FResolvedCoreGeometryAnchor Anchor{ StrateIndex, PositionCm };
CoreGeometryAnchors.Add(FullName, Anchor);
const int32 Marker = FullName.Find(TEXT("/core/"), ESearchCase::CaseSensitive, ESearchDir::FromEnd);
if (Marker != INDEX_NONE)
{
const FString ShortName = FullName.Mid(Marker + 6);
if (!ShortName.IsEmpty()) CoreGeometryAnchors.Add(ShortName, Anchor);
}
};
for (const VoxelForgeCore::ResolvedLayoutStampV1& Stamp : ResolvedLayout->stamps)
AddAnchor(Stamp.stableId, Stamp.strateIndex, Stamp.positionMicrometres);
for (const VoxelForgeCore::ResolvedCavernRoomV1& Room : ResolvedLayout->cavernRooms)
AddAnchor(Room.stableId, Room.strateIndex, Room.centerMicrometres);
for (const VoxelForgeCore::ResolvedCavernEdgeV1& Edge : ResolvedLayout->cavernEdges)
if (!Edge.polylineMicrometres.empty())
AddAnchor(Edge.stableId, Edge.strateIndex, Edge.polylineMicrometres.front());
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[VoxelContentManager] Could not read resolved Core geometry anchors: %s"),
UTF8_TO_TCHAR(LayoutError.c_str()));
}
}
const bool bHasRuntimeAssembly = VoxelWorld != nullptr
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
if (!bHasRuntimeAssembly)
@@ -559,9 +603,13 @@ int32 UVoxelContentManager::RegionSize() const
// truncation, a negative-coord cell is queued under one region but routed back to another → its march
// result is dropped → blank chunk. Truncation == floor for A>=0 and for R==1, which is exactly why only
// NEGATIVE coordinates at R>=2 were affected. R (region size) is always > 0 here.
static FORCEINLINE int32 FloorDivPos(int32 A, int32 R)
static FORCEINLINE int32 FloorDivPos(int64 A, int32 R)
{
return (A >= 0) ? (A / R) : -(((-A) + R - 1) / R);
if (R <= 0) { return 0; }
const int64 Quotient = A / R;
const int64 Remainder = A % R;
return static_cast<int32>(FMath::Clamp<int64>(
Remainder < 0 ? Quotient - 1 : Quotient, MIN_int32, MAX_int32));
}
static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
@@ -573,10 +621,10 @@ static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell,
int32 FarR, int32 R)
{
const int32 MinX = FloorDivPos(PlayerCell.X - FarR, R);
const int32 MaxX = FloorDivPos(PlayerCell.X + FarR, R);
const int32 MinY = FloorDivPos(PlayerCell.Y - FarR, R);
const int32 MaxY = FloorDivPos(PlayerCell.Y + FarR, R);
const int32 MinX = FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R);
const int32 MaxX = FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R);
const int32 MinY = FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R);
const int32 MaxY = FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R);
return Region.X >= MinX && Region.X <= MaxX && Region.Y >= MinY && Region.Y <= MaxY;
}
@@ -1038,8 +1086,19 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual,
FMath::Abs(static_cast<double>(Density)));
++OutAudit.AnchorChecks;
if (FMath::Abs(Density)>0.25f)
{ OutError=TEXT("Populated fixture is not anchored on the canonical field."); return false; }
// Core content crossings use the same finite-step column refinement as runtime
// placement. Allow at most half a Core voxel of field residual in this audit.
// Les croisements de contenu Core gardent le raffinement discret du runtime;
// l'audit tolere au plus un demi-voxel Core de residu de champ.
if (FMath::Abs(Density)>0.5f)
{
OutError=FString::Printf(
TEXT("Populated fixture is not anchored on the canonical field: residual=%.4f core_vox=(%.3f,%.3f,%.3f) local_cm=(%.2f,%.2f,%.2f)."),
Density, Local.X/CurrentCtx.VoxelSizeCm,
Local.Y/CurrentCtx.VoxelSizeCm, Local.Z/CurrentCtx.VoxelSizeCm,
Local.X, Local.Y, Local.Z);
return false;
}
}
}
if (!Spawns.IsEmpty())
@@ -1102,17 +1161,22 @@ void UVoxelContentManager::RebuildDesiredCells(FDecoGrid& G, const FIntPoint& Pl
const int32 R = RegionSize();
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
const FIntPoint RMin(FloorDivPos(PlayerCell.X - FarR, R),
FloorDivPos(PlayerCell.Y - FarR, R));
const FIntPoint RMax(FloorDivPos(PlayerCell.X + FarR, R),
FloorDivPos(PlayerCell.Y + FarR, R));
const FIntPoint RMin(FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R),
FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R));
const FIntPoint RMax(FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R),
FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R));
TSet<FIntPoint> DesiredRegions;
DesiredRegions.Reserve((RMax.X - RMin.X + 1) * (RMax.Y - RMin.Y + 1));
for (int32 ry = RMin.Y; ry <= RMax.Y; ++ry)
for (int32 rx = RMin.X; rx <= RMax.X; ++rx)
constexpr int64 MaxRegionReserve = 4096;
const int64 WidthX = static_cast<int64>(RMax.X) - RMin.X + 1;
const int64 WidthY = static_cast<int64>(RMax.Y) - RMin.Y + 1;
const int64 ReserveCount = WidthY > 0 && WidthX > MaxRegionReserve / WidthY
? MaxRegionReserve : FMath::Max<int64>(0, WidthX * WidthY);
DesiredRegions.Reserve(static_cast<int32>(ReserveCount));
for (int64 ry = RMin.Y; ry <= RMax.Y; ++ry)
for (int64 rx = RMin.X; rx <= RMax.X; ++rx)
{
DesiredRegions.Add(FIntPoint(rx, ry));
DesiredRegions.Add(FIntPoint(static_cast<int32>(rx), static_cast<int32>(ry)));
}
// Unload loaded regions no longer desired (plain DestroyComponent — no per-instance removal).
@@ -1464,8 +1528,15 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
const int32 Span = FMath::Max(0, Comp.CountMax - Comp.CountMin);
const int32 Count = Comp.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(CH ^ 0x1234u)) * (float)(Span + 1));
const int64 Span = FMath::Max<int64>(0,
static_cast<int64>(Comp.CountMax) - Comp.CountMin);
const uint64 CountRange = static_cast<uint64>(Span) + 1u;
const float CountOffset = VoxelHash::ToFloat01(
VoxelHash::Mix(CH ^ 0x1234u)) * static_cast<float>(CountRange);
const int64 CountWide = static_cast<int64>(Comp.CountMin)
+ static_cast<int64>(CountOffset);
const int32 Count = static_cast<int32>(FMath::Clamp<int64>(
CountWide, MIN_int32, MAX_int32));
for (int32 ii = 0; ii < Count; ++ii)
{
if (CompBudget <= 0) break;
@@ -1514,8 +1585,15 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
const int32 SubSpan = FMath::Max(0, Sub.CountMax - Sub.CountMin);
const int32 SubCount = Sub.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(SH ^ 0x1234u)) * (float)(SubSpan + 1));
const int64 SubSpan = FMath::Max<int64>(0,
static_cast<int64>(Sub.CountMax) - Sub.CountMin);
const uint64 SubCountRange = static_cast<uint64>(SubSpan) + 1u;
const float SubCountOffset = VoxelHash::ToFloat01(
VoxelHash::Mix(SH ^ 0x1234u)) * static_cast<float>(SubCountRange);
const int64 SubCountWide = static_cast<int64>(Sub.CountMin)
+ static_cast<int64>(SubCountOffset);
const int32 SubCount = static_cast<int32>(FMath::Clamp<int64>(
SubCountWide, MIN_int32, MAX_int32));
for (int32 sj = 0; sj < SubCount; ++sj)
{
if (CompBudget <= 0) break;
@@ -2374,6 +2452,28 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
if (ExclWorld > 0.0f) bAnyExclusion = true;
if (!L.CoreGeometryStableName.IsEmpty())
{
const FResolvedCoreGeometryAnchor* Anchor = CoreGeometryAnchors.Find(L.CoreGeometryStableName);
if (Anchor == nullptr || Anchor->StrateIndex != StrateIndex) continue;
const uint32 StableNameHash = FCrc::StrCrc32(*L.CoreGeometryStableName);
const uint32 H = DecoHash(static_cast<int32>(StableNameHash), StrateIndex,
0, 0, 0, EntryIdx, LocalSeed, 0xC04E6E0u);
if (VoxelHash::ToFloat01(H) > L.SpawnProbability) continue;
const float LocalX = static_cast<float>(Anchor->LocalPositionCm.X);
const float LocalY = static_cast<float>(Anchor->LocalPositionCm.Y);
const float dx = LocalX - LocalPlayer.X, dy = LocalY - LocalPlayer.Y;
const float DistSq = dx * dx + dy * dy;
if (DistSq <= GatherWorld * GatherWorld)
{
// A Core-bound landmark is one stable candidate, independent of the hash lattice.
// Un landmark lie a Core reste un candidat stable, independant de la grille de hachage.
Cands.Add({ FIntVector(EntryIdx, 0, EntryIdx), EntryIdx, H, LocalX, LocalY,
ExclWorld, L.Priority, DistSq <= RadiusWorld * RadiusWorld });
}
continue;
}
if (L.AnchorMode == ELandmarkAnchor::HashLattice)
{
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
@@ -155,9 +155,11 @@ int32 UVoxelDensityVolume::NumLevels() const
FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B)
{
// True floor division (B > 0). FMath::DivideAndRoundDown truncates toward zero for negatives —
// a footgun the content manager hit too (see FloorDivPos there). Cells span the origin, so floor.
return (A >= 0) ? (A / B) : -(((-A) + B - 1) / B);
// True floor division (B > 0). Remainder adjustment handles INT_MIN without negating it.
if (B <= 0) { return 0; }
const int32 Quotient = A / B;
const int32 Remainder = A % B;
return Remainder < 0 ? Quotient - 1 : Quotient;
}
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
@@ -180,7 +182,8 @@ void UVoxelDensityVolume::EnsureAllocated()
for (int32 L = 0; L < N; ++L)
{
FClipLevel& Lv = Levels[L];
Lv.Step = 1 << L;
Lv.Step = static_cast<int32>(uint32{1}
<< static_cast<uint32>(FMath::Clamp(L, 0, 8)));
Lv.OriginCells = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
Lv.bHasData = false;
Lv.Density.SetNumZeroed(Count); // start all-air (0)
@@ -632,7 +635,8 @@ void UVoxelDensityVolume::ProcessOneFill(const FPendingFill& F)
if (!Gen) return;
if (bShuttingDown.load(std::memory_order_relaxed) || bFillThreadStop.load(std::memory_order_relaxed)) return;
const int32 Step = 1 << F.Level;
const int32 Step = static_cast<int32>(uint32{1}
<< static_cast<uint32>(FMath::Clamp(F.Level, 0, 8)));
FFillResult R;
R.Level = F.Level;
R.Epoch = F.Epoch;
@@ -0,0 +1,12 @@
#pragma once
#include "CoreMinimal.h"
// Shared custom package version for idempotent VoxelForge asset migrations.
// Version de package partagee pour les migrations VoxelForge idempotentes.
namespace VoxelForgeAssetVersion
{
inline const FGuid GUID(0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
inline constexpr int32 TunnelFloorSlopeDegrees = 1;
inline constexpr int32 MetreAuthoringV1 = 2;
}
@@ -82,6 +82,113 @@ std::int64_t WorldAssemblyWavelength(double Frequency, double FallbackVoxels)
return std::max<std::int64_t>(1, WorldAssemblyFixed(Wavelength));
}
std::int64_t WorldAssemblyMicrometresToSubvoxels(
std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres);
bool WorldAssemblyMetresToMicrometres(double Metres, std::int64_t& OutMicrometres)
{
if (!std::isfinite(Metres)
|| std::abs(Metres) > static_cast<double>(std::numeric_limits<std::int64_t>::max()) / 1000000.0)
{
return false;
}
OutMicrometres = static_cast<std::int64_t>(std::llround(Metres * 1000000.0));
return true;
}
bool WorldAssemblyMetresToSubvoxels(double Metres, std::uint32_t CoreVoxelMicrometres,
std::int64_t& OutSubvoxels)
{
std::int64_t Micrometres = 0;
if (!WorldAssemblyMetresToMicrometres(Metres, Micrometres) || CoreVoxelMicrometres == 0)
{
return false;
}
OutSubvoxels = WorldAssemblyMicrometresToSubvoxels(Micrometres, CoreVoxelMicrometres);
return true;
}
bool WorldAssemblyVectorToMicrometres(const FVector& Metres, std::array<std::int64_t, 3>& Out)
{
return WorldAssemblyMetresToMicrometres(Metres.X, Out[0])
&& WorldAssemblyMetresToMicrometres(Metres.Y, Out[1])
&& WorldAssemblyMetresToMicrometres(Metres.Z, Out[2]);
}
bool WorldAssemblyVectorToMicrometres(const FVector2D& Metres, std::array<std::int64_t, 2>& Out)
{
return WorldAssemblyMetresToMicrometres(Metres.X, Out[0])
&& WorldAssemblyMetresToMicrometres(Metres.Y, Out[1]);
}
std::string CoreGeometryStableId(int32 StrateIndex, const FString& StableName)
{
FTCHARToUTF8 Utf8Name(*StableName);
return std::string("strate/") + std::to_string(StrateIndex) + "/core/" + Utf8Name.Get();
}
bool HasCoreGeometry(const UVoxelStrateDefinition& Definition)
{
const FVoxelForgeCoreGeometry& Geometry = Definition.CoreGeometry;
return Geometry.GeneratorRecipe != EVoxelForgeCoreGeneratorRecipe::None
|| !Geometry.Stamps.IsEmpty() || !Geometry.Rooms.IsEmpty() || !Geometry.Tunnels.IsEmpty();
}
bool ConfigureCoreGeometryStratum(const FStrateSlot& Slot, ECaveGeneratorType GeneratorType,
bool bImportedOverride, VoxelForgeCore::ResolvedLayoutStratumV2& Item, FString& OutError)
{
const FVoxelForgeCoreGeometry& Geometry = Slot.Definition->CoreGeometry;
switch (Geometry.GeneratorRecipe)
{
case EVoxelForgeCoreGeneratorRecipe::None:
return true;
case EVoxelForgeCoreGeneratorRecipe::NativeCaves:
if (GeneratorType != ECaveGeneratorType::TunnelNetwork
&& GeneratorType != ECaveGeneratorType::Underwater)
{
OutError = FString::Printf(TEXT("Strate %d selects NativeCaves but its generator archetype is not TunnelNetwork/Underwater."),
Slot.StrateIndex);
return false;
}
Item.generatorKind = "Caves";
Item.recipeId = "Core.Caves.PrimitiveField";
Item.recipeVersion = 1;
Item.boundDispatchPath = "Core native cave graph uses resolved room and tunnel records";
Item.queryDispatchPath = "NativeCaveDensityAt evaluates the resolved cavern graph";
return true;
case EVoxelForgeCoreGeneratorRecipe::NativeLattice:
if (GeneratorType != ECaveGeneratorType::Maze
&& GeneratorType != ECaveGeneratorType::VerticalShafts)
{
OutError = FString::Printf(TEXT("Strate %d selects NativeLattice but its generator archetype is not Maze/VerticalShafts."),
Slot.StrateIndex);
return false;
}
Item.recipeId = "Core.Lattice.PrimitiveField";
Item.recipeVersion = 1;
Item.boundDispatchPath = "Core native lattice uses resolved room and explicit tunnel path records";
Item.queryDispatchPath = "NativeLatticeDensityAt evaluates the resolved cavern graph";
return true;
case EVoxelForgeCoreGeneratorRecipe::ProfiledHeightfield:
if (GeneratorType != ECaveGeneratorType::SurfaceWorld)
{
OutError = FString::Printf(TEXT("Strate %d selects ProfiledHeightfield but its generator archetype is not SurfaceWorld."),
Slot.StrateIndex);
return false;
}
if (bImportedOverride)
{
OutError = FString::Printf(TEXT("Strate %d has ProfiledHeightfield authoring, but an imported WorldAssemblyManifest owns that recipe. Clear the imported override to use per-strate CoreGeometry."),
Slot.StrateIndex);
return false;
}
Item.recipeId = "SurfaceWorld.ProfiledHeightfield";
Item.recipeVersion = 1;
return true;
}
return true;
}
std::uint64_t WorldAssemblyFeatureId(std::uint32_t Seed, std::int64_t X,
std::int64_t Y, std::uint32_t Kind)
{
@@ -111,7 +218,7 @@ std::int64_t WorldAssemblyMicrometresToSubvoxels(
std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres)
{
if (CoreVoxelMicrometres == 0) return 0;
return WorldAssemblyRoundRatio(Micrometres * WorldAssemblySubvoxelsPerVoxel,
return WorldAssemblyRoundRatio(Micrometres * VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<std::int64_t>(CoreVoxelMicrometres));
}
@@ -139,7 +246,8 @@ std::uint64_t WorldAssemblyRoundedSqrt(std::uint64_t Value)
}
bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum, std::string& Error)
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum,
std::int64_t ArrivalRadiusQ, std::int64_t DepartureRadiusQ, std::string& Error)
{
using namespace VoxelForgeCore;
constexpr std::int64_t PlayerRadiusQ = 348;
@@ -147,7 +255,10 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
constexpr std::int64_t HalfHeightQ = PlayerHeightQ / 2;
constexpr std::int64_t StepQ = 128;
constexpr std::int64_t ProbeRadiusQ = 174;
constexpr std::int64_t SupportDepthQ = 256;
// The live owner surface recipe can sit slightly below the fixed-point route
// heightfield used during candidate search; allow up to 0.5 m for the final
// resolved-field support probe before rejecting an otherwise clear route.
constexpr std::int64_t SupportDepthQ = 512;
constexpr std::int64_t GradeQ16 = 17560;
constexpr std::int64_t RouteRadiusQ = 4 * WorldAssemblySubvoxelsPerVoxel;
struct FDirection { std::int32_t X; std::int32_t Y; };
@@ -157,9 +268,12 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
struct FCandidate
{
std::int64_t Score = 0;
std::int64_t OutwardProjection = 0;
std::uint64_t EndpointProximityCost = 0;
std::int32_t CenterX = 0;
std::int32_t CenterY = 0;
std::int32_t Direction = 0;
std::int32_t HalfLength = 128;
std::vector<WorldAssemblyRoutePointV1> Points;
};
@@ -171,7 +285,7 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
Constraints.sweepSpacingSubvoxels = StepQ;
Constraints.supportProbeRadiusSubvoxels = ProbeRadiusQ;
Constraints.supportSearchDepthSubvoxels = SupportDepthQ;
Constraints.minimumClearanceSubvoxels = 8;
Constraints.minimumClearanceSubvoxels = 128;
Constraints.minimumSupportProbes = 3;
const Heightfield2D BaseField(Recipe, Stratum);
@@ -182,11 +296,14 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
}
std::vector<FCandidate> Candidates;
constexpr std::int32_t WaypointSpacing = 8;
std::uint64_t LegacyCandidatesVisited = 0;
std::uint64_t LegacyGradeRejected = 0;
for (std::int32_t CenterY = -256; CenterY <= 256; CenterY += 64)
for (std::int32_t CenterX = -256; CenterX <= 256; CenterX += 64)
for (std::int32_t DirectionIndex = 0;
DirectionIndex < static_cast<std::int32_t>(Directions.size()); ++DirectionIndex)
{
++LegacyCandidatesVisited;
FCandidate Candidate;
Candidate.CenterX = CenterX;
Candidate.CenterY = CenterY;
@@ -234,15 +351,33 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
PreviousY = Y;
}
if (bGradePass) Candidates.push_back(std::move(Candidate));
else ++LegacyGradeRejected;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeSurfaceRouteCandidates] stage=legacy_64m visited=%llu rejected_grade=%llu eligible=%llu arrival_radius_q=%lld departure_radius_q=%lld"),
static_cast<unsigned long long>(LegacyCandidatesVisited),
static_cast<unsigned long long>(LegacyGradeRejected),
static_cast<unsigned long long>(Candidates.size()),
static_cast<long long>(ArrivalRadiusQ), static_cast<long long>(DepartureRadiusQ));
const auto TryCandidates = [&](const TCHAR* Stage)
{
std::stable_sort(Candidates.begin(), Candidates.end(), [](const FCandidate& A, const FCandidate& B)
{
if (A.EndpointProximityCost != B.EndpointProximityCost)
return A.EndpointProximityCost < B.EndpointProximityCost;
if (A.OutwardProjection != B.OutwardProjection)
return A.OutwardProjection > B.OutwardProjection;
if (A.Score != B.Score) return A.Score < B.Score;
if (A.CenterY != B.CenterY) return A.CenterY < B.CenterY;
if (A.CenterX != B.CenterX) return A.CenterX < B.CenterX;
return A.Direction < B.Direction;
});
std::uint64_t SweepAttempts = 0;
std::uint64_t BestFailureCount = std::numeric_limits<std::uint64_t>::max();
WorldAssemblyRouteAuditResult BestAudit;
const FCandidate* BestCandidate = nullptr;
std::int64_t BestLiftQ = 0;
for (const FCandidate& Candidate : Candidates)
{
Stratum.route = Candidate.Points;
@@ -260,11 +395,189 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
}
}
const Heightfield2D CandidateField(Recipe, Stratum);
if (AuditHeightfieldRoute(CandidateField).passed) return true;
const WorldAssemblyRouteAuditResult Audit = AuditHeightfieldRoute(CandidateField);
++SweepAttempts;
const std::uint64_t FailureCount = Audit.blockedCapsuleSamples
+ Audit.unsupportedCenterSamples + Audit.gradeFailures;
if (FailureCount < BestFailureCount)
{
BestFailureCount = FailureCount;
BestAudit = Audit;
BestCandidate = &Candidate;
BestLiftQ = LiftQ;
}
if (Audit.passed)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeSurfaceRouteAudit] stage=%s candidates=%llu sweep_attempts=%llu blocked=%llu unsupported=%llu grade_failures=%llu min_clearance_q=%lld max_support_depth_q=%lld center_x=%d center_y=%d direction=%d half_length_vox=%d lift_q=%lld status=PASS"),
Stage, static_cast<unsigned long long>(Candidates.size()),
static_cast<unsigned long long>(SweepAttempts),
static_cast<unsigned long long>(Audit.blockedCapsuleSamples),
static_cast<unsigned long long>(Audit.unsupportedCenterSamples),
static_cast<unsigned long long>(Audit.gradeFailures),
static_cast<long long>(Audit.minimumClearanceSubvoxels),
static_cast<long long>(Audit.maximumSupportDepthSubvoxels),
Candidate.CenterX, Candidate.CenterY, Candidate.Direction,
Candidate.HalfLength, static_cast<long long>(LiftQ));
return true;
}
if (LiftQ >= MaximumRepairLiftQ) break;
LiftQ += RepairStepQ;
}
}
UE_LOG(LogTemp, Warning,
TEXT("[VoxelForgeSurfaceRouteAudit] stage=%s candidates=%llu sweep_attempts=%llu best_blocked=%llu best_unsupported=%llu best_grade_failures=%llu min_clearance_q=%lld max_support_depth_q=%lld center_x=%d center_y=%d direction=%d half_length_vox=%d lift_q=%lld status=FAIL"),
Stage, static_cast<unsigned long long>(Candidates.size()),
static_cast<unsigned long long>(SweepAttempts),
static_cast<unsigned long long>(BestAudit.blockedCapsuleSamples),
static_cast<unsigned long long>(BestAudit.unsupportedCenterSamples),
static_cast<unsigned long long>(BestAudit.gradeFailures),
static_cast<long long>(BestAudit.minimumClearanceSubvoxels),
static_cast<long long>(BestAudit.maximumSupportDepthSubvoxels),
BestCandidate ? BestCandidate->CenterX : 0,
BestCandidate ? BestCandidate->CenterY : 0,
BestCandidate ? BestCandidate->Direction : 0,
BestCandidate ? BestCandidate->HalfLength : 0,
static_cast<long long>(BestLiftQ));
return false;
};
// Keep the established candidate set first so worlds that already pass retain
// their exact route. The bounded-mouth fallback searches different route lengths.
if (TryCandidates(TEXT("legacy_64m"))) return true;
if (ArrivalRadiusQ > 0 || DepartureRadiusQ > 0)
{
// The endpoint radius constrains the mouth end, not the full route length. Keep
// enough reach for a route to leave a steep portal area and find gentler terrain.
constexpr std::int32_t maximumHalfLength = 128;
const std::vector<std::int32_t> HalfLengths{128, 112, 96, 80, 64, 48, 32, 24, 16};
Candidates.clear();
std::uint64_t FallbackCandidatesVisited = 0;
std::uint64_t FallbackGradeRejected = 0;
std::uint64_t FallbackPlacementRejected = 0;
std::uint64_t BestGradeEndpointDistanceQ = std::numeric_limits<std::uint64_t>::max();
FCandidate BestGradeCandidate;
for (const std::int32_t HalfLength : HalfLengths)
for (std::int32_t CenterY = -256; CenterY <= 256; CenterY += 16)
for (std::int32_t CenterX = -256; CenterX <= 256; CenterX += 16)
for (std::int32_t DirectionIndex = 0;
DirectionIndex < static_cast<std::int32_t>(Directions.size()); ++DirectionIndex)
{
++FallbackCandidatesVisited;
FCandidate Candidate;
Candidate.CenterX = CenterX;
Candidate.CenterY = CenterY;
Candidate.Direction = DirectionIndex;
Candidate.HalfLength = HalfLength;
Candidate.Points.reserve(33);
std::int64_t PreviousGround = 0;
std::int64_t PreviousX = 0;
std::int64_t PreviousY = 0;
bool bGradePass = true;
bool bPlacementPass = true;
for (std::int32_t Index = 0; Index <= 32; ++Index)
{
const std::int64_t Along = (Index - 16) * HalfLength / 16;
const std::int64_t X = CenterX + WorldAssemblyRoundRatio(
Along * Directions[DirectionIndex].X, 65536);
const std::int64_t Y = CenterY + WorldAssemblyRoundRatio(
Along * Directions[DirectionIndex].Y, 65536);
const std::int64_t XQ = WorldAssemblyFixed(static_cast<double>(X));
const std::int64_t YQ = WorldAssemblyFixed(static_cast<double>(Y));
const auto InRadius = [](std::int64_t XValue, std::int64_t YValue,
std::int64_t Radius)
{
return Radius <= 0 || XValue * XValue + YValue * YValue <= Radius * Radius;
};
if ((Index == 0 && ArrivalRadiusQ > 0
&& !InRadius(XQ, YQ, ArrivalRadiusQ))
|| (Index == 32 && DepartureRadiusQ > 0
&& !InRadius(XQ, YQ, DepartureRadiusQ)))
{
bPlacementPass = false;
}
if (Index == 0 && ArrivalRadiusQ > 0)
{
Candidate.EndpointProximityCost += static_cast<std::uint64_t>(XQ * XQ + YQ * YQ) * 16ull;
}
if (Index == 32 && DepartureRadiusQ > 0)
{
Candidate.EndpointProximityCost += static_cast<std::uint64_t>(XQ * XQ + YQ * YQ);
// Approach the surface portal from the world-facing side. The
// resolved passage leaves the mouth along its outward tangent;
// a route that approaches from beyond the mouth overlaps that
// bore and turns its centerline into solid ground.
const std::int64_t OutwardProjection = XQ * Directions[DirectionIndex].X
+ YQ * Directions[DirectionIndex].Y;
Candidate.OutwardProjection = OutwardProjection;
}
const auto Column = BaseField.point(WorldAssemblyFixed(static_cast<double>(X)),
WorldAssemblyFixed(static_cast<double>(Y)), 0);
const std::int64_t DeltaX = X - PreviousX;
const std::int64_t DeltaY = Y - PreviousY;
const std::uint64_t HorizontalVoxels = WorldAssemblyRoundedSqrt(
static_cast<std::uint64_t>(DeltaX * DeltaX + DeltaY * DeltaY));
const std::int64_t HorizontalQ = static_cast<std::int64_t>(HorizontalVoxels)
* WorldAssemblySubvoxelsPerVoxel;
if (Index > 0)
{
const std::int64_t RiseQ = std::abs(Column.groundHeightSubvoxels - PreviousGround);
if (HorizontalQ == 0 || RiseQ * 65536 > HorizontalQ * GradeQ16)
{
bGradePass = false;
}
Candidate.Score += RiseQ * 65536 / std::max<std::int64_t>(1, HorizontalQ);
}
WorldAssemblyRoutePointV1 Point;
Point.positionSubvoxels = {XQ, YQ, Column.groundHeightSubvoxels + HalfHeightQ};
Point.radiusSubvoxels = RouteRadiusQ;
Point.stableFeatureId = WorldAssemblyFeatureId(Recipe.seed, X, Y,
0x52540000u + static_cast<std::uint32_t>(Index));
Candidate.Points.push_back(Point);
PreviousGround = Column.groundHeightSubvoxels;
PreviousX = X;
PreviousY = Y;
}
Candidate.Score /= 32;
if (bGradePass)
{
const auto& Endpoint = DepartureRadiusQ > 0
? Candidate.Points.back().positionSubvoxels
: Candidate.Points.front().positionSubvoxels;
const std::uint64_t EndpointDistanceQ = WorldAssemblyRoundedSqrt(
static_cast<std::uint64_t>(Endpoint[0] * Endpoint[0]
+ Endpoint[1] * Endpoint[1]));
if (EndpointDistanceQ < BestGradeEndpointDistanceQ)
{
BestGradeEndpointDistanceQ = EndpointDistanceQ;
BestGradeCandidate.CenterX = CenterX;
BestGradeCandidate.CenterY = CenterY;
BestGradeCandidate.Direction = DirectionIndex;
BestGradeCandidate.HalfLength = HalfLength;
}
}
if (bGradePass && bPlacementPass) Candidates.push_back(std::move(Candidate));
else
{
if (!bGradePass) ++FallbackGradeRejected;
if (!bPlacementPass) ++FallbackPlacementRejected;
}
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeSurfaceRouteCandidates] stage=bounded_single_mouth visited=%llu rejected_grade=%llu rejected_placement=%llu eligible=%llu max_half_length_vox=%d half_length_variants=%d best_grade_endpoint_q=%llu best_grade_center_x=%d best_grade_center_y=%d best_grade_direction=%d best_grade_half_length_vox=%d arrival_radius_q=%lld departure_radius_q=%lld"),
static_cast<unsigned long long>(FallbackCandidatesVisited),
static_cast<unsigned long long>(FallbackGradeRejected),
static_cast<unsigned long long>(FallbackPlacementRejected),
static_cast<unsigned long long>(Candidates.size()), maximumHalfLength,
static_cast<int32>(HalfLengths.size()),
static_cast<unsigned long long>(BestGradeEndpointDistanceQ),
BestGradeCandidate.CenterX, BestGradeCandidate.CenterY,
BestGradeCandidate.Direction, BestGradeCandidate.HalfLength,
static_cast<long long>(ArrivalRadiusQ),
static_cast<long long>(DepartureRadiusQ));
if (TryCandidates(TEXT("bounded_single_mouth"))) return true;
}
Error = "SurfaceWorld route candidates failed the capsule and support sweep.";
return false;
}
@@ -370,6 +683,54 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
BottomVoxel, HeightVoxels));
}
if (Definition != nullptr
&& Definition->CoreGeometry.GeneratorRecipe == EVoxelForgeCoreGeneratorRecipe::ProfiledHeightfield)
{
const FVoxelForgeCoreGeometry& Geometry = Definition->CoreGeometry;
WorldAssemblySurfaceProfileV1 Profile;
Profile.version = 1;
switch (Geometry.HeightfieldProfile)
{
case EVoxelForgeCoreHeightfieldProfile::PeriodicHills:
Profile.kind = WorldAssemblySurfaceProfileKindV1::periodicHills;
break;
case EVoxelForgeCoreHeightfieldProfile::MirroredRelief:
Profile.kind = WorldAssemblySurfaceProfileKindV1::mirroredRelief;
break;
case EVoxelForgeCoreHeightfieldProfile::FlatField:
Profile.kind = WorldAssemblySurfaceProfileKindV1::flatField;
break;
}
std::array<std::int64_t, 2> AmplitudeMicrometres{};
std::array<std::int64_t, 2> WavelengthMicrometres{};
std::int64_t BaseSubvoxels = 0;
std::int64_t ClearanceSubvoxels = 0;
if (!WorldAssemblyVectorToMicrometres(Geometry.HeightfieldAmplitudeMeters, AmplitudeMicrometres)
|| !WorldAssemblyVectorToMicrometres(Geometry.HeightfieldWavelengthMeters, WavelengthMicrometres)
|| !WorldAssemblyMetresToSubvoxels(Geometry.HeightfieldBaseMeters, CoreVoxelMicrometres, BaseSubvoxels)
|| !WorldAssemblyMetresToSubvoxels(Geometry.MinimumArenaClearanceMeters,
CoreVoxelMicrometres, ClearanceSubvoxels)
|| !std::isfinite(Geometry.MirrorLambda))
{
OutError = FString::Printf(TEXT("Strate %d Core profiled-heightfield fields contain a non-finite or out-of-range metre value."),
Slot.StrateIndex);
return false;
}
Profile.h0Subvoxels = BaseSubvoxels;
Profile.amplitudeXSubvoxels = WorldAssemblyMicrometresToSubvoxels(
AmplitudeMicrometres[0], CoreVoxelMicrometres);
Profile.amplitudeYSubvoxels = WorldAssemblyMicrometresToSubvoxels(
AmplitudeMicrometres[1], CoreVoxelMicrometres);
Profile.wavelengthXSubvoxels = WorldAssemblyMicrometresToSubvoxels(
WavelengthMicrometres[0], CoreVoxelMicrometres);
Profile.wavelengthYSubvoxels = WorldAssemblyMicrometresToSubvoxels(
WavelengthMicrometres[1], CoreVoxelMicrometres);
Profile.mirrorLambdaQ16 = WorldAssemblyQ16(Geometry.MirrorLambda);
Profile.minimumArenaClearanceSubvoxels = ClearanceSubvoxels;
ResolveWorldAssemblySurfaceProfilePhasesV1(Profile, Recipe.seed);
Recipe.surfaceProfile = Profile;
}
Stratum.strateIndex = Slot.StrateIndex;
Stratum.topChunkZ = Slot.TopChunkZ;
Stratum.bottomChunkZ = Slot.BottomChunkZ;
@@ -476,7 +837,29 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
}
}
std::string RouteError;
if (!BuildFiniteSurfaceRoute(Recipe, Stratum, RouteError))
const auto OwnerVoxelsToCoreQ = [CoreVoxelMicrometres](double OwnerVoxels)
{
const double OwnerVoxelMicrometres = OwnerVoxelSizeCm * 10000.0;
return static_cast<std::int64_t>(std::llround(OwnerVoxels * OwnerVoxelMicrometres
* WorldAssemblySubvoxelsPerVoxel / std::max<std::uint32_t>(1, CoreVoxelMicrometres)));
};
std::int64_t ArrivalRadiusQ = 0;
std::int64_t DepartureRadiusQ = 0;
for (const FStrateSlot& Neighbor : StrateManager->GetLayout())
{
if (Neighbor.Definition == nullptr) continue;
if (Neighbor.StrateIndex + 1 == Slot.StrateIndex)
{
ArrivalRadiusQ = std::max(OwnerVoxelsToCoreQ(Neighbor.Definition->PassageConfig.DistanceMin),
OwnerVoxelsToCoreQ(Neighbor.Definition->PassageConfig.DistanceMax));
}
if (Neighbor.StrateIndex == Slot.StrateIndex + 1 && Definition != nullptr)
{
DepartureRadiusQ = std::max(OwnerVoxelsToCoreQ(Definition->PassageConfig.DistanceMin),
OwnerVoxelsToCoreQ(Definition->PassageConfig.DistanceMax));
}
}
if (!BuildFiniteSurfaceRoute(Recipe, Stratum, ArrivalRadiusQ, DepartureRadiusQ, RouteError))
{
OutError = UTF8_TO_TCHAR(RouteError.c_str());
return false;
@@ -836,6 +1219,349 @@ void WriteCoreJsonValue(FCoreJsonWriter& Writer, const TCHAR* Identifier,
}
}
bool ResolveAuthoredCoreGeometry(VoxelForgeCore::ResolvedLayoutSnapshotV3& Layout,
const UVoxelStrateManager* StrateManager, std::int32_t Seed, FString& OutError)
{
using namespace VoxelForgeCore;
std::vector<ResolvedLayoutStampRequestV1> LegacyStamps, MotifStamps;
std::vector<ResolvedCavernRoomV1> Rooms;
std::vector<ResolvedCavernEdgeV1> Edges;
TSet<FString> FeatureNames;
TSet<int32> NativeGraphStrates;
const auto StableId = [&FeatureNames, &OutError](const FString& Name, int32 StrateIndex,
const TCHAR* Kind, std::string& OutId)
{
if (Name.IsEmpty() || Name != Name.TrimStartAndEnd())
{
OutError = FString::Printf(TEXT("Core %s on strate %d needs a non-empty stable name without surrounding whitespace."), Kind, StrateIndex);
return false;
}
if (FeatureNames.Contains(Name))
{
OutError = FString::Printf(TEXT("Core stable name '%s' is duplicated across this world."), *Name);
return false;
}
FeatureNames.Add(Name);
OutId = CoreGeometryStableId(StrateIndex, Name);
return true;
};
const auto Microns = [&OutError](double Value, int32 StrateIndex, const TCHAR* Field, std::int64_t& Out)
{
if (WorldAssemblyMetresToMicrometres(Value, Out)) return true;
OutError = FString::Printf(TEXT("Strate %d CoreGeometry.%s must be finite and bounded in metres."), StrateIndex, Field);
return false;
};
const auto VectorMicrons = [&OutError](const FVector& Value, int32 StrateIndex, const TCHAR* Field,
std::array<std::int64_t, 3>& Out)
{
if (WorldAssemblyVectorToMicrometres(Value, Out)) return true;
OutError = FString::Printf(TEXT("Strate %d CoreGeometry.%s must contain finite, bounded metre coordinates."), StrateIndex, Field);
return false;
};
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
const UVoxelStrateDefinition* Definition = Slot.Definition;
if (!Definition || !HasCoreGeometry(*Definition)) continue;
const FVoxelForgeCoreGeometry& Geometry = Definition->CoreGeometry;
if (Geometry.GeneratorRecipe == EVoxelForgeCoreGeneratorRecipe::NativeCaves
|| Geometry.GeneratorRecipe == EVoxelForgeCoreGeneratorRecipe::NativeLattice)
NativeGraphStrates.Add(Slot.StrateIndex);
for (const FVoxelForgeCoreStampDefinition& Authored : Geometry.Stamps)
{
ResolvedLayoutStampRequestV1 Request;
if (!StableId(Authored.StableName, Slot.StrateIndex, TEXT("stamp"), Request.stableId)
|| !VectorMicrons(Authored.AnchorPositionMeters, Slot.StrateIndex, TEXT("Stamps.AnchorPositionMeters"), Request.anchorPositionMicrometres)
|| !VectorMicrons(Authored.HalfExtentsMeters, Slot.StrateIndex, TEXT("Stamps.HalfExtentsMeters"), Request.halfExtentsMicrometres)
|| !Microns(Authored.PlacementRadiusMeters, Slot.StrateIndex, TEXT("Stamps.PlacementRadiusMeters"), Request.placementRadiusMicrometres)
|| !Microns(Authored.SmoothWidthMeters, Slot.StrateIndex, TEXT("Stamps.SmoothWidthMeters"), Request.smoothWidthMicrometres)
|| !Microns(Authored.IrregularityAmplitudeMeters, Slot.StrateIndex, TEXT("Stamps.IrregularityAmplitudeMeters"), Request.irregularityAmplitudeMicrometres)
|| !Microns(Authored.IrregularityWavelengthMeters, Slot.StrateIndex, TEXT("Stamps.IrregularityWavelengthMeters"), Request.irregularityWavelengthMicrometres)
|| !Microns(Authored.RoundingRadiusMeters, Slot.StrateIndex, TEXT("Stamps.RoundingRadiusMeters"), Request.roundingRadiusMicrometres)
|| !Microns(Authored.MinimumSpacingMeters, Slot.StrateIndex, TEXT("Stamps.MinimumSpacingMeters"), Request.minimumSpacingMicrometres)
|| !Microns(Authored.FloorMeters, Slot.StrateIndex, TEXT("Stamps.FloorMeters"), Request.floorMicrometres)) return false;
Request.strateIndex = Slot.StrateIndex;
switch (Authored.Kind)
{
case EVoxelForgeCoreStampKind::CarveEllipsoid: Request.kind = ResolvedSdfStampKindV1::carveEllipsoid; break;
case EVoxelForgeCoreStampKind::CarveArch: Request.kind = ResolvedSdfStampKindV1::carveArch; break;
case EVoxelForgeCoreStampKind::FillPillar: Request.kind = ResolvedSdfStampKindV1::fillPillar; break;
case EVoxelForgeCoreStampKind::FillDais: Request.kind = ResolvedSdfStampKindV1::fillDais; break;
case EVoxelForgeCoreStampKind::FillMonolith: Request.kind = ResolvedSdfStampKindV1::fillMonolith; break;
case EVoxelForgeCoreStampKind::FillPlatform: Request.kind = ResolvedSdfStampKindV1::fillPlatform; break;
}
Request.attachmentPolicy = Authored.bAttachToFloor ? 1u : 0u;
Request.yawQuarterTurns = Authored.YawQuarterTurns;
if (Request.kind == ResolvedSdfStampKindV1::fillMonolith || Request.kind == ResolvedSdfStampKindV1::fillPlatform)
MotifStamps.push_back(std::move(Request));
else LegacyStamps.push_back(std::move(Request));
}
TSet<FString> RoomNames;
for (const FVoxelForgeCoreRoomDefinition& Authored : Geometry.Rooms)
{
ResolvedCavernRoomV1 Room;
if (!StableId(Authored.StableName, Slot.StrateIndex, TEXT("room"), Room.stableId)
|| !VectorMicrons(Authored.CenterMeters, Slot.StrateIndex, TEXT("Rooms.CenterMeters"), Room.centerMicrometres)
|| !VectorMicrons(Authored.HalfExtentsMeters, Slot.StrateIndex, TEXT("Rooms.HalfExtentsMeters"), Room.halfExtentsMicrometres)
|| !Microns(Authored.FloorMeters, Slot.StrateIndex, TEXT("Rooms.FloorMeters"), Room.floorMicrometres)
|| !Microns(Authored.SmoothWidthMeters, Slot.StrateIndex, TEXT("Rooms.SmoothWidthMeters"), Room.smoothWidthMicrometres)
|| !Microns(Authored.UnionWidthMeters, Slot.StrateIndex, TEXT("Rooms.UnionWidthMeters"), Room.unionWidthMicrometres)
|| !Microns(Authored.IrregularityAmplitudeMeters, Slot.StrateIndex, TEXT("Rooms.IrregularityAmplitudeMeters"), Room.irregularityAmplitudeMicrometres)
|| !Microns(Authored.IrregularityWavelengthMeters, Slot.StrateIndex, TEXT("Rooms.IrregularityWavelengthMeters"), Room.irregularityWavelengthMicrometres)) return false;
Room.strateIndex = Slot.StrateIndex;
RoomNames.Add(Authored.StableName);
Rooms.push_back(std::move(Room));
}
for (const FVoxelForgeCoreTunnelDefinition& Authored : Geometry.Tunnels)
{
ResolvedCavernEdgeV1 Edge;
if (!StableId(Authored.StableName, Slot.StrateIndex, TEXT("tunnel"), Edge.stableId)) return false;
if (Authored.StartAnchorStableName.IsEmpty() || Authored.EndRoomStableName.IsEmpty()
|| !RoomNames.Contains(Authored.EndRoomStableName))
{
OutError = FString::Printf(TEXT("Core tunnel '%s' needs a start-anchor stable name and an end-room name that identifies a room on strate %d."), *Authored.StableName, Slot.StrateIndex);
return false;
}
FTCHARToUTF8 StartAnchorUtf8(*Authored.StartAnchorStableName);
Edge.startAnchorStableId = StartAnchorUtf8.Get();
Edge.endRoomStableId = CoreGeometryStableId(Slot.StrateIndex, Authored.EndRoomStableName);
Edge.strateIndex = Slot.StrateIndex;
const FVector ControlPoints[4] = { Authored.ControlPoint0Meters, Authored.ControlPoint1Meters,
Authored.ControlPoint2Meters, Authored.ControlPoint3Meters };
for (std::size_t Point = 0; Point < 4; ++Point)
if (!WorldAssemblyVectorToMicrometres(ControlPoints[Point], Edge.controlPointsMicrometres[Point]))
{
OutError = FString::Printf(TEXT("Core tunnel '%s' has a non-finite or out-of-range control point in metres."), *Authored.StableName);
return false;
}
if (!Microns(Authored.BoreRadiusMeters, Slot.StrateIndex, TEXT("Tunnels.BoreRadiusMeters"), Edge.boreRadiusMicrometres)
|| !Microns(Authored.BoreHeightMeters, Slot.StrateIndex, TEXT("Tunnels.BoreHeightMeters"), Edge.boreHeightMicrometres)
|| !Microns(Authored.FloorSupportDepthMeters, Slot.StrateIndex, TEXT("Tunnels.FloorSupportDepthMeters"), Edge.floorSupportDepthMicrometres)
|| !Microns(Authored.SmoothWidthMeters, Slot.StrateIndex, TEXT("Tunnels.SmoothWidthMeters"), Edge.smoothWidthMicrometres)
|| !Microns(Authored.UnionWidthMeters, Slot.StrateIndex, TEXT("Tunnels.UnionWidthMeters"), Edge.unionWidthMicrometres)
|| !Microns(Authored.IrregularityAmplitudeMeters, Slot.StrateIndex, TEXT("Tunnels.IrregularityAmplitudeMeters"), Edge.irregularityAmplitudeMicrometres)
|| !Microns(Authored.IrregularityWavelengthMeters, Slot.StrateIndex, TEXT("Tunnels.IrregularityWavelengthMeters"), Edge.irregularityWavelengthMicrometres)) return false;
for (const FVector& Point : Authored.PolylineMeters)
{
std::array<std::int64_t, 3> Converted{};
if (!WorldAssemblyVectorToMicrometres(Point, Converted))
{
OutError = FString::Printf(TEXT("Core tunnel '%s' has an invalid metre polyline point."), *Authored.StableName);
return false;
}
Edge.polylineMicrometres.push_back(Converted);
}
Edges.push_back(std::move(Edge));
}
if ((!Geometry.Rooms.IsEmpty() || !Geometry.Tunnels.IsEmpty())
&& Geometry.GeneratorRecipe != EVoxelForgeCoreGeneratorRecipe::NativeCaves
&& Geometry.GeneratorRecipe != EVoxelForgeCoreGeneratorRecipe::NativeLattice)
{
OutError = FString::Printf(TEXT("Strate %d authors a cavern graph without selecting NativeCaves or NativeLattice."), Slot.StrateIndex);
return false;
}
}
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
if (!Slot.Definition) continue;
for (const FStrateLandmark& Landmark : Slot.Definition->Landmarks)
if (!Landmark.CoreGeometryStableName.IsEmpty() && !FeatureNames.Contains(Landmark.CoreGeometryStableName))
{
OutError = FString::Printf(TEXT("Landmark on strate %d references unknown Core stable name '%s'."), Slot.StrateIndex, *Landmark.CoreGeometryStableName);
return false;
}
}
for (int32 StrateIndex : NativeGraphStrates)
{
const bool bHasRoom = std::any_of(Rooms.begin(), Rooms.end(), [StrateIndex](const auto& Room) { return Room.strateIndex == StrateIndex; });
const bool bHasEdge = std::any_of(Edges.begin(), Edges.end(), [StrateIndex](const auto& Edge) { return Edge.strateIndex == StrateIndex; });
if (!bHasRoom || !bHasEdge)
{
OutError = FString::Printf(TEXT("Native cave/lattice recipe on strate %d needs at least one authored room and one tunnel."), StrateIndex);
return false;
}
}
if (!LegacyStamps.empty() || !MotifStamps.empty())
{
std::string CoreError;
const ResolvedLayoutSnapshotV3 BaseLayout = Layout;
if (!LegacyStamps.empty() && !ResolveResolvedLayoutStampsV1(Layout, Seed, LegacyStamps, CoreError))
{
OutError = FString::Printf(TEXT("Core authored stamp resolution failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
if (!MotifStamps.empty())
{
ResolvedLayoutSnapshotV3 MotifLayout = BaseLayout;
if (!ResolveResolvedLayoutStampsV3(MotifLayout, Seed, MotifStamps, CoreError))
{
OutError = FString::Printf(TEXT("Core authored motif resolution failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
if (LegacyStamps.empty()) Layout = std::move(MotifLayout);
else
{
Layout.stamps.insert(Layout.stamps.end(), MotifLayout.stamps.begin(), MotifLayout.stamps.end());
Layout.stampVocabularyVersion = ResolvedLayoutStampVocabularyVersionV3;
std::vector<ResolvedLayoutIssueV2> Issues;
if (!Layout.finalize(CoreError) || !Layout.validate(ResolvedLayoutValidationModeV3::strict, Issues, CoreError))
{
OutError = FString::Printf(TEXT("Combined Core stamp layout failed validation: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
}
}
}
if (!Rooms.empty() || !Edges.empty())
{
std::string CoreError;
if (!ResolveResolvedCavernGraphV1(Layout, Seed, Rooms, Edges, CoreError))
{
OutError = FString::Printf(TEXT("Core authored cavern graph resolution failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
}
return true;
}
bool AttachCoreGeometryOnlyLayout(const UVoxelStrateManager* StrateManager,
double OwnerVoxelCm, double CoreVoxelCm, std::uint32_t LayoutChunkMicrometres,
std::int64_t LayoutChunkSubvoxels, const TSharedRef<FJsonObject>& Root, FString& OutError)
{
using namespace VoxelForgeCore;
ResolvedLayoutSnapshotV3 Layout;
Layout.worldIdentity = std::string("owner-world/seed/") + std::to_string(StrateManager->GetWorldSeed());
Layout.ownerVoxelSizeMicrometres = static_cast<std::uint32_t>(std::llround(OwnerVoxelCm * 10000.0));
Layout.coreVoxelSizeMicrometres = static_cast<std::uint32_t>(std::llround(CoreVoxelCm * 10000.0));
Layout.strateLayoutChunkSizeMicrometres = LayoutChunkMicrometres;
Layout.strateLayoutChunkWidthSubvoxels = LayoutChunkSubvoxels;
const std::uint32_t CoreVoxelMicrometres = Layout.coreVoxelSizeMicrometres;
const auto GeneratorName = [](ECaveGeneratorType Type) -> const char*
{
switch (Type)
{
case ECaveGeneratorType::TunnelNetwork: return "TunnelNetwork";
case ECaveGeneratorType::FlatPlain: return "FlatPlain";
case ECaveGeneratorType::CrystalChamber: return "CrystalChamber";
case ECaveGeneratorType::Maze: return "Maze";
case ECaveGeneratorType::SurfaceWorld: return "SurfaceWorld";
case ECaveGeneratorType::VerticalShafts: return "VerticalShafts";
case ECaveGeneratorType::FloatingIslands: return "FloatingIslands";
case ECaveGeneratorType::Underwater: return "Underwater";
}
return "Unknown";
};
const auto TransitionName = [](EVoxelStrateTransition Type) -> const char*
{
switch (Type)
{
case EVoxelStrateTransition::Gradient: return "Gradient";
case EVoxelStrateTransition::Hard: return "Hard";
case EVoxelStrateTransition::Interleaved: return "Interleaved";
}
return "Unknown";
};
std::vector<ResolvedLayoutPassageConfigV1> PassageConfigs;
const std::int64_t OwnerVoxelMicrometres = Layout.ownerVoxelSizeMicrometres;
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
if (!Slot.Definition) continue;
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, Slot.TopChunkZ));
ResolvedLayoutStratumV2 Item;
Item.strateIndex = Slot.StrateIndex;
Item.generatorKind = GeneratorName(Type);
Item.stableId = Layout.worldIdentity + "/strate/" + std::to_string(Slot.StrateIndex)
+ "/generator/" + GeneratorName(Type);
Item.recipeId = Type == ECaveGeneratorType::SurfaceWorld ? "SurfaceWorld.Heightfield2D"
: Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField"
: std::string("legacy.") + GeneratorName(Type);
Item.recipeVersion = 0;
if (!ConfigureCoreGeometryStratum(Slot, Type, false, Item, OutError)) return false;
Item.voxelSizeMicrometres = CoreVoxelMicrometres;
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
Item.managerChunkWidthMicrometres = LayoutChunkMicrometres;
Item.dispatchChunkWidthMicrometres = LayoutChunkMicrometres;
Item.transitionMode = TransitionName(Slot.ResolvedTransitionType);
Item.transitionBlendChunks = Slot.ResolvedTransitionBlendChunks;
Item.transitionWidthMicrometres = Slot.TransitionWidthMicrometres;
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
? std::min(Item.transitionWidthMicrometres / 2, static_cast<std::int64_t>(LayoutChunkMicrometres)) : 0;
Item.transitionInterpenetrationMicrometres = Item.transitionMode == "Interleaved"
? Item.transitionWidthMicrometres : Item.transitionMode == "Gradient" ? Item.transitionWidthMicrometres / 4 : 0;
Item.transitionFallsBackToHard = Item.transitionMode != "Hard" && Item.transitionWidthMicrometres <= 0;
Item.transitionHonouredInCore = Item.transitionMode == "Hard" || !Item.transitionFallsBackToHard;
Item.managerLookup = "UVoxelStrateManager::GetGeneratorTypeForChunk; upper strate owns transitions";
Item.boundDispatchPath = "CoreGeometry-only resolved strate layout";
Item.queryDispatchPath = "CoreGeometry-only native layout field";
const std::int64_t SealMicrometres = static_cast<std::int64_t>(std::llround(
std::max(0.0f, StrateBoundarySealThickness(*Slot.Definition)) * OwnerVoxelMicrometres));
if (SealMicrometres <= 0)
{
OutError = FString::Printf(TEXT("Strate %d has no positive BoundarySealThickness for the shared ceiling and bedrock bands."), Slot.StrateIndex);
return false;
}
const std::int64_t SolidDensityQ16 = std::max<std::int64_t>(1,
WorldAssemblyQ16(std::max(0.05f, StrateBoundarySolidDensity(*Slot.Definition))));
Item.ceiling = { "ceiling", Item.interiorTopMicrometres - SealMicrometres,
Item.interiorTopMicrometres, true, true, false, "solid ceiling from strate BoundarySealThickness",
"interior-facing lower surface", "shared resolved-layout density band", SolidDensityQ16 };
Item.bedrock = { "bedrock", Item.interiorBottomMicrometres,
Item.interiorBottomMicrometres + SealMicrometres, true, true, false,
"solid bedrock floor from strate BoundarySealThickness", "interior-facing upper surface",
"shared resolved-layout density band", SolidDensityQ16 };
Item.content = { "content", Item.interiorBottomMicrometres + SealMicrometres,
Item.interiorTopMicrometres - SealMicrometres, true, true, false,
"resolved generator interior", "none", "none" };
Layout.strates.push_back(std::move(Item));
const FStratePassageConfig& Passage = Slot.ResolvedPassageConfig;
ResolvedLayoutPassageConfigV1 Config;
Config.upperStrateIndex = Slot.StrateIndex;
Config.connections = Passage.Connections;
Config.style = static_cast<std::uint32_t>(Passage.Style);
Config.mouthRadiusSubvoxels = std::max(WorldAssemblyMicrometresToSubvoxels(
Slot.PassageMouthRadiusMicrometres, CoreVoxelMicrometres),
WorldAssemblyMicrometresToSubvoxels(36 * OwnerVoxelMicrometres, CoreVoxelMicrometres));
Config.midRadiusSubvoxels = std::max(WorldAssemblyMicrometresToSubvoxels(
Slot.PassageMidRadiusMicrometres, CoreVoxelMicrometres),
WorldAssemblyMicrometresToSubvoxels(24 * OwnerVoxelMicrometres, CoreVoxelMicrometres));
Config.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageReachMinimumMicrometres, CoreVoxelMicrometres);
Config.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageReachMaximumMicrometres, CoreVoxelMicrometres);
Config.distanceMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageDistanceMinimumMicrometres, CoreVoxelMicrometres);
Config.distanceMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageDistanceMaximumMicrometres, CoreVoxelMicrometres);
Config.wanderSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageWanderMicrometres, CoreVoxelMicrometres);
Config.segments = static_cast<std::uint32_t>(FMath::Clamp(Passage.Segments, 1, 48));
Config.verticalWobbleSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageVerticalWobbleMicrometres, CoreVoxelMicrometres);
Config.spiralRadiusSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageSpiralRadiusMicrometres, CoreVoxelMicrometres);
Config.spiralTurnsQ16 = static_cast<std::int64_t>(std::llround(static_cast<double>(Passage.SpiralTurns) * 65536.0));
Config.cascadeSteps = static_cast<std::uint32_t>(FMath::Clamp(Passage.CascadeSteps, 1, 16));
Config.cascadeLedgeSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageCascadeLedgeMicrometres, CoreVoxelMicrometres);
PassageConfigs.push_back(Config);
}
std::string CoreError;
if (!PlanResolvedLayoutPassagesV1(Layout, StrateManager->GetWorldSeed(), PassageConfigs, CoreError))
{
OutError = FString::Printf(TEXT("Resolved inter-strate passage planning failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
if (!ResolveAuthoredCoreGeometry(Layout, StrateManager, StrateManager->GetWorldSeed(), OutError)) return false;
if (!Layout.finalize(CoreError))
{
OutError = FString::Printf(TEXT("Core resolved-layout export failed: %s"), UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
const std::string Json = Layout.ToJson();
Root->SetStringField(TEXT("resolved_layout_json"), UTF8_TO_TCHAR(Json.c_str()));
return true;
}
bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
const UVoxelStrateManager* StrateManager,
double CoreVoxelSizeCm,
@@ -876,6 +1602,7 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
Generated.recipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels;
int32 SurfaceSlotCount = 0;
int32 IslandSlotCount = 0;
int32 CoreGeometrySlotCount = 0;
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
if (Slot.Definition == nullptr) continue;
@@ -883,10 +1610,13 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
FIntVector(0, 0, Slot.TopChunkZ));
SurfaceSlotCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0;
IslandSlotCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0;
CoreGeometrySlotCount += HasCoreGeometry(*Slot.Definition) ? 1 : 0;
}
if (SurfaceSlotCount == 0 && IslandSlotCount == 0)
{
return true;
return CoreGeometrySlotCount == 0 ? true
: AttachCoreGeometryOnlyLayout(StrateManager, OwnerVoxelSizeCm, CoreVoxelSizeCm,
LayoutChunkSizeMicrometres, LayoutChunkWidthSubvoxels, Root, OutError);
}
if (SurfaceSlotCount == 0)
{
@@ -918,7 +1648,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
Generated.plan.strates.push_back(std::move(Stratum));
++BuiltSurfaceCount;
}
else if (Type == ECaveGeneratorType::FloatingIslands)
// Native floating-island evaluation remains an explicit imported-plan opt-in.
// The default owner path uses the legacy generator for this slot; emitting an
// automatic FI plan here silently switches density dispatch and changes the
// shared passage mouth before the owner has opted into that field.
else if (Type == ECaveGeneratorType::FloatingIslands && bImportedOverride)
{
if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated,
CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError))
@@ -991,10 +1725,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
}
}
if (Manifest->plan.strates.size() != static_cast<std::size_t>(SurfaceSlotCount)
|| Manifest->floatingIslands.size() != static_cast<std::size_t>(FloatingIslandSlotCount))
|| (!Manifest->floatingIslands.empty()
&& Manifest->floatingIslands.size() != static_cast<std::size_t>(FloatingIslandSlotCount)))
{
OutError = FString::Printf(
TEXT("Core world assembly has %d SurfaceWorld and %d floating-island plans for %d resolved SurfaceWorld and %d FloatingIslands strates."),
TEXT("Core world assembly has %d SurfaceWorld and %d floating-island plans for %d resolved SurfaceWorld strates; an explicit FI plan must cover all %d FloatingIslands strates."),
static_cast<int32>(Manifest->plan.strates.size()),
static_cast<int32>(Manifest->floatingIslands.size()),
SurfaceSlotCount, FloatingIslandSlotCount);
@@ -1088,12 +1823,12 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
WorldAssemblyMicrometresToSubvoxels(
Slot.PassageMouthRadiusMicrometres, ExpectedCoreVoxelMicrons),
WorldAssemblyMicrometresToSubvoxels(
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
36 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
PassageConfig.midRadiusSubvoxels = std::max(
WorldAssemblyMicrometresToSubvoxels(
Slot.PassageMidRadiusMicrometres, ExpectedCoreVoxelMicrons),
WorldAssemblyMicrometresToSubvoxels(
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
24 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
PassageConfig.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.PassageReachMinimumMicrometres, ExpectedCoreVoxelMicrons);
PassageConfig.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
@@ -1128,6 +1863,10 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
: Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField"
: std::string("legacy.") + generatorName(Type);
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
if (!ConfigureCoreGeometryStratum(Slot, Type, bImportedOverride, Item, OutError))
{
return false;
}
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
@@ -1264,6 +2003,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
UTF8_TO_TCHAR(PassagePlanError.c_str()));
return false;
}
if (!ResolveAuthoredCoreGeometry(Layout, StrateManager,
StrateManager->GetWorldSeed(), OutError))
{
return false;
}
std::string LayoutError;
if (!Layout.finalize(LayoutError))
{
@@ -1372,6 +2116,10 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0)));
Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm);
Root->SetNumberField(TEXT("core_voxel_size_microns"), CoreVoxelSizeMicrons);
// Core import des longueurs owner avec le rapport physique resolu, pas un ratio suppose.
// Core imports owner lengths with the resolved physical ratio, never an assumed scale.
Root->SetNumberField(TEXT("owner_to_core_length_scale"),
OwnerVoxelSizeCm / ResolvedCoreVoxelSizeCm);
Root->SetNumberField(TEXT("seed"), Seed);
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
@@ -2590,8 +2590,8 @@ static float EvaluateInternal(const UVoxelGenerator& Generator,
{
EnsurePlan(Generator);
}
const bool bChunkResolved = Context != nullptr && Context->bChunkResolved
|| ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved;
const bool bChunkResolved = (Context != nullptr && Context->bChunkResolved)
|| (ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved);
const FIntVector Chunk = Context != nullptr && Context->bChunkResolved
? Context->Chunk
: ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved
@@ -1958,10 +1958,11 @@ namespace VoxelSeasonCompositionPrivate
}
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
FVoxelSeasonFixedStrate& OutFixed)
double LayoutChunkMeters, FVoxelSeasonFixedStrate& OutFixed)
{
OutFixed = FVoxelSeasonFixedStrate();
OutFixed.HeightInChunks = Definition.StrateHeightInChunks;
OutFixed.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
Definition.StrateHeightMeters / FMath::Max(0.001, LayoutChunkMeters)));
OutFixed.Archetype = Definition.GeneratorType;
OutFixed.SourceDefinitionPath = Definition.GetPathName();
OutFixed.bUsesRecipe = false;
@@ -1999,7 +2000,9 @@ namespace VoxelSeasonCompositionPrivate
return false;
}
FVoxelSeasonFixedStrate Fixed;
CopyDefinitionParams(*Definition, Fixed);
const double LayoutChunkMeters = Settings.WorldSettings != nullptr
? Settings.WorldSettings->StrateLayoutChunkSizeMeters : 8.0;
CopyDefinitionParams(*Definition, LayoutChunkMeters, Fixed);
Fixed.Seed = SeasonSeed;
if (Fixed.SourceDefinitionPath.IsEmpty())
{
@@ -2974,7 +2977,9 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* Wo
if (WorldSettings != nullptr)
{
Settings.SelectedStrateCount = WorldSettings->TotalStrates;
Settings.InterStrateGapChunks = WorldSettings->InterStrateGapChunks;
Settings.InterStrateGapChunks = FMath::Max(0, FMath::RoundToInt(
WorldSettings->InterStrateGapMeters * 1000000.0
/ FMath::Max(1.0, static_cast<double>(WorldSettings->StrateLayoutChunkSizeMeters) * 1000000.0)));
Settings.OriginSpineRadius = WorldSettings->OriginSpineRadius;
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
Settings.SeasonNumber = WorldSettings->CurrentSeason;
@@ -1,5 +1,7 @@
#include "VoxelSettings.h"
#include "VoxelForgeAssetVersion.h"
#include "VoxelStrateDefinition.h"
#include "VoxelSeasonAsset.h"
#if WITH_EDITOR
@@ -9,6 +11,74 @@
#include <VoxelForgeCore/VoxelForgeCore.h>
#include <string>
void UVoxelSettings::Serialize(FArchive& Ar)
{
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
Super::Serialize(Ar);
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
< VoxelForgeAssetVersion::MetreAuthoringV1)
{
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
* LayoutChunkMeters;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeAssetUpgrade] type=Settings asset=%s gap_chunks=%d gap_m=%.6f layout_chunk_m=%.6f"),
*GetPathName(), InterStrateGapChunks, InterStrateGapMeters, LayoutChunkMeters);
}
}
void UVoxelSettings::PostLoad()
{
Super::PostLoad();
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
>= VoxelForgeAssetVersion::MetreAuthoringV1)
{
return;
}
// Resolve the legacy count against this settings asset's saved physical layout width.
// Convertit les anciens comptes avec la largeur physique sauvegardee dans ces reglages.
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
* LayoutChunkMeters;
TSet<UVoxelStrateDefinition*> Migrated;
const auto MigrateStrate = [this, LayoutChunkMeters, &Migrated](
const TSoftObjectPtr<UVoxelStrateDefinition>& SoftDefinition)
{
UVoxelStrateDefinition* Definition = SoftDefinition.LoadSynchronous();
if (Definition == nullptr || Migrated.Contains(Definition)
|| Definition->GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
>= VoxelForgeAssetVersion::MetreAuthoringV1)
{
return;
}
Migrated.Add(Definition);
const int32 OldHeightChunks = Definition->StrateHeightInChunks;
const int32 OldTransitionChunks = Definition->TransitionBlendChunks;
Definition->StrateHeightMeters = static_cast<double>(FMath::Max(1, OldHeightChunks))
* LayoutChunkMeters;
Definition->TransitionWidthMeters = static_cast<double>(FMath::Max(0, OldTransitionChunks))
* LayoutChunkMeters;
Definition->MarkPackageDirty();
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f effective_layout_chunk_m=%.6f"),
*Definition->GetPathName(), OldHeightChunks, Definition->StrateHeightMeters,
OldTransitionChunks, Definition->TransitionWidthMeters, LayoutChunkMeters);
};
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Definition : StratePool)
{
MigrateStrate(Definition);
}
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Definition : FixedStrates)
{
MigrateStrate(Definition.Value);
}
MarkPackageDirty();
}
int32 UVoxelSettings::GetEffectiveWorldSeed() const
{
if (!Season.IsNull())
@@ -297,7 +297,11 @@ void RecordTile(const FTileSample& Sample)
"\"submitted_t_s\":%s,\"worker_start_t_s\":%s,\"worker_end_t_s\":%s,\"worker_thread_id\":%u,"
"\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu,"
"\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,\"worker_task_s\":%s,"
"\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
"\"generation_s\":%s,"
#if PLATFORM_LINUX
"\"core_generate_s\":%s,\"mesh_copy_s\":%s,\"rmc_stream_build_s\":%s,"
#endif
"\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
Sample.TileX, Sample.TileY, Sample.TileZ,
Sample.Level, Sample.WorkPriority,
static_cast<unsigned long long>(Sample.RequestOrdinal),
@@ -312,8 +316,12 @@ void RecordTile(const FTileSample& Sample)
static_cast<unsigned long long>(Sample.GeometryBytes),
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
*Number(Sample.WorkerTaskSeconds),
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds),
*Number(Sample.WorkerTaskSeconds), *Number(Sample.GenerationSeconds),
#if PLATFORM_LINUX
*Number(Sample.CoreGenerateSeconds), *Number(Sample.MeshCopySeconds),
*Number(Sample.StreamBuildSeconds),
#endif
*Number(Sample.ClassifySeconds),
*Number(Sample.MeshSeconds), *Number(Sample.StreamSeconds), *Number(Sample.ApplySeconds)));
}
@@ -1,20 +1,17 @@
// Serialized strate-definition migrations.
#include "VoxelStrateDefinition.h"
#include "VoxelForgeAssetVersion.h"
#include "Serialization/CustomVersion.h"
#include "VoxelPassageGeometry.h"
namespace
{
// The field name is retained for package compatibility, but version 1 changes its meaning
// from a raw tangent/gradient to authored degrees. This GUID is local to the plugin.
const FGuid GVoxelForgeStrateSerializationVersion(
0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = 1;
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = VoxelForgeAssetVersion::TunnelFloorSlopeDegrees;
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
GVoxelForgeStrateSerializationVersion,
GVoxelForgeStrateGentleSlopeDegreesVersion,
VoxelForgeAssetVersion::GUID,
VoxelForgeAssetVersion::MetreAuthoringV1,
TEXT("VoxelForgeStrateSerialization"));
float LegacyGradientToDegrees(float LegacyGradient)
@@ -58,6 +55,31 @@ namespace
}
}
void UVoxelStrateDefinition::Serialize(FArchive& Ar)
{
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
Super::Serialize(Ar);
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
< VoxelForgeAssetVersion::MetreAuthoringV1)
{
// A stand-alone strate has no owning settings object during Serialize. Convert using the
// current 8 m layout default here; UVoxelSettings corrects referenced old strates using
// their saved effective width during its own load upgrade.
// Une strate seule n'a pas ses reglages proprietaires ici; UVoxelSettings corrige ensuite
// les references avec leur largeur physique sauvegardee.
constexpr double LegacyDefaultLayoutChunkMeters = 8.0;
StrateHeightMeters = static_cast<double>(FMath::Max(1, StrateHeightInChunks))
* LegacyDefaultLayoutChunkMeters;
TransitionWidthMeters = static_cast<double>(FMath::Max(0, TransitionBlendChunks))
* LegacyDefaultLayoutChunkMeters;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f fallback_layout_chunk_m=%.6f"),
*GetPathName(), StrateHeightInChunks, StrateHeightMeters,
TransitionBlendChunks, TransitionWidthMeters, LegacyDefaultLayoutChunkMeters);
}
}
void UVoxelStrateDefinition::PostLoad()
{
Super::PostLoad();
@@ -67,7 +89,7 @@ void UVoxelStrateDefinition::PostLoad()
// is normalized to the ignored compatibility default before any runtime copy is made.
GenerationParams.bTunnelFloorEnabled = true;
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion)
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
{
return;
@@ -1255,7 +1255,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
? SeasonManifest.Strates.Num() : Settings->TotalStrates;
const int32 LayoutGapChunks = bUseSeason
? SeasonManifest.InterStrateGapChunks
: FMath::Max(0, Settings->InterStrateGapChunks);
: FMath::Max(0, FMath::RoundToInt(static_cast<double>(Settings->InterStrateGapMeters)
* 1000000.0 / std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
//=========================================================================
// STEP 1: Build shuffled pool (seed-based randomization)
@@ -1369,7 +1370,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
}
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
Slot.Definition->StrateHeightMeters = static_cast<double>(SeasonStrate.HeightInChunks)
* StrateLayoutChunkSizeMicrometres / 1000000.0;
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams;
@@ -1426,7 +1428,9 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
}
// Resolve the authored chunk height into an exact physical interval.
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
Slot.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
Slot.Definition->StrateHeightMeters * 1000000.0
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
Slot.TopWorldMicrometres = CurrentTopWorldMicrometres;
Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks)
* StrateLayoutChunkSizeMicrometres;
@@ -1439,9 +1443,11 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
}
Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
Slot.ResolvedTransitionBlendChunks = Slot.Definition->TransitionBlendChunks;
Slot.TransitionWidthMicrometres = static_cast<int64>(
Slot.ResolvedTransitionBlendChunks) * StrateLayoutChunkSizeMicrometres;
Slot.ResolvedTransitionBlendChunks = FMath::Max(0, FMath::RoundToInt(
Slot.Definition->TransitionWidthMeters * 1000000.0
/ std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
Slot.TransitionWidthMicrometres = static_cast<int64>(Slot.ResolvedTransitionBlendChunks)
* StrateLayoutChunkSizeMicrometres;
Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig;
Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.MouthRadius);
+196 -72
View File
@@ -82,6 +82,17 @@
#include <atomic>
#include <unordered_set>
static FORCEINLINE int32 VF_ClipLevelScale(int32 Level)
{
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
return static_cast<int32>(uint32{1} << SafeLevel);
}
static FORCEINLINE int32 VF_ClipLevelExtentVoxels(int32 Level)
{
return static_cast<int32>(static_cast<uint32>(CHUNK_SIZE)
* static_cast<uint32>(VF_ClipLevelScale(Level)));
}
static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile,
const FVector& BoundsMin, const FVector& BoundsMax);
static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B);
@@ -195,7 +206,7 @@ namespace
FAutoConsoleVariableRef CVarVoxelForgeStreamingCoarseFirst(
TEXT("voxel.StreamingCoarseFirst"),
GVoxelForgeStreamingCoarseFirst,
TEXT("Prioritize current walkable tiles, then far coarse clipmap levels, then near refinement; 0 restores legacy LOD order."));
TEXT("Prioritize walkable tiles, then finish LOD0-2 before far clipmap work; controls far-level order."));
int32 GVoxelForgeStreamingDynamicNearReserve = 1;
FAutoConsoleVariableRef CVarVoxelForgeStreamingDynamicNearReserve(
@@ -227,11 +238,11 @@ namespace
GVoxelForgeStreamingResultBacklogCap,
TEXT("Maximum queued results plus admitted tile tasks; 0 disables the safety cap. This is separate from the worker-slot cap."));
int32 GVoxelForgeStreamingReadyQueueDepth = 8;
int32 GVoxelForgeStreamingReadyQueueDepth = 16;
FAutoConsoleVariableRef CVarVoxelForgeStreamingReadyQueueDepth(
TEXT("voxel.StreamingReadyQueueDepth"),
GVoxelForgeStreamingReadyQueueDepth,
TEXT("Extra admitted tile tasks kept ready beyond the worker slots, up to one worker-set; 0 disables the refill queue."));
TEXT("Extra admitted tile tasks kept ready beyond worker slots, up to four worker-sets/64 tasks; 0 disables the refill queue."));
int32 GVoxelForgeStreamingBackgroundWorkerCap = 1;
FAutoConsoleVariableRef CVarVoxelForgeStreamingBackgroundWorkerCap(
@@ -301,8 +312,8 @@ namespace
FAutoConsoleVariableRef CVarVoxelForgeStreamingStartupApplyBoost(
TEXT("voxel.StreamingStartupApplyBoost"),
GVoxelForgeStreamingStartupApplyBoost,
TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 12-mesh budget until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
constexpr int32 StreamingStartupMaxMeshApplies = 12;
TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 24-mesh cap until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
constexpr int32 StreamingStartupMaxMeshApplies = 24;
constexpr float StreamingStartupApplyBudgetMilliseconds = 14.0f;
constexpr double StreamingStartupApplyMaxSeconds = 30.0;
@@ -2196,7 +2207,7 @@ void AVoxelWorld::BeginPlay()
CommandLineStreamingReadyQueueDepth))
{
GVoxelForgeStreamingReadyQueueDepth = FMath::Clamp(
CommandLineStreamingReadyQueueDepth, 0, 32);
CommandLineStreamingReadyQueueDepth, 0, 64);
}
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingBackgroundWorkerCap="),
CommandLineStreamingBackgroundWorkerCap))
@@ -2623,7 +2634,8 @@ void AVoxelWorld::BeginPlay()
FTaskGraphInterface& TaskGraph = FTaskGraphInterface::Get();
LowLevelTasks::FScheduler& TaskScheduler = LowLevelTasks::FScheduler::Get();
const int32 QueueDepth = FMath::Clamp(
GVoxelForgeStreamingReadyQueueDepth, 0, GetMaxConcurrentStreamingTasks());
GVoxelForgeStreamingReadyQueueDepth, 0,
FMath::Min(64, FMath::Max(1, GetMaxConcurrentStreamingTasks()) * 4));
VoxelForgeStartupTrace::RecordEvent(TEXT("worker_backend_config"), FString::Printf(
TEXT("\"logical_cores\":%d,\"effective_tile_slots\":%d,"
"\"task_api\":\"UE::Tasks::Launch\",\"task_scheduler_workers\":%u,"
@@ -6240,17 +6252,19 @@ static void VF_AppendTouchingAncestors(
const FVoxelTileKey& Tile, int32 AncestorLevel, CandidateArray& OutCandidates)
{
if (AncestorLevel <= Tile.Level) { return; }
const int32 Divisor = 1 << (AncestorLevel - Tile.Level);
const auto FloorDiv = [Divisor](int32 Value)
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
const auto FloorDiv = [Divisor](int64 Value)
{
return Value >= 0 ? Value / Divisor : -(((-Value) + Divisor - 1) / Divisor);
const int64 Quotient = Value / Divisor;
const int64 Remainder = Value % Divisor;
return static_cast<int32>(Remainder < 0 ? Quotient - 1 : Quotient);
};
const auto AxisCandidates = [&FloorDiv, Divisor](int32 Value)
{
TArray<int32, TInlineAllocator<2>> Candidates;
int32 First = FloorDiv(Value);
if ((Value % Divisor) == 0) { --First; }
const int32 Last = FloorDiv(Value + 1);
const int32 Last = FloorDiv(static_cast<int64>(Value) + 1);
for (int32 Coord = First; Coord <= Last; ++Coord) { Candidates.Add(Coord); }
return Candidates;
};
@@ -6338,7 +6352,7 @@ void AVoxelWorld::UpdateVisualStalenessAudit()
const FVector& WorldPosition, int32 Level, auto& OutCandidates)
{
const FVector LocalVoxel = WorldToLocalVoxel(WorldPosition);
const float TileExtentVoxels = static_cast<float>(CHUNK_SIZE * (1 << Level));
const float TileExtentVoxels = static_cast<float>(VF_ClipLevelExtentVoxels(Level));
TArray<int32, TInlineAllocator<2>> XCandidates;
TArray<int32, TInlineAllocator<2>> YCandidates;
TArray<int32, TInlineAllocator<2>> ZCandidates;
@@ -7182,7 +7196,7 @@ void AVoxelWorld::Tick(float DeltaTime)
{
const FIntVector Min = SteadyLevelMins.FindChecked(Level);
const FIntVector Max = SteadyLevelMaxs.FindChecked(Level);
const int32 ExtentVoxels = CHUNK_SIZE << Level;
const int32 ExtentVoxels = VF_ClipLevelExtentVoxels(Level);
const double TileExtentMetres = static_cast<double>(ExtentVoxels) * VOXEL_SIZE / 100.0;
const double SpanX = static_cast<double>(Max.X - Min.X + 1) * TileExtentMetres;
const double SpanY = static_cast<double>(Max.Y - Min.Y + 1) * TileExtentMetres;
@@ -7554,6 +7568,14 @@ bool AVoxelWorld::GetVoxelForgeCoreWorldAssembly(
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutContentHash.IsEmpty();
}
bool AVoxelWorld::GetVoxelForgeCoreResolvedLayout(
FString& OutCanonicalJson, FString& OutStableIdentity) const
{
OutCanonicalJson = VoxelForgeCoreResolvedLayoutJson;
OutStableIdentity = VoxelForgeCoreResolvedLayoutIdentity;
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutStableIdentity.IsEmpty();
}
bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const
{
OutDensity = 0.0f;
@@ -7572,6 +7594,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
OutFailureReason.Reset();
VoxelForgeCoreWorldAssemblyJson.Reset();
VoxelForgeCoreWorldAssemblyHash.Reset();
VoxelForgeCoreResolvedLayoutJson.Reset();
VoxelForgeCoreResolvedLayoutIdentity.Reset();
if (!bUseVoxelForgeCore) return true;
if (!Settings || !StrateManager)
{
@@ -7604,6 +7628,17 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason);
return false;
}
if (const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout = Snapshot->resolvedLayout())
{
const std::string CanonicalLayout = ResolvedLayout->ToJson();
if (CanonicalLayout.empty())
{
OutFailureReason = TEXT("the resolved Core layout could not be serialized canonically.");
return false;
}
VoxelForgeCoreResolvedLayoutJson = UTF8_TO_TCHAR(CanonicalLayout.c_str());
VoxelForgeCoreResolvedLayoutIdentity = UTF8_TO_TCHAR(ResolvedLayout->stableIdentity.c_str());
}
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
{
const std::string CanonicalAssembly = Assembly->ToJson();
@@ -7663,10 +7698,20 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
};
for (const VoxelForgeCore::TileKey& Key : InitialDesired)
{
const int32 Extent = 32 << Key.lod;
const int32 OriginX = Key.coordinate.x * Extent;
const int32 OriginY = Key.coordinate.y * Extent;
const int32 OriginZ = Key.coordinate.z * Extent;
const int32 Extent = VF_ClipLevelExtentVoxels(Key.lod);
const int64 OriginXWide = static_cast<int64>(Key.coordinate.x) * Extent;
const int64 OriginYWide = static_cast<int64>(Key.coordinate.y) * Extent;
const int64 OriginZWide = static_cast<int64>(Key.coordinate.z) * Extent;
if (OriginXWide < MIN_int32 || OriginYWide < MIN_int32 || OriginZWide < MIN_int32
|| OriginXWide + Extent > MAX_int32 || OriginYWide + Extent > MAX_int32
|| OriginZWide + Extent > MAX_int32)
{
OutFailureReason = TEXT("Core initial clipmap exceeds the supported voxel-coordinate range.");
return false;
}
const int32 OriginX = static_cast<int32>(OriginXWide);
const int32 OriginY = static_cast<int32>(OriginYWide);
const int32 OriginZ = static_cast<int32>(OriginZWide);
IncludeExtent(ClipMin.x, ClipMax.x, OriginX, Extent);
IncludeExtent(ClipMin.y, ClipMax.y, OriginY, Extent);
IncludeExtent(ClipMin.z, ClipMax.z, OriginZ, Extent);
@@ -7770,6 +7815,8 @@ void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString&
}
VoxelForgeCoreWorldAssemblyJson.Reset();
VoxelForgeCoreWorldAssemblyHash.Reset();
VoxelForgeCoreResolvedLayoutJson.Reset();
VoxelForgeCoreResolvedLayoutIdentity.Reset();
bUseVoxelForgeCore = false;
// The Core adapter sets this before validation; legacy generation must not inherit it.
// L'adaptateur Core l'active avant validation ; le générateur historique ne doit pas en hériter.
@@ -9914,6 +9961,9 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
Sample.QueueWaitSeconds = WorkerQueueSeconds + ResultQueueSeconds;
Sample.WorkerTaskSeconds = WorkerTaskSeconds;
Sample.GenerationSeconds = GenerationSeconds;
Sample.CoreGenerateSeconds = Result.CoreGenerateSeconds;
Sample.MeshCopySeconds = Result.MeshCopySeconds;
Sample.StreamBuildSeconds = Result.StreamBuildSeconds;
Sample.ClassifySeconds = Result.ClassifySeconds;
Sample.MeshSeconds = Result.MeshSeconds;
Sample.StreamSeconds = Result.StreamSeconds;
@@ -10074,8 +10124,11 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
{
BandChunkLo = MeshBandChunkLo;
BandChunkHi = MeshBandChunkHi;
BandVoxLo = MeshBandChunkLo * CHUNK_SIZE;
BandVoxHi = (MeshBandChunkHi + 1) * CHUNK_SIZE - 1;
BandVoxLo = static_cast<int32>(FMath::Clamp<int64>(
static_cast<int64>(MeshBandChunkLo) * CHUNK_SIZE, MIN_int32, MAX_int32));
BandVoxHi = static_cast<int32>(FMath::Clamp<int64>(
(static_cast<int64>(MeshBandChunkHi) + 1) * CHUNK_SIZE - 1,
MIN_int32, MAX_int32));
}
FChunkResult Result;
@@ -10238,7 +10291,7 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
ReadyToHide.Reserve(TileComponents.Num());
auto GetAncestorAtLevel = [](const FVoxelTileKey& Tile, int32 AncestorLevel)
{
const int32 Divisor = 1 << (AncestorLevel - Tile.Level);
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
return FVoxelTileKey(VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord, Divisor), AncestorLevel);
};
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& Pair : TileComponents)
@@ -10361,7 +10414,10 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
// Integer floor-division (correct for negatives), scalar + vector.
static FORCEINLINE int32 VF_FloorDiv(int32 V, int32 D)
{
return V >= 0 ? (V / D) : -(((-V) + D - 1) / D);
if (D <= 0) { return 0; }
const int32 Quotient = V / D;
const int32 Remainder = V % D;
return Remainder < 0 ? Quotient - 1 : Quotient;
}
static FORCEINLINE FIntVector VF_FloorDiv(const FIntVector& V, int32 D)
{
@@ -11441,13 +11497,13 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
{
VF_OuterShell(Settings, R, MaxLevelBase, MaxLevel, ROuter);
}
const int32 PowMax = 1 << MaxLevel;
const int32 PowMax = VF_ClipLevelScale(MaxLevel);
const FIntVector CMax = VF_FloorDiv(Center, PowMax);
FVector CV;
if (bUseVoxelForgeCore)
{
const double TileExtentVoxels = static_cast<double>(CHUNK_SIZE << Tile.Level);
const double TileExtentVoxels = static_cast<double>(VF_ClipLevelExtentVoxels(Tile.Level));
CV = FVector(
(static_cast<double>(Tile.Coord.X) + 0.5) * TileExtentVoxels,
(static_cast<double>(Tile.Coord.Y) + 0.5) * TileExtentVoxels,
@@ -11463,9 +11519,9 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
VF_FloorDiv(FMath::FloorToInt(CV.Y), SizeMax),
VF_FloorDiv(FMath::FloorToInt(CV.Z), SizeMax));
return FMath::Abs(TMax.X - CMax.X) <= ROuter
&& FMath::Abs(TMax.Y - CMax.Y) <= ROuter
&& FMath::Abs(TMax.Z - CMax.Z) <= ROuter;
return FMath::Abs(static_cast<int64>(TMax.X) - CMax.X) <= ROuter
&& FMath::Abs(static_cast<int64>(TMax.Y) - CMax.Y) <= ROuter
&& FMath::Abs(static_cast<int64>(TMax.Z) - CMax.Z) <= ROuter;
}
int32 AVoxelWorld::GetMaxConcurrentTasks() const
@@ -11575,7 +11631,8 @@ void AVoxelWorld::EnqueueStreamingResult(FChunkResult&& Result)
{
EditedProcessQueue.Enqueue(MoveTemp(Result));
}
else if (Result.WorkPriority == EVoxelTileWorkPriority::LOD0)
else if (Result.WorkPriority == EVoxelTileWorkPriority::LOD0
|| Result.Tile.Level <= 2)
{
NearProcessQueue.Enqueue(MoveTemp(Result));
}
@@ -11818,11 +11875,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
}
}
const int32 MaxTasks = GetMaxConcurrentStreamingTasks();
// Keep at most one worker-set of ready work so an admission pass can feed idle slots
// without letting far work consume the near reserve. / Garder une file d'un ensemble
// de workers pour nourrir les slots sans consommer la reserve proche.
// Keep several bounded worker-sets ready so frame-paced admission can feed the
// background lane continuously without moving mesh application onto the game thread.
const int32 ReadyQueueDepth = FMath::Clamp(
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4));
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
const int32 AuthoredNearTaskReserve = GetNearTaskReserve(MaxTasks);
const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition));
@@ -11892,8 +11948,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
const int32 NearTaskReserve = EffectiveNearTaskReserve;
const int32 NearAdmissionCap = bPawnMoving ? MaxTasks : TotalAdmissionCap;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve) + ReadyQueueDepth;
const int32 FirstCoarseLevel = FMath::Max(
2, Settings ? Settings->FullResClipLevels : 2);
constexpr int32 FirstFarLevel = 3;
const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0
&& bWalkableRingReadyForReserve;
int32 SchedulerSubmittedByLevel[9] = {};
@@ -12058,7 +12113,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
LevelSummaries.Num(), Leavers.Num(), CollisionOnlyTiles.Num()));
for (const FDesiredLevelSummary& Summary : LevelSummaries)
{
const int32 ExtentVoxels = CHUNK_SIZE << Summary.Level;
const int32 ExtentVoxels = VF_ClipLevelExtentVoxels(Summary.Level);
const double TileExtentMetres = static_cast<double>(ExtentVoxels) * VOXEL_SIZE / 100.0;
const double SpanX = static_cast<double>(Summary.Max.X - Summary.Min.X + 1) * TileExtentMetres;
const double SpanY = static_cast<double>(Summary.Max.Y - Summary.Min.Y + 1) * TileExtentMetres;
@@ -12109,8 +12164,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
for (const FVoxelTileKey& T : LoadedTiles)
{
if (T.Level < CutMin) continue;
const int32 CLo = T.Coord.Z << T.Level;
const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1;
const int64 LevelScale = VF_ClipLevelScale(T.Level);
const int64 CLo = static_cast<int64>(T.Coord.Z) * LevelScale;
const int64 CHi = (static_cast<int64>(T.Coord.Z) + 1) * LevelScale - 1;
const bool bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi;
const bool bInNew = CLo >= NewLo && CHi <= NewHi;
const bool bOutOld = CHi < MeshBandChunkLo || CLo > MeshBandChunkHi;
@@ -12330,7 +12386,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
}
Heading = Heading.GetSafeNormal();
const FVector PlayerLocalCm = WorldToLocalCm(CenterPosition);
const double TileSizeCm = static_cast<double>(CHUNK_SIZE << Level)
const double TileSizeCm = static_cast<double>(VF_ClipLevelExtentVoxels(Level))
* GetActiveVoxelSizeCm();
LevelTiles.Sort([&](const FVoxelTileKey& A, const FVoxelTileKey& B)
{
@@ -12369,34 +12425,55 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
}
};
// Coarse-first gives the far shell the first non-critical admissions. Keep the current
// collision-ready 3x3 ring above it, then spend the remaining slots on full-resolution
// refinement. The opt-out reproduces the S4 LOD0-to-LOD8 submission order for a control.
// Le shell lointain passe apres l'anneau de collision, puis le raffinement fin. L'option
// inverse restaure l'ordre LOD0-vers-LOD8 de S4 comme controle negatif.
// Keep the walkable ring on the legacy near-first path; only spend its reserved task slots
// on coarse work after all nine collision gates are ready.
// Garder l'anneau praticable en priorite ; les taches grossieres commencent apres ses neuf collisions.
const auto AreCriticalTilesReady = [this]()
{
for (const FVoxelTileKey& Tile : CriticalDesiredTiles)
{
if (!LoadedTiles.Contains(Tile) && !CollisionNotRequiredTiles.Contains(Tile))
{
return false;
}
}
return true;
};
const auto AreNearLODTilesReady = [this]()
{
for (const FVoxelTileKey& Tile : DesiredSorted)
{
if (Tile.Level <= 2 && !LoadedTiles.Contains(Tile)
&& !CollisionNotRequiredTiles.Contains(Tile))
{
return false;
}
}
return true;
};
// The support and collision-critical set drains first. Then LOD0, LOD1, and LOD2
// use the near lane. Far tiles are not admitted until every requested near tile is ready.
if (AreCriticalTilesReady() && bWalkableRingReadyForReserve)
{
for (int32 Level = 0; Level <= 2 && Level <= MaxDesiredLevel; ++Level)
{
const EVoxelTileWorkPriority Priority = Level == 0
? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon;
SubmitLevel(Level, Priority, NearAdmissionCap, true);
}
}
if (AreNearLODTilesReady())
{
if (bUseCoarseFirstOrder)
{
for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level)
for (int32 Level = MaxDesiredLevel; Level >= FirstFarLevel; --Level)
{
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
CoarseTaskCap, true);
}
for (int32 Level = 0; Level < FirstCoarseLevel && Level <= MaxDesiredLevel; ++Level)
{
const EVoxelTileWorkPriority Priority = Level == 0
? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon;
const int32 LevelAdmissionCap = Priority == EVoxelTileWorkPriority::Horizon
? CoarseTaskCap : NearAdmissionCap;
SubmitLevel(Level, Priority, LevelAdmissionCap, true);
}
}
else
{
SubmitLevel(0, EVoxelTileWorkPriority::LOD0, NearAdmissionCap, false);
for (int32 Level = 1; Level <= MaxDesiredLevel; ++Level)
for (int32 Level = FirstFarLevel; Level <= MaxDesiredLevel; ++Level)
{
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
CoarseTaskCap, false);
@@ -12409,6 +12486,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
}
}
}
}
if (bMeasureReadyTransitionVisibility)
{
FFarFieldSubmitSample& Sample = FarFieldSubmitSamples.AddDefaulted_GetRef();
@@ -12565,9 +12643,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
> ResultBacklogCapStopsAtUpdateEntry;
for (int32 Level = 0; Level < 9; ++Level)
{
AdmissionCapByLevel[Level] = bUseCoarseFirstOrder
? (Level >= FirstCoarseLevel || Level > 0 ? CoarseTaskCap : NearAdmissionCap)
: (Level == 0 ? NearAdmissionCap : CoarseTaskCap);
AdmissionCapByLevel[Level] = Level <= 2 ? NearAdmissionCap : CoarseTaskCap;
if (DesiredMissingByLevel[Level] <= 0) continue;
if (bAllChunksLoaded)
@@ -12655,7 +12731,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
"\"max_tasks\":%d,\"ready_queue_depth\":%d,"
"\"total_admission_cap\":%d,\"near_admission_cap\":%d,"
"\"near_reserve\":%d,"
"\"coarse_cap\":%d,\"first_coarse_level\":%d,"
"\"coarse_cap\":%d,\"first_far_level\":%d,"
"\"submission_stop_level\":%d,\"submission_stop_cap\":%d,"
"\"pending_entry\":%d,\"pending_after\":%d,\"admission_occupancy\":%d,"
"\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%d,"
@@ -12678,7 +12754,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
GVoxelForgeStreamingLegacyPendingResultCap,
GVoxelForgeCoreRegionConcurrentAdmission, MaxTasks, ReadyQueueDepth,
TotalAdmissionCap, NearAdmissionCap, NearTaskReserve, CoarseTaskCap, FirstCoarseLevel,
TotalAdmissionCap, NearAdmissionCap, NearTaskReserve, CoarseTaskCap, FirstFarLevel,
SchedulerOrderStopLevel, SchedulerOrderStopCap,
PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard,
ActiveGenerationWorkerCount.load(std::memory_order_relaxed), PendingResultsEstimate,
@@ -12731,17 +12807,21 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); // Core and worker lane clamped. / Core et voie worker plafonnes.
const int32 ReadyQueueDepth = FMath::Clamp(
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4));
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon
|| Tile.Level <= 2;
const int32 NearReserve = EffectiveNearTaskReserve >= 0
? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1))
: GetNearTaskReserve(MaxTasks);
const int32 NearAdmissionCap = bStreamingPlayerMoving ? MaxTasks : TotalAdmissionCap;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth;
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
&& Tile.Level > 2
? CoarseTaskCap
: (WorkPriority == EVoxelTileWorkPriority::LOD0 ? NearAdmissionCap : TotalAdmissionCap);
: ((WorkPriority == EVoxelTileWorkPriority::LOD0
|| (WorkPriority == EVoxelTileWorkPriority::Horizon && Tile.Level <= 2))
? NearAdmissionCap : TotalAdmissionCap);
if (GetGenerationAdmissionCount() >= AdmissionCap)
{
if (GVoxelForgeProfileTileGeneration != 0
@@ -12788,7 +12868,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
const int32 FullRes = GVoxelForgeTestFullResClipLevels >= 0
? FMath::Clamp(GVoxelForgeTestFullResClipLevels, 1, 9)
: (Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2);
const int32 Extent = CHUNK_SIZE << Tile.Level;
const int32 Extent = VF_ClipLevelExtentVoxels(Tile.Level);
const int32 Cells = (Tile.Level < FullRes)
? CHUNK_SIZE
@@ -12853,7 +12933,9 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
// et mélanges de matériaux qu'avant la coupe) — la tuile ne peut rendre que des artefacts
// ⇒ on n'émet RIEN. Résultat vide via ProcessQueue (bookkeeping normal : PendingTiles,
// LoadedTiles, epoch) ; jamais figé — le changement de bande re-queue via BandRemeshQueue.
if (Step > (BandChunkHi - BandChunkLo + 1) * CHUNK_SIZE)
const int64 BandHeightVoxels =
(static_cast<int64>(BandChunkHi) - BandChunkLo + 1) * CHUNK_SIZE;
if (static_cast<int64>(Step) > BandHeightVoxels)
{
FChunkResult Empty;
Empty.Tile = Tile;
@@ -13072,17 +13154,21 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
const int32 MaxTasks = GetMaxConcurrentStreamingTasks();
const int32 ReadyQueueDepth = FMath::Clamp(
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4));
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon
|| Tile.Level <= 2;
const int32 NearReserve = EffectiveNearTaskReserve >= 0
? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1))
: GetNearTaskReserve(MaxTasks);
const int32 NearAdmissionCap = bStreamingPlayerMoving ? MaxTasks : TotalAdmissionCap;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth;
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
&& Tile.Level > 2
? CoarseTaskCap
: (WorkPriority == EVoxelTileWorkPriority::LOD0 ? NearAdmissionCap : TotalAdmissionCap);
: ((WorkPriority == EVoxelTileWorkPriority::LOD0
|| (WorkPriority == EVoxelTileWorkPriority::Horizon && Tile.Level <= 2))
? NearAdmissionCap : TotalAdmissionCap);
if (GetGenerationAdmissionCount() >= AdmissionCap) return;
PendingTiles.Add(Tile);
@@ -13180,6 +13266,11 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
Result.WorkPriority = WorkPriority;
Result.GenerationStartCycles = FPlatformTime::Cycles64();
Result.GenerationOrdinal = NextStreamingGenerationOrdinal.fetch_add(1, std::memory_order_relaxed) + 1;
#if PLATFORM_LINUX
const bool bMeasureWorkerPhases = VoxelForgeStartupTrace::IsActive();
#else
constexpr bool bMeasureWorkerPhases = false;
#endif
auto EnqueueObsolete = [&]()
{
@@ -13235,7 +13326,14 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
FReadScopeLock Lock(State->WorldLock);
if (State->World)
{
const uint64 CoreGenerateStartCycles = bMeasureWorkerPhases
? FPlatformTime::Cycles64() : 0;
CoreMesh = State->World->generateTile(CoreKey, Options, CoreError);
if (CoreGenerateStartCycles != 0)
{
Result.CoreGenerateSeconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - CoreGenerateStartCycles);
}
}
}
Result.GenerationEndCycles = FPlatformTime::Cycles64();
@@ -13277,6 +13375,8 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
if (!Result.bEmpty)
{
const uint64 MeshCopyStartCycles = bMeasureWorkerPhases
? FPlatformTime::Cycles64() : 0;
FVoxelMeshData MeshData;
MeshData.Vertices.Reserve(Result.NumVertices);
MeshData.Normals.Reserve(Result.NumVertices);
@@ -13297,8 +13397,20 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
MeshData.NumSurfaceVertices = static_cast<int32>(CoreMesh->groups.surfaceVertexCount);
MeshData.NumCeilingTriangles = Result.NumCeilingTriangles;
MeshData.NumSkirtTriangles = Result.NumSkirtTriangles;
if (MeshCopyStartCycles != 0)
{
Result.MeshCopySeconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - MeshCopyStartCycles);
}
const uint64 StreamBuildStartCycles = bMeasureWorkerPhases
? FPlatformTime::Cycles64() : 0;
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
BuildTileStreamSet(*Result.Streams, MeshData);
if (StreamBuildStartCycles != 0)
{
Result.StreamBuildSeconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - StreamBuildStartCycles);
}
}
Result.GenerationEndCycles = FPlatformTime::Cycles64();
if (Cancellation->bObsolete.load(std::memory_order_acquire)
@@ -13988,7 +14100,8 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
// Convertir les bornes physiques de tuile vers les strates owner, y compris les rapports fractionnaires.
const double OwnerChunksPerCoreTile = static_cast<double>(GetActiveVoxelSizeCm())
/ VOXEL_SIZE;
const double TileExtentCoreVoxels = static_cast<double>(CHUNK_SIZE << Tile.Level);
const double TileExtentCoreVoxels =
static_cast<double>(VF_ClipLevelExtentVoxels(Tile.Level));
const int32 MinOwnerChunkX = FMath::FloorToInt(
Tile.Coord.X * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE);
const int32 MinOwnerChunkY = FMath::FloorToInt(
@@ -15084,6 +15197,7 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
}
FString AssemblyHash = VoxelForgeCoreWorldAssemblyHash;
FString ResolvedLayoutIdentity = VoxelForgeCoreResolvedLayoutIdentity;
if (AssemblyHash.IsEmpty())
{
FString AssemblyJson;
@@ -15120,12 +15234,17 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
AssemblyHash = FString::Printf(TEXT("%016llx"),
static_cast<unsigned long long>(Assembly->contentHash));
}
if (const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout = Snapshot->resolvedLayout())
{
ResolvedLayoutIdentity = UTF8_TO_TCHAR(ResolvedLayout->stableIdentity.c_str());
}
}
}
const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
const uint32 CoreVoxelSizeMicrons = static_cast<uint32>(
FMath::RoundToInt(GetActiveVoxelSizeCm() * 10000.0f));
if (AssemblyHash.IsEmpty() && !bHybridMesher && CoreVoxelSizeMicrons == 250000)
if (AssemblyHash.IsEmpty() && ResolvedLayoutIdentity.IsEmpty()
&& !bHybridMesher && CoreVoxelSizeMicrons == 250000)
{
return SeasonHash;
}
@@ -15150,6 +15269,11 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
Payload += TEXT("|world_assembly=");
Payload += AssemblyHash;
}
if (!ResolvedLayoutIdentity.IsEmpty())
{
Payload += TEXT("|resolved_layout=");
Payload += ResolvedLayoutIdentity;
}
if (CoreVoxelSizeMicrons != 250000)
{
Payload += FString::Printf(TEXT("|core_voxel_size_microns=%u"), CoreVoxelSizeMicrons);
@@ -15180,7 +15304,7 @@ void AVoxelWorld::RemeshDirtyChunks(const TArray<FIntVector>& DirtyCoords, const
// BackgroundHigh; the old component remains visible until its post-edit replacement is applied.
auto TileCoversDirtyCoord = [&DirtyCoords](const FVoxelTileKey& Tile) -> bool
{
const int32 Span = 1 << Tile.Level;
const int32 Span = VF_ClipLevelScale(Tile.Level);
const FIntVector MinChunk = Tile.Coord * Span;
const FIntVector MaxChunk = MinChunk + FIntVector(Span, Span, Span);
for (const FIntVector& Coord : DirtyCoords)
+20 -9
View File
@@ -261,8 +261,12 @@ namespace VoxelHash
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
{
uint32 H = Seed;
H ^= Mix((uint32)(CellX + 0x7FFFFFFF));
H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u);
// Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for
// positive cells and makes the world hash compiler-dependent.
const uint32 BiasedX = static_cast<uint32>(CellX) + 0x7FFFFFFFu;
const uint32 BiasedY = static_cast<uint32>(CellY) + 0x7FFFFFFFu;
H ^= Mix(BiasedX);
H ^= Mix(BiasedY * 2654435761u);
return Mix(H);
}
@@ -274,10 +278,14 @@ namespace VoxelHash
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
uint32 H = Seed ^ 0xDEADBEEF;
H ^= Mix((uint32)(MinX + 0x7FFFFFFF));
H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u);
H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u);
H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u);
const uint32 BiasedMinX = static_cast<uint32>(MinX) + 0x7FFFFFFFu;
const uint32 BiasedMinY = static_cast<uint32>(MinY) + 0x7FFFFFFFu;
const uint32 BiasedMaxX = static_cast<uint32>(MaxX) + 0x7FFFFFFFu;
const uint32 BiasedMaxY = static_cast<uint32>(MaxY) + 0x7FFFFFFFu;
H ^= Mix(BiasedMinX);
H ^= Mix(BiasedMinY * 2654435761u);
H ^= Mix(BiasedMaxX * 374761393u);
H ^= Mix(BiasedMaxY * 668265263u);
return Mix(H);
}
@@ -477,10 +485,13 @@ namespace VoxelNoise
// Hash the neighbor cell to get a feature point position [0,1)
// Using three different hash mixes for X, Y, Z offsets
const uint32 BiasedX = static_cast<uint32>(NX) + 0x7FFFFFFFu;
const uint32 BiasedY = static_cast<uint32>(NY) + 0x7FFFFFFFu;
const uint32 BiasedZ = static_cast<uint32>(NZ) + 0x7FFFFFFFu;
uint32 H = VoxelHash::Mix(
(uint32)(NX + 0x7FFFFFFF)
^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u)
^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u)
BiasedX
^ VoxelHash::Mix(BiasedY * 2654435761u)
^ VoxelHash::Mix(BiasedZ * 374761393u)
);
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
@@ -340,6 +340,14 @@ private:
float SuppressRadiusWorld = 0.0f;
};
// Game-thread lookup copied from the immutable resolved Core layout.
// Recherche game-thread copiee depuis la disposition Core resolue et immuable.
struct FResolvedCoreGeometryAnchor
{
int32 StrateIndex = INDEX_NONE;
FVector LocalPositionCm = FVector::ZeroVector;
};
/** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle
* (cheap, O(1)/column); other archetypes ray-march the density column. No UObject access except
* Generator (thread-safe). Determinism-critical. Resolves the dominant biome PER COLUMN
@@ -436,6 +444,7 @@ private:
// (kept until the cell leaves range so the surface-find isn't repeated). Strate-bounded.
// Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint.
TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
TMap<FString, FResolvedCoreGeometryAnchor> CoreGeometryAnchors;
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
// Set in BeginDestroy; worker tasks check it before touching us.
+10 -6
View File
@@ -21,6 +21,8 @@ class UVoxelSettings : public UPrimaryDataAsset
GENERATED_BODY()
public:
virtual void Serialize(FArchive& Ar) override;
virtual void PostLoad() override;
/** Season seed/radius replace the mutable authored values only when Season is assigned. */
int32 GetEffectiveWorldSeed() const;
@@ -411,14 +413,16 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
int32 TotalStrates = 10;
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock.
// 0 = strates touch (just the seals between them). Higher = a thick rock layer
// between each biome that the player must dig through at (0,0), and that the
// auto-carved passages tunnel across.
// 0 → adjacent layers (default) · 1-2 → a real bedrock band · 4+ → deep separation
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "0", ClampMax = "16"))
// Legacy count retained so pre-metre assets still deserialize during the upgrade.
// Ancien compte conserve pour lire les assets precedant les metres.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (DeprecatedProperty, DeprecationMessage = "Use InterStrateGapMeters."))
int32 InterStrateGapChunks = 0;
// Physical separation between strata; the layout remains aligned to whole layout chunks.
// Separation physique; la disposition reste alignee sur des chunks entiers.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (Units = "m", ClampMin = "0", ClampMax = "1024"))
double InterStrateGapMeters = 0.0;
// Physical width of one vertical strate-layout chunk. This is independent of the
// Core tile width (32^3 voxels) and of the source generator's voxel resolution.
// 8 m preserves the owner's current SurfaceWorld extent at 25 cm Core voxels.
@@ -35,6 +35,9 @@ namespace VoxelForgeStartupTrace
double ResultQueueSeconds = 0.0;
double WorkerTaskSeconds = 0.0;
double GenerationSeconds = 0.0;
double CoreGenerateSeconds = 0.0;
double MeshCopySeconds = 0.0;
double StreamBuildSeconds = 0.0;
double ClassifySeconds = 0.0;
double MeshSeconds = 0.0;
double StreamSeconds = 0.0;
@@ -48,6 +48,7 @@ class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
GENERATED_BODY()
public:
virtual void Serialize(FArchive& Ar) override;
virtual void PostLoad() override;
//=========================================================================
@@ -66,14 +67,14 @@ public:
// DIMENSIONS
//=========================================================================
// How many chunks tall this strate is.
// Each strate can have a different height!
// Human-scale defaults use 16 chunks = 64 m. This gives a 20 m+ vertical budget for
// cathedral rooms, tall shafts, and the surface sky cap without changing the lateral gate.
// 1 chunk = 32 voxels x 12.5cm = 4 m; 16 chunks preserves the 64 m authored default.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "256"))
// Legacy serialized chunk count kept for Blueprint/package compatibility during the unit migration.
// Ancien nombre de chunks conserve pour la compatibilite Blueprint/package pendant la migration.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (DeprecatedProperty, DeprecationMessage = "Use StrateHeightMeters."))
int32 StrateHeightInChunks = 16;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (Units = "m", ClampMin = "1", ClampMax = "2048"))
double StrateHeightMeters = 128.0;
//=========================================================================
// BOUNDARY TRANSITION
//=========================================================================
@@ -99,9 +100,13 @@ public:
// 2 → moderate (good default)
// 4 → wide, gentle blend across many chunks
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
meta = (ClampMin = "1", ClampMax = "8", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
meta = (DeprecatedProperty, DeprecationMessage = "Use TransitionWidthMeters."))
int32 TransitionBlendChunks = 2;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
meta = (Units = "m", ClampMin = "0", ClampMax = "512", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
double TransitionWidthMeters = 16.0;
//=========================================================================
// GENERATOR TYPE
//=========================================================================
@@ -115,6 +120,11 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
// Optional native Core authoring; leaving it empty keeps the established generator path.
// Edition Core native facultative; laissez vide pour garder le generateur existant.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Core Geometry")
FVoxelForgeCoreGeometry CoreGeometry;
/**
* Authoritative per-strate worm switch. Existing assets default to enabled. The manager
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
+207
View File
@@ -200,6 +200,208 @@ enum class ECaveGeneratorType : uint8
Underwater UMETA(DisplayName = "Underwater (flooded cavern system)"),
};
// Optional native-Core recipe selected independently for one strate.
// Recette Core native facultative, choisie independamment pour chaque strate.
UENUM(BlueprintType)
enum class EVoxelForgeCoreGeneratorRecipe : uint8
{
None UMETA(DisplayName = "Off (legacy recipe)"),
NativeCaves UMETA(DisplayName = "Native caves"),
NativeLattice UMETA(DisplayName = "Native lattice"),
ProfiledHeightfield UMETA(DisplayName = "Profiled heightfield")
};
UENUM(BlueprintType)
enum class EVoxelForgeCoreHeightfieldProfile : uint8
{
PeriodicHills,
MirroredRelief,
FlatField
};
UENUM(BlueprintType)
enum class EVoxelForgeCoreStampKind : uint8
{
CarveEllipsoid,
CarveArch,
FillPillar,
FillDais,
FillMonolith,
FillPlatform
};
// Stable authored volume stamp. All distances and positions are metres at the asset boundary.
// Volume stable, defini par son auteur. Toutes les positions et distances sont en metres.
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelForgeCoreStampDefinition
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
FString StableName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
EVoxelForgeCoreStampKind Kind = EVoxelForgeCoreStampKind::CarveEllipsoid;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector AnchorPositionMeters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double PlacementRadiusMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
FVector HalfExtentsMeters = FVector(2.0, 2.0, 2.0);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double SmoothWidthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityAmplitudeMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityWavelengthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double RoundingRadiusMeters = 0.5;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double MinimumSpacingMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (ClampMin = "0", ClampMax = "3"))
uint8 YawQuarterTurns = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
bool bAttachToFloor = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
double FloorMeters = 0.0;
};
// Authored cavern chamber, in local world metres.
// Salle cavernicole authored, en metres locaux du monde.
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelForgeCoreRoomDefinition
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
FString StableName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector CenterMeters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
FVector HalfExtentsMeters = FVector(8.0, 8.0, 4.0);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
double FloorMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double SmoothWidthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double UnionWidthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityAmplitudeMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityWavelengthMeters = 0.0;
};
// Authored cubic tunnel between a stable start anchor and a stable room name.
// Tunnel cubique authored entre une ancre stable et une salle nommee.
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelForgeCoreTunnelDefinition
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
FString StableName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
FString StartAnchorStableName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
FString EndRoomStableName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector ControlPoint0Meters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector ControlPoint1Meters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector ControlPoint2Meters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector ControlPoint3Meters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
double BoreRadiusMeters = 2.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "1"))
double BoreHeightMeters = 4.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double FloorSupportDepthMeters = 0.5;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double SmoothWidthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double UnionWidthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityAmplitudeMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityWavelengthMeters = 0.0;
// Lattice tunnels require an explicit bounded polyline; cave tunnels use the cubic curve.
// Les tunnels en treillis exigent une polyligne bornee; les tunnels caves utilisent la courbe cubique.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
TArray<FVector> PolylineMeters;
};
// Optional Core authored recipe. Empty arrays and None preserve the prior asset output.
// Recette Core authored facultative. None et les tableaux vides conservent la sortie historique.
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelForgeCoreGeometry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
EVoxelForgeCoreGeneratorRecipe GeneratorRecipe = EVoxelForgeCoreGeneratorRecipe::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
EVoxelForgeCoreHeightfieldProfile HeightfieldProfile = EVoxelForgeCoreHeightfieldProfile::PeriodicHills;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
double HeightfieldBaseMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
FVector2D HeightfieldAmplitudeMeters = FVector2D(20.0, 20.0);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
FVector2D HeightfieldWavelengthMeters = FVector2D(256.0, 256.0);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (ClampMin = "0.01", ClampMax = "4.0"))
double MirrorLambda = 1.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
double MinimumArenaClearanceMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
TArray<FVoxelForgeCoreStampDefinition> Stamps;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
TArray<FVoxelForgeCoreRoomDefinition> Rooms;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
TArray<FVoxelForgeCoreTunnelDefinition> Tunnels;
};
/**
* EVoxelStrateTransition — How two adjacent strates meet at their shared boundary.
*
@@ -2289,6 +2491,11 @@ struct VOXELFORGE_API FStrateLandmark
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice;
// Optional stable Core feature binding; an empty value keeps lattice/mouth placement unchanged.
// Liaison facultative vers une ancre Core stable; vide, le placement existant reste identique.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Core Geometry")
FString CoreGeometryStableName;
// ----- Hash lattice (AnchorMode == HashLattice — scattered placement; cheap at any radius) -----
// Average spacing between instances, IN CHUNKS = the lattice cell size (one candidate per cell, so cost
+21 -4
View File
@@ -332,16 +332,33 @@ struct FVoxelTileKey
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
int32 StepVoxels() const { return 1 << Level; }
int32 ExtentVoxels() const { return CHUNK_SIZE << Level; }
int32 StepVoxels() const
{
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
return static_cast<int32>(uint32{1} << SafeLevel);
}
int32 ExtentVoxels() const
{
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
return static_cast<int32>(static_cast<uint32>(CHUNK_SIZE) * (uint32{1} << SafeLevel));
}
// Min-corner of the tile in VOXEL coords (what the mesher takes).
FIntVector OriginVoxels() const { return Coord * (CHUNK_SIZE << Level); }
FIntVector OriginVoxels() const
{
const int64 Extent = ExtentVoxels();
const auto ScaleAxis = [Extent](int32 Axis)
{
return static_cast<int32>(FMath::Clamp<int64>(
static_cast<int64>(Axis) * Extent, MIN_int32, MAX_int32));
};
return FIntVector(ScaleAxis(Coord.X), ScaleAxis(Coord.Y), ScaleAxis(Coord.Z));
}
// Tile centre in world cm (for distance sorting / LOD selection).
FVector CenterCm() const
{
const double Ext = (double)(CHUNK_SIZE << Level) * (double)VOXEL_SIZE;
const double Ext = static_cast<double>(ExtentVoxels()) * static_cast<double>(VOXEL_SIZE);
return FVector((Coord.X + 0.5) * Ext, (Coord.Y + 0.5) * Ext, (Coord.Z + 0.5) * Ext);
}
+7
View File
@@ -145,6 +145,10 @@ struct FChunkResult
// per-tile profiling remains opt-in.
uint64 GenerationStartCycles = 0;
uint64 GenerationEndCycles = 0;
// Optional phase timings recorded only while the bounded startup trace is active.
double CoreGenerateSeconds = 0.0;
double MeshCopySeconds = 0.0;
double StreamBuildSeconds = 0.0;
uint64 ApplyStartCycles = 0;
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
// Ordinals make admitted order and actual worker start order auditable across MPSC results.
@@ -614,6 +618,7 @@ public:
FVector GetVoxelForgeCoreDefaultCenterWorld() const;
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
bool GetVoxelForgeCoreResolvedLayout(FString& OutCanonicalJson, FString& OutStableIdentity) const;
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
int32 GetStrateAtPosition(FVector WorldPosition) const;
@@ -778,6 +783,8 @@ private:
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
FString VoxelForgeCoreWorldAssemblyJson;
FString VoxelForgeCoreWorldAssemblyHash;
FString VoxelForgeCoreResolvedLayoutJson;
FString VoxelForgeCoreResolvedLayoutIdentity;
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
bool bCoreRegionCompilePending = false;
TArray<FVoxelTileKey> PendingCoreRegionTiles;
+7
View File
@@ -7,6 +7,13 @@ public class VoxelForge : ModuleRules
{
public VoxelForge(ReadOnlyTargetRules Target) : base(Target)
{
if (Target.Platform == UnrealTargetPlatform.Linux)
{
// Test translation units have independent anonymous-namespace helpers that collide in
// Linux Unity batches. Keep Linux compilation one source per TU; Windows remains unchanged.
bUseUnity = false;
}
// PCH = Precompiled Headers - speeds up compilation
// UseExplicitOrSharedPCHs is the modern recommended setting
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
@@ -39,6 +39,13 @@ public class VoxelForgeCore : ModuleRules
PublicDependencyModuleNames.Add("Core");
CppStandard = CppStandardVersion.Cpp20;
if (Target.Platform == UnrealTargetPlatform.Linux)
{
// The compatibility include directory contains Unreal-shaped placeholders. On Linux,
// let generated module TUs pass those names through to UE while Core shims opt in.
PrivateDefinitions.Add("VFCORE_COMPAT_FORWARD_HEADERS=1");
MinCpuArchX64 = MinimumCpuArchitectureX64.AVX2;
}
// Canonical Standalone code predates UE's stricter warning defaults; keep this narrow to legal local shadowing.
CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off;
FPSemantics = FPSemanticsMode.Precise;
@@ -878,6 +878,8 @@ struct FExploreArguments
bool bPassageFloorWritesOverride = false;
int32 PassageFloorWrites = 1;
bool bStandaloneWorldExport = false;
bool bOwnerFourStratePlan = false;
bool bSkipStandaloneCoreAdapterParity = false;
bool bCoreWorldTest = false;
bool bCoreWorldStreamTraceOnly = false;
bool bRenderCameraOverride = false;
@@ -922,6 +924,12 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
int32 StandaloneWorldExport = 0;
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
int32 OwnerFourStratePlan = 0;
FParse::Value(*Params, TEXT("ownerfourstrateplan="), OwnerFourStratePlan);
OutArguments.bOwnerFourStratePlan = OwnerFourStratePlan != 0;
int32 SkipStandaloneCoreAdapterParity = 0;
FParse::Value(*Params, TEXT("skipstandalonecoreadapterparity="), SkipStandaloneCoreAdapterParity);
OutArguments.bSkipStandaloneCoreAdapterParity = SkipStandaloneCoreAdapterParity != 0;
int32 CoreWorldTest = 0;
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
@@ -1266,12 +1274,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
return false;
}
// The historical explorer contract needs two adjacent mouths around an interior slot. A
// parity export can instead target the game's authored single-strate layout, which has no
// such mouths and is valid for export-only mode. Build() still validates the slot against
// the loaded settings layout.
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig ? 0 : 1;
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig
// Passage walks need both mouths; a render/export-only generation can inspect an end slot.
// Parity exports also accept the game's authored single-strate layout.
const bool bRequiresInteriorSlot = OutArguments.bWalk;
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot ? 0 : 1;
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot
? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2;
if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot)
{
@@ -1284,8 +1291,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
else
{
OutError = FString::Printf(
TEXT("slot must be an interior slot in [1,%d] so arrival and departure mouths exist."),
MaxSyntheticStrates - 2);
TEXT("slot must be in [0,%d]; walk mode requires an interior slot in [1,%d] for both passage mouths."),
MaxSyntheticStrates - 1, MaxSyntheticStrates - 2);
}
return false;
}
@@ -1498,6 +1505,17 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
return false;
}
if (OutArguments.bOwnerFourStratePlan
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
{
OutError = TEXT("ownerfourstrateplan requires settingsref and must omit strateref.");
return false;
}
if (OutArguments.bSkipStandaloneCoreAdapterParity && !OutArguments.bStandaloneWorldExport)
{
OutError = TEXT("skipstandalonecoreadapterparity requires standaloneworldexport.");
return false;
}
if (OutArguments.bCoreWorldTest
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
{
@@ -1702,6 +1720,66 @@ struct FExploreMeshBvhNode
}
};
bool ApplyOwnerFourStratePlan(UVoxelSettings* Settings,
TArray<TStrongObjectPtr<UVoxelStrateDefinition>>& OutDefinitions,
bool bDuplicateDefinitions, FString& OutError)
{
if (Settings == nullptr)
{
OutError = TEXT("The transient owner plan has no settings object.");
return false;
}
if (!Settings->Season.IsNull())
{
OutError = TEXT("The transient four-strate owner plan cannot replace an assigned season manifest.");
return false;
}
static const TCHAR* AssetPaths[] = {
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
};
Settings->TotalStrates = UE_ARRAY_COUNT(AssetPaths);
Settings->StratePool.Reset();
Settings->FixedStrates.Reset();
OutDefinitions.Reset();
OutDefinitions.Reserve(UE_ARRAY_COUNT(AssetPaths));
for (int32 Index = 0; Index < UE_ARRAY_COUNT(AssetPaths); ++Index)
{
UVoxelStrateDefinition* AuthoredDefinition = LoadObject<UVoxelStrateDefinition>(
nullptr, AssetPaths[Index]);
if (AuthoredDefinition == nullptr)
{
OutError = FString::Printf(
TEXT("Could not load staged owner strate asset '%s'."), AssetPaths[Index]);
return false;
}
UVoxelStrateDefinition* Definition = AuthoredDefinition;
if (bDuplicateDefinitions)
{
Definition = DuplicateObject<UVoxelStrateDefinition>(
AuthoredDefinition, GetTransientPackage(),
FName(*FString::Printf(TEXT("VoxelForgeOwnerPlan_Strate_%d"), Index)));
if (Definition == nullptr)
{
OutError = FString::Printf(
TEXT("Could not make a transient copy of owner strate asset '%s'."), AssetPaths[Index]);
return false;
}
}
Settings->FixedStrates.Add(Index,
TSoftObjectPtr<UVoxelStrateDefinition>(Definition));
OutDefinitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeOwnerPlan] transient fixed slots=4 assets=DA_Strate1,DA_Strate2,DA_Strate3,DA_Strate4 settings_asset_untouched=1"));
return true;
}
struct FExploreWorld
{
TStrongObjectPtr<UVoxelSettings> Settings;
@@ -1834,7 +1912,7 @@ struct FExploreWorld
Settings->InterStrateGapChunks, Settings->SkirtCells);
}
const int32 NumStrates = bUseAuthoredSettings
const int32 NumStrates = Arguments.bOwnerFourStratePlan ? 4 : bUseAuthoredSettings
? (Arguments.bGameRuntimeConfig
? FMath::Max(Settings->TotalStrates, 1)
: FMath::Max(Settings->TotalStrates, Arguments.Slot + 2))
@@ -1864,10 +1942,18 @@ struct FExploreWorld
// loaded package remains untouched.
Settings->TotalStrates = NumStrates;
if (Arguments.bOwnerFourStratePlan)
{
if (!ApplyOwnerFourStratePlan(Settings.Get(), Definitions, true, OutError))
{
return false;
}
}
else
{
if (bUseAuthoredStrate)
{
// 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.
// Keep the historical single-asset interior probe isolated from loaded packages.
Settings->StratePool.Reset();
Settings->FixedStrates.Reset();
}
@@ -1923,6 +2009,7 @@ struct FExploreWorld
}
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
}
}
auto ApplyTargetOverrides = [&Arguments](UVoxelStrateDefinition* Definition) -> bool
{
@@ -13472,14 +13559,16 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
const std::array<std::uint64_t, 4> Actual = StandaloneCoreAggregateHashes(Rows);
const std::array<std::uint64_t, 4> Expected{
0xdb2991c73215af4cull, 0x8236c5edd7fc8e4full,
0x4c48fa75a543d58eull, 0xbea22e76b34a4dfcull};
0xde345030348f2c5dull, 0xcbf0888004618b87ull,
0x7a11e28e052d69a8ull, 0x52b88607e8dbcbfdull};
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
const bool bPassageWalkabilityPassed = Stats.walkSamples == 5642 && Stats.walkFailures == 0;
if (Rows.size() != 2226 || Actual != Expected || !bPassageWalkabilityPassed)
constexpr std::uint64_t ExpectedWalkSamples = 21425;
const bool bPassageWalkabilityPassed = Stats.walkSamples == ExpectedWalkSamples
&& Stats.walkFailures == 0;
if (Rows.size() != 2578 || Actual != Expected || !bPassageWalkabilityPassed)
{
OutError = FString::Printf(
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected=5642/5642"),
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected_walk=%llu"),
static_cast<unsigned long long>(WorkerLimit),
static_cast<unsigned long long>(Rows.size()),
static_cast<unsigned long long>(Actual[0]),
@@ -13487,12 +13576,13 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
static_cast<unsigned long long>(Actual[2]),
static_cast<unsigned long long>(Actual[3]),
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
static_cast<unsigned long long>(Stats.walkSamples));
static_cast<unsigned long long>(Stats.walkSamples),
static_cast<unsigned long long>(ExpectedWalkSamples));
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreAdapter] pass schema=1 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=5642/5642"),
TEXT("[VoxelForgeCoreAdapter] pass schema=2 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu"),
static_cast<unsigned long long>(WorkerLimit),
OwnerClipRadius,
OwnerMaxLod,
@@ -13500,7 +13590,9 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
static_cast<unsigned long long>(Actual[0]),
static_cast<unsigned long long>(Actual[1]),
static_cast<unsigned long long>(Actual[2]),
static_cast<unsigned long long>(Actual[3]));
static_cast<unsigned long long>(Actual[3]),
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
static_cast<unsigned long long>(ExpectedWalkSamples));
}
return true;
}
@@ -13515,6 +13607,24 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
*Arguments.SettingsReference);
return false;
}
TStrongObjectPtr<UVoxelSettings> OwnerPlanSettings;
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
UVoxelSettings* ExportSettings = AuthoredSettings;
if (Arguments.bOwnerFourStratePlan)
{
OwnerPlanSettings = TStrongObjectPtr<UVoxelSettings>(
DuplicateObject<UVoxelSettings>(AuthoredSettings, GetTransientPackage()));
if (!OwnerPlanSettings.IsValid())
{
OutError = TEXT("Could not make a transient copy of owner settings for the four-strate plan.");
return false;
}
ExportSettings = OwnerPlanSettings.Get();
if (!ApplyOwnerFourStratePlan(ExportSettings, OwnerPlanDefinitions, false, OutError))
{
return false;
}
}
UVoxelStrateManager* Manager = NewObject<UVoxelStrateManager>(
GetTransientPackage(), NAME_None, RF_Transient);
int32 Seed = AuthoredSettings->Seed;
@@ -13526,7 +13636,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
OriginSpineRadius = Season->GetOriginSpineRadius();
WorldRadiusVoxels = Season->GetWorldRadiusVoxels();
}
if (!Manager->Initialize(AuthoredSettings, Seed))
if (!Manager->Initialize(ExportSettings, Seed))
{
OutError = TEXT("Owner settings did not produce a valid resolved strate layout.");
return false;
@@ -13534,7 +13644,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
Seed = Manager->GetWorldSeed();
UVoxelGenerator* Generator = NewObject<UVoxelGenerator>(
GetTransientPackage(), NAME_None, RF_Transient);
Generator->InitializeSettings(AuthoredSettings);
Generator->InitializeSettings(ExportSettings);
Generator->SetStrateManager(Manager);
TArray<FStandaloneExportSample> Samples;
@@ -13668,25 +13778,25 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export"));
Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm);
Writer->WriteValue(TEXT("seed"), Seed);
Writer->WriteValue(TEXT("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry);
Writer->WriteValue(TEXT("open_surface_entry"), ExportSettings->bOpenSurfaceEntry);
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
Writer->WriteValue(TEXT("world_radius_voxels"), WorldRadiusVoxels);
Writer->WriteValue(TEXT("edge_seal_thickness"), AuthoredSettings->EdgeSealThickness);
Writer->WriteValue(TEXT("edge_seal_thickness"), ExportSettings->EdgeSealThickness);
Writer->WriteValue(TEXT("total_strates"), Manager->GetNumStrates());
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), AuthoredSettings->InterStrateGapChunks);
Writer->WriteValue(TEXT("clip_radius"), AuthoredSettings->ClipRadius);
Writer->WriteValue(TEXT("max_clip_level"), AuthoredSettings->MaxClipLevel);
Writer->WriteValue(TEXT("full_res_clip_levels"), AuthoredSettings->FullResClipLevels);
Writer->WriteValue(TEXT("coarse_tile_cells"), AuthoredSettings->CoarseTileCells);
Writer->WriteValue(TEXT("render_distance_chunks"), AuthoredSettings->RenderDistanceChunks);
Writer->WriteValue(TEXT("generate_skirts"), AuthoredSettings->bGenerateSkirts);
Writer->WriteValue(TEXT("skirt_cells"), AuthoredSettings->SkirtCells);
Writer->WriteValue(TEXT("lod_octave_drop"), AuthoredSettings->LODOctaveDrop);
Writer->WriteValue(TEXT("strate_content_cut_min_level"), AuthoredSettings->GetEffectiveStrateContentCutMinLevel());
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), ExportSettings->InterStrateGapChunks);
Writer->WriteValue(TEXT("clip_radius"), ExportSettings->ClipRadius);
Writer->WriteValue(TEXT("max_clip_level"), ExportSettings->MaxClipLevel);
Writer->WriteValue(TEXT("full_res_clip_levels"), ExportSettings->FullResClipLevels);
Writer->WriteValue(TEXT("coarse_tile_cells"), ExportSettings->CoarseTileCells);
Writer->WriteValue(TEXT("render_distance_chunks"), ExportSettings->RenderDistanceChunks);
Writer->WriteValue(TEXT("generate_skirts"), ExportSettings->bGenerateSkirts);
Writer->WriteValue(TEXT("skirt_cells"), ExportSettings->SkirtCells);
Writer->WriteValue(TEXT("lod_octave_drop"), ExportSettings->LODOctaveDrop);
Writer->WriteValue(TEXT("strate_content_cut_min_level"), ExportSettings->GetEffectiveStrateContentCutMinLevel());
TSet<const UObject*> ActiveObjects;
StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"),
StandaloneSerializeStruct(AuthoredSettings->GetClass(), AuthoredSettings,
ActiveObjects, AuthoredSettings));
StandaloneSerializeStruct(ExportSettings->GetClass(), ExportSettings,
ActiveObjects, ExportSettings));
Writer->WriteArrayStart(TEXT("strates"));
for (const FStrateSlot& Slot : Manager->GetLayout())
@@ -13756,6 +13866,68 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
Writer->WriteObjectEnd();
Writer->Close();
// Export the exact Core manifest and resolved layout that the Unreal adapter just
// generated from these live settings and slots. The standalone research tools consume
// these snapshots instead of maintaining a second copy of the planning rules.
FString RuntimeSnapshotJson;
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
ExportSettings, Manager, 3, 8, StandaloneCoreVoxelSizeCm,
RuntimeSnapshotJson, OutError))
{
return false;
}
FTCHARToUTF8 RuntimeSnapshotUtf8(*RuntimeSnapshotJson);
std::string CoreSnapshotError;
const std::optional<VoxelForgeCore::ResolvedWorldSnapshot> CoreSnapshot =
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
std::string_view(RuntimeSnapshotUtf8.Get(),
static_cast<std::size_t>(RuntimeSnapshotUtf8.Length())), CoreSnapshotError);
if (!CoreSnapshot || CoreSnapshot->worldAssembly() == nullptr
|| CoreSnapshot->resolvedLayout() == nullptr)
{
OutError = FString::Printf(
TEXT("Core adapter export did not produce a complete manifest and resolved layout: %s"),
UTF8_TO_TCHAR(CoreSnapshotError.c_str()));
return false;
}
const std::string CoreAssemblyText = CoreSnapshot->worldAssembly()->ToJson();
const std::string CoreLayoutText = CoreSnapshot->resolvedLayout()->ToJson();
if (CoreAssemblyText.empty() || CoreLayoutText.empty())
{
OutError = TEXT("Core adapter export produced an empty canonical manifest or layout.");
return false;
}
const FString CanonicalAssemblyJson = UTF8_TO_TCHAR(CoreAssemblyText.c_str());
const FString CanonicalLayoutJson = UTF8_TO_TCHAR(CoreLayoutText.c_str());
TSharedPtr<FJsonObject> ExportRoot;
const TSharedRef<TJsonReader<>> ExportReader = TJsonReaderFactory<>::Create(Json);
if (!FJsonSerializer::Deserialize(ExportReader, ExportRoot) || !ExportRoot.IsValid())
{
OutError = TEXT("Could not reopen the owner export to attach the Core plan snapshots.");
return false;
}
TSharedPtr<FJsonObject> AssemblyObject;
const TSharedRef<TJsonReader<>> AssemblyReader =
TJsonReaderFactory<>::Create(CanonicalAssemblyJson);
if (!FJsonSerializer::Deserialize(AssemblyReader, AssemblyObject) || !AssemblyObject.IsValid())
{
OutError = TEXT("Could not parse the Core adapter's canonical manifest for export.");
return false;
}
ExportRoot->SetObjectField(TEXT("world_assembly"), AssemblyObject.ToSharedRef());
ExportRoot->SetStringField(TEXT("core_world_assembly_json"), CanonicalAssemblyJson);
ExportRoot->SetStringField(TEXT("resolved_layout_json"), CanonicalLayoutJson);
FString ExportedJson;
const TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> ExportWriter =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&ExportedJson);
if (!FJsonSerializer::Serialize(ExportRoot.ToSharedRef(), ExportWriter))
{
OutError = TEXT("Could not serialize the owner export with its canonical Core plan.");
return false;
}
Json = MoveTemp(ExportedJson);
const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json"));
if (!FFileHelper::SaveStringToFile(
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
@@ -13764,11 +13936,16 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
return false;
}
if (OutJson) *OutJson = Json;
if (!VerifyStandaloneCoreAdapter(Json, OutError))
if (!Arguments.bSkipStandaloneCoreAdapterParity && !VerifyStandaloneCoreAdapter(Json, OutError))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
return false;
}
if (Arguments.bSkipStandaloneCoreAdapterParity)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreAdapter] skipped legacy W1/W8/W16 fixture parity for this export-only update."));
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
Seed, StandaloneCoreVoxelSizeCm, Manager->GetNumStrates(), Manager->GetPassages().Num(),
@@ -14808,7 +14985,7 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
UVoxelSettings* Source, int32 Seed,
TStrongObjectPtr<UVoxelSettings>& OutSettings,
TStrongObjectPtr<UVoxelStrateDefinition>& OutSurfaceDefinition,
FString& OutError)
FString& OutError, bool bCompactTracePlan = false)
{
if (Source == nullptr)
{
@@ -14824,11 +15001,10 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
nullptr, TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"));
if (SurfaceDefinition == nullptr
|| SurfaceDefinition->GeneratorType != ECaveGeneratorType::SurfaceWorld
|| SurfaceDefinition->StrateHeightInChunks <= 0
|| Source->TotalStrates != 1)
|| SurfaceDefinition->StrateHeightInChunks <= 0)
{
OutError = FString::Printf(
TEXT("The staged owner assets do not describe the logged single SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."),
TEXT("The staged owner assets do not contain a usable DA_Strate1 SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."),
SurfaceDefinition != nullptr ? 1 : 0,
SurfaceDefinition != nullptr ? static_cast<int32>(SurfaceDefinition->GeneratorType) : INDEX_NONE,
SurfaceDefinition != nullptr ? SurfaceDefinition->StrateHeightInChunks : INDEX_NONE,
@@ -14843,9 +15019,24 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
return false;
}
OutSettings = TStrongObjectPtr<UVoxelSettings>(SettingsCopy);
if (bCompactTracePlan)
{
SurfaceDefinition = DuplicateObject<UVoxelStrateDefinition>(SurfaceDefinition,
GetTransientPackage(), TEXT("VoxelForgeCompactSurfaceTraceDefinition"));
if (SurfaceDefinition == nullptr)
{
OutError = TEXT("Could not make a transient DA_Strate1 copy for the compact common-plan trace.");
return false;
}
// L14's historical fixed-length SurfaceWorld route search cannot place the
// new trace fixture even at 16 chunks. Use the same simple, walkable FlatPlain
// plan in both host revisions so the streaming comparison reaches AVoxelWorld.
SurfaceDefinition->GeneratorType = ECaveGeneratorType::FlatPlain;
SurfaceDefinition->StrateHeightInChunks = 16;
}
OutSurfaceDefinition = TStrongObjectPtr<UVoxelStrateDefinition>(SurfaceDefinition);
SettingsCopy->Seed = Seed;
SettingsCopy->TotalStrates = Source->TotalStrates;
SettingsCopy->TotalStrates = 1;
SettingsCopy->StratePool.Reset();
SettingsCopy->FixedStrates.Reset();
SettingsCopy->FixedStrates.Add(0,
@@ -14855,8 +15046,10 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
SettingsCopy->bUseWorldAssemblyManifestOverride = false;
const int32 BottomChunkZ = 1 - SurfaceDefinition->StrateHeightInChunks;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 seed=%d strata=1 bounds=[0,%d] height_chunks=%d gap=%d imported_override=cleared"),
Seed, BottomChunkZ, SurfaceDefinition->StrateHeightInChunks, SettingsCopy->InterStrateGapChunks);
TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 generator=%d seed=%d strata=1 bounds=[0,%d] height_chunks=%d compact_trace=%d gap=%d imported_override=cleared"),
static_cast<int32>(SurfaceDefinition->GeneratorType), Seed, BottomChunkZ,
SurfaceDefinition->StrateHeightInChunks,
bCompactTracePlan ? 1 : 0, SettingsCopy->InterStrateGapChunks);
return true;
}
@@ -16432,7 +16625,15 @@ bool RunCoreFloatingIslandActorIntegrationTest(
}
float StagedWalkSpeedCmPerSecond = 0.0f;
float StagedSprintSpeedCmPerSecond = 0.0f;
if (!ReadStagedPlayerSpeeds(
if (Arguments.bCoreWorldStreamTraceOnly)
{
StagedWalkSpeedCmPerSecond = 600.0f;
StagedSprintSpeedCmPerSecond = 6000.0f;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgePawnSpeedFixture] source=trace_fixture_defaults walk_cm_s=%.3f sprint_cm_s=%.3f"),
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond);
}
else if (!ReadStagedPlayerSpeeds(
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
{
return false;
@@ -17654,7 +17855,15 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
float StagedWalkSpeedCmPerSecond = 0.0f;
float StagedSprintSpeedCmPerSecond = 0.0f;
if (!ReadStagedPlayerSpeeds(
if (Arguments.bCoreWorldStreamTraceOnly)
{
StagedWalkSpeedCmPerSecond = 600.0f;
StagedSprintSpeedCmPerSecond = 6000.0f;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgePawnSpeedFixture] source=trace_fixture_defaults walk_cm_s=%.3f sprint_cm_s=%.3f"),
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond);
}
else if (!ReadStagedPlayerSpeeds(
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
{
return false;
@@ -17663,8 +17872,26 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings);
TStrongObjectPtr<UVoxelSettings> OwnerSurfaceSettings;
TStrongObjectPtr<UVoxelStrateDefinition> OwnerSurfaceDefinition;
if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed,
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError))
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
if (Arguments.bOwnerFourStratePlan)
{
UVoxelSettings* PlanSettings = DuplicateObject<UVoxelSettings>(
AuthoredSettings, GetTransientPackage(), TEXT("VoxelForgeOwnerFourStrateTraceSettings"));
if (PlanSettings == nullptr)
{
OutError = TEXT("Could not duplicate owner settings for the transient four-strate Core trace.");
return false;
}
PlanSettings->Seed = OwnerSeed;
if (!ApplyOwnerFourStratePlan(PlanSettings, OwnerPlanDefinitions, true, OutError))
{
return false;
}
OwnerSurfaceSettings = TStrongObjectPtr<UVoxelSettings>(PlanSettings);
}
else if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed,
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError,
Arguments.bCoreWorldStreamTraceOnly))
{
return false;
}
@@ -17822,6 +18049,25 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
return false;
}
FVector StartupTraceSpawnCm = FVector::ZeroVector;
FString StartupTraceSpawnText;
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceSpawnCm="),
StartupTraceSpawnText, false))
{
TArray<FString> Components;
StartupTraceSpawnText.ParseIntoArray(Components, TEXT(","), true);
if (Components.Num() != 3
|| !LexTryParseString(StartupTraceSpawnCm.X, *Components[0])
|| !LexTryParseString(StartupTraceSpawnCm.Y, *Components[1])
|| !LexTryParseString(StartupTraceSpawnCm.Z, *Components[2])
|| !FMath::IsFinite(StartupTraceSpawnCm.X)
|| !FMath::IsFinite(StartupTraceSpawnCm.Y)
|| !FMath::IsFinite(StartupTraceSpawnCm.Z))
{
OutError = TEXT("voxel.StartupTraceSpawnCm must be finite X,Y,Z centimetres.");
return false;
}
}
USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient);
if (PawnRoot == nullptr)
{
@@ -17830,7 +18076,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
}
PlayerPawn->SetRootComponent(PawnRoot);
PawnRoot->RegisterComponent();
PlayerPawn->SetActorLocation(FVector::ZeroVector, false, nullptr, ETeleportType::TeleportPhysics);
PlayerPawn->SetActorLocation(StartupTraceSpawnCm, false, nullptr, ETeleportType::TeleportPhysics);
PlayerController->Possess(PlayerPawn);
if (TestWorld->GetFirstPlayerController() != PlayerController
|| PlayerController->GetPawn() != PlayerPawn)
@@ -17859,7 +18105,10 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1);
Actor->CoreClipRadius = 3;
Actor->CoreMaxClipLevel = 8;
Actor->bCaptureVoxelForgeCoreHashesForValidation = true;
// A timing trace must use the production tile path. Core density hashing disables the
// native heightfield band shortcut and measures a different workload; hash validation
// remains enabled in the dedicated parity tests below.
Actor->bCaptureVoxelForgeCoreHashesForValidation = false;
Actor->FinishSpawning(ActorTransform);
TestWorld->InitializeActorsForPlay(FURL());
TestWorld->BeginPlay();
@@ -17869,6 +18118,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
return false;
}
Actor->SetActorTickEnabled(true);
VoxelForgeStartupTrace::RecordEvent(TEXT("trace_spawn_location"), FString::Printf(
TEXT("\"location_cm\":[%s,%s,%s]"),
*FString::SanitizeFloat(StartupTraceSpawnCm.X),
*FString::SanitizeFloat(StartupTraceSpawnCm.Y),
*FString::SanitizeFloat(StartupTraceSpawnCm.Z)));
auto DesiredTilesLoaded = [](const AVoxelWorld* World)
{
@@ -18023,6 +18277,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
const double CarveBrushRadiusCoreVoxels =
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
FIntVector ColdStartCollisionCenter = FIntVector::ZeroValue;
uint64 ExpectedEditDigest = 0;
std::array<std::uint64_t, 4> InitialAggregates{};
std::array<std::uint64_t, 4> TravelAggregates{};
@@ -18208,10 +18463,17 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
BiomeProbeSpanVoxels);
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({});
// Match the actor's cold-start fallback camera when the transient world has no pawn.
// Aligner la reference sur la camera de repli de l'acteur sans pawn au demarrage.
const VoxelForgeCore::WorldConfiguration& ReferenceConfiguration =
ReferenceWorld.configuration();
const VoxelForgeCore::Vec3d InitialCameraVoxels = ReferenceConfiguration.viewerStartFound
? ReferenceConfiguration.viewerStartVoxels : VoxelForgeCore::Vec3d{};
const std::vector<VoxelForgeCore::TileKey> Desired =
ReferenceWorld.desiredTiles(InitialCameraVoxels);
if (Desired.empty())
{
OutError = TEXT("Standalone Core returned an empty reference clipmap at the origin.");
OutError = TEXT("Standalone Core returned an empty reference clipmap at the cold-start camera.");
return false;
}
ExpectedInitialTileCount = Desired.size();
@@ -18258,7 +18520,23 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
ReferenceWorld.retainTileRegions(Desired);
// Match the runtime collision probe around the same cold-start viewer tile.
// Reproduire la sonde de collision autour de la meme tuile de depart a froid.
ColdStartCollisionCenter = FIntVector(
FMath::FloorToInt(InitialCameraVoxels.x / 32.0),
FMath::FloorToInt(InitialCameraVoxels.y / 32.0),
FMath::FloorToInt(InitialCameraVoxels.z / 32.0));
std::vector<VoxelForgeCore::TileKey> ColdStartCollisionTiles;
ColdStartCollisionTiles.reserve(static_cast<std::size_t>(2 * CollisionRadius + 1)
* static_cast<std::size_t>(2 * CollisionRadius + 1)
* static_cast<std::size_t>(2 * CollisionRadius + 1));
for (int32 Z = -CollisionRadius; Z <= CollisionRadius; ++Z)
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
ColdStartCollisionTiles.push_back({
{ColdStartCollisionCenter.X + X,
ColdStartCollisionCenter.Y + Y,
ColdStartCollisionCenter.Z + Z}, 0});
if (Arguments.bCoreHybridCliffOnly)
{
@@ -18332,6 +18610,47 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
else
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
// Keep collision hashes outside ExpectedInitialDesiredSet; the collision cube is an
// additional runtime request even where it overlaps the visual clipmap.
// Garder les hachages de collision hors de l'ensemble visuel initial, meme s'ils se
// recoupent avec le clipmap.
std::vector<VoxelForgeCore::TileKey> RetainedReferenceTiles = Desired;
RetainedReferenceTiles.insert(RetainedReferenceTiles.end(),
ColdStartCollisionTiles.begin(), ColdStartCollisionTiles.end());
ReferenceWorld.retainTileRegions(RetainedReferenceTiles);
if (!ReferenceWorld.compileTileRegion(ColdStartCollisionTiles, 16, Scheduler, CoreError))
{
OutError = FString::Printf(TEXT("Standalone collision reference compile failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
std::size_t CollisionBodyTiles = 0;
for (const VoxelForgeCore::TileKey& Key : ColdStartCollisionTiles)
{
const std::optional<VoxelForgeCore::TileMesh> Mesh =
ReferenceWorld.generateTile(Key, Options, CoreError);
if (!Mesh)
{
OutError = FString::Printf(TEXT("Standalone collision tile generation failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
FVoxelForgeCoreTileHashRecord Record;
Record.bEmpty = Mesh->indices.empty();
Record.bHasHashes = Mesh->hasDensityHash && Mesh->hasMeshHash
&& Mesh->hasPayloadHash && Mesh->hasTopologyHash;
if (!Record.bEmpty) ++CollisionBodyTiles;
ExpectedHashes.Add(FVoxelTileKey(
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod), Record);
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] standalone_collision_reference=1 center=(%d,%d,%d) tiles=%d body=%llu no_body=%llu"),
ColdStartCollisionCenter.X, ColdStartCollisionCenter.Y, ColdStartCollisionCenter.Z,
static_cast<int32>(ColdStartCollisionTiles.size()),
static_cast<unsigned long long>(CollisionBodyTiles),
static_cast<unsigned long long>(ColdStartCollisionTiles.size() - CollisionBodyTiles));
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
AggregateRows.reserve(Desired.size());
bool bFoundCarveVertex = false;
@@ -19367,7 +19686,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
{
const FVoxelTileKey Key(FIntVector(X, Y, Z), 0);
const FVoxelTileKey Key(ColdStartCollisionCenter + FIntVector(X, Y, Z), 0);
ExpectedCollisionSet.Add(Key);
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
if (Record == nullptr)
@@ -21361,6 +21680,44 @@ int32 RunBatch(const FString& Params)
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
{
if (FParse::Param(*Params, TEXT("coreassetupgradeaudit")))
{
// Load the untouched BuildHost copies and emit their in-memory conversions; never save a package.
// Charge les copies BuildHost intactes et affiche les conversions en memoire, sans sauver de package.
UVoxelSettings* SettingsAsset = LoadObject<UVoxelSettings>(nullptr,
TEXT("/Game/VoxelForge/DA_Settings.DA_Settings"));
if (SettingsAsset == nullptr)
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing DA_Settings copy"));
return 2;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_gap_chunks=%d new_gap_m=%.6f layout_chunk_m=%.6f"),
*SettingsAsset->GetPathName(), SettingsAsset->InterStrateGapChunks,
SettingsAsset->InterStrateGapMeters, SettingsAsset->StrateLayoutChunkSizeMeters);
const TCHAR* StratePaths[] = {
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
};
for (const TCHAR* Path : StratePaths)
{
UVoxelStrateDefinition* Definition = LoadObject<UVoxelStrateDefinition>(nullptr, Path);
if (Definition == nullptr)
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing strate copy %s"), Path);
return 2;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_height_chunks=%d new_height_m=%.6f old_transition_chunks=%d new_transition_m=%.6f"),
*Definition->GetPathName(), Definition->StrateHeightInChunks,
Definition->StrateHeightMeters, Definition->TransitionBlendChunks,
Definition->TransitionWidthMeters);
}
return 0;
}
if (FParse::Param(*Params, TEXT("step2f")))
{
return RunStep2f(Params);
+31 -3
View File
@@ -13,6 +13,8 @@ COMPILER="clang++-19"
VARIANT="clang19"
FP_CONTRACT="off"
JOBS=12
SANITIZERS=""
EXCLUDE_UNSIGNED_OVERFLOW=0
while (($#)); do
case "$1" in
@@ -20,6 +22,8 @@ while (($#)); do
--variant=*) VARIANT="${1#*=}" ;;
--fp-contract=*) FP_CONTRACT="${1#*=}" ;;
--jobs=*) JOBS="${1#*=}" ;;
--sanitize=*) SANITIZERS="${1#*=}" ;;
--exclude-unsigned-integer-overflow) EXCLUDE_UNSIGNED_OVERFLOW=1 ;;
*) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;;
esac
shift
@@ -60,14 +64,32 @@ INCLUDES=(
"-I$ROOT/Source/VoxelForge/Public"
"-I$ROOT/Source/VoxelForge/Private"
)
OPT_LEVEL=-O2
LINK_FLAGS=()
if [[ -n "$SANITIZERS" ]]; then
OPT_LEVEL=-O1
LINK_FLAGS+=("-fsanitize=$SANITIZERS" -fno-sanitize-recover=undefined)
if ((EXCLUDE_UNSIGNED_OVERFLOW)); then
LINK_FLAGS+=(-fno-sanitize=unsigned-integer-overflow)
fi
fi
FLAGS=(
-std=c++20 -O2 -mavx2 -mfma -fno-fast-math
-std=c++20 "$OPT_LEVEL" -mavx2 -mfma -fno-fast-math
-fexceptions -pthread "${INCLUDES[@]}"
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\""
)
FLAGS+=("${LINK_FLAGS[@]}")
if [[ "$FP_CONTRACT" != "default" ]]; then
FLAGS+=("-ffp-contract=$FP_CONTRACT")
fi
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
@@ -124,6 +146,11 @@ 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=()
@@ -131,7 +158,7 @@ PIDS=()
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT"
"$COMPILER" -pthread "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
"$COMPILER" -pthread "${LINK_FLAGS[@]}" "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT"
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" -I"$ROOT/Source" \
@@ -145,4 +172,5 @@ if [[ "$POST_BUILD_STAMP" != "$SOURCE_STAMP" ]]; then
fi
printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT"
printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_OUTPUT"
printf 'Compiler: %s | variant: %s | fp-contract: %s | jobs: %s\n' "$COMPILER" "$VARIANT" "$FP_CONTRACT" "$JOBS"
printf 'Compiler: %s | variant: %s | fp-contract: %s | sanitizers: %s | jobs: %s\n' \
"$COMPILER" "$VARIANT" "$FP_CONTRACT" "${SANITIZERS:-off}" "$JOBS"
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <AssetRegistry/AssetData.h>
#endif
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <AssetRegistry/AssetRegistryModule.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Async/ParallelFor.h>
#endif
+9
View File
@@ -1,5 +1,10 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <CoreMinimal.h>
#else
// Deliberately small UE-style surface used by the copied VoxelForge CPU sources.
// This is a value/container compatibility layer; it does not start or link Unreal.
#include <algorithm>
@@ -45,6 +50,9 @@
#define GENERATED_UCLASS_BODY()
#define VOXELFORGE_API
#define FORCEINLINE inline
#ifdef WITH_EDITOR
#undef WITH_EDITOR
#endif
#define WITH_EDITOR 0
#define TRACE_CPUPROFILER_EVENT_SCOPE(...)
#define PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS
@@ -705,3 +713,4 @@ struct FParse
struct FCrc { static uint32 MemCrc32(const void*Data,int32 Length,uint32 Crc=0){const auto*P=static_cast<const uint8*>(Data);uint32 V=~Crc;for(int32 I=0;I<Length;++I){V^=P[I];for(int J=0;J<8;++J)V=(V>>1)^(0xEDB88320u&static_cast<uint32>(-static_cast<int32>(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast<int32>(std::strlen(S)),Crc);} };
template<class T> using TAtomic = std::atomic<T>;
class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomic<int32>Value{0};};
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Dom/JsonObject.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Dom/JsonValue.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Engine/DataAsset.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <GameplayTagContainer.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/CriticalSection.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/Event.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/FileManager.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/IConsoleManager.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/PlatformProcess.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/PlatformTime.h>
#endif
+6
View File
@@ -1,5 +1,10 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Json.h>
#else
#include <charconv>
#include <cmath>
#include <cstddef>
@@ -162,3 +167,4 @@ namespace VFJson
return Parser(std::move(Text)).Parse(Out, Error);
}
}
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Math/RandomStream.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/CommandLine.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/Crc.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/FileHelper.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/Parse.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/Paths.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/ScopeLock.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/ScopeRWLock.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/SecureHash.h>
#endif
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Modules/ModuleManager.h>
#endif
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <ProfilingDebugging/CpuProfilerTrace.h>
#endif
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Serialization/CustomVersion.h>
#endif
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Serialization/JsonSerializer.h>
#endif
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Serialization/JsonWriter.h>
#endif
+6
View File
@@ -1,4 +1,9 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Stats/Stats.h>
#else
#define STATCAT_Advanced 0
#define DECLARE_STATS_GROUP(Name,Group,Category) constexpr int32 Group=0
#define DECLARE_DWORD_COUNTER_STAT_EXTERN(Name,Stat,Group,Api) constexpr int32 Stat=0
@@ -8,3 +13,4 @@
#define SET_DWORD_STAT(Stat,Value) ((void)0)
#define SCOPE_CYCLE_COUNTER(Stat) ((void)0)
#define QUICK_SCOPE_CYCLE_COUNTER(Stat) ((void)0)
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Templates/SubclassOf.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Templates/UniquePtr.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <UObject/EnumProperty.h>
#endif
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <UObject/FieldIterator.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <UObject/Object.h>
#endif
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <UObject/UObjectGlobals.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <UObject/UnrealType.h>
#endif
@@ -3,6 +3,7 @@
#include "VoxelForgeCore/CpuCycleCounter.h"
#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
@@ -26,9 +27,12 @@ namespace VoxelForgeCore::floating_island_detail
static constexpr std::uint64_t SampleMask=255;
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> sampledFieldCalls{};
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> fieldCycles{};
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> fieldNanoseconds{};
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> sampledNativeCalls{};
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> nativeCycles{};
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> nativeNanoseconds{};
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> exclusiveCycles{};
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> exclusiveNanoseconds{};
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> scopeSamples{};
std::uint64_t traceSampledMisses=0,traceSeenBefore=0,traceEvictions=0,
traceReusedEvictions=0;
@@ -118,7 +122,11 @@ namespace VoxelForgeCore::floating_island_detail
&FIslandFieldAttributionInstrumentation::SampleMask;
sampled=(((calls+phase)&FIslandFieldAttributionInstrumentation::SampleMask)==0);
}
if(sampled)start=VoxelForgeCoreShared::ReadCpuCycles();
if(sampled)
{
start=VoxelForgeCoreShared::ReadCpuCycles();
wallStart=std::chrono::steady_clock::now();
}
}
~FScopedIslandAttributionTimer(){Stop();}
std::uint64_t Stop() noexcept
@@ -128,12 +136,18 @@ namespace VoxelForgeCore::floating_island_detail
const std::uint64_t raw=VoxelForgeCoreShared::ReadCpuCycles()-start;
const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead();
const std::uint64_t cycles=raw>overhead?raw-overhead:0;
const auto elapsed=std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now()-wallStart).count();
metrics->exclusiveCycles[bucketIndex]+=cycles;
metrics->exclusiveNanoseconds[bucketIndex]+=elapsed>0
?static_cast<std::uint64_t>(elapsed):0;
++metrics->scopeSamples[bucketIndex];
if(bucketIndex==static_cast<std::size_t>(EIslandAttributionBucket::Field))
{
const std::size_t callerIndex=static_cast<std::size_t>(ActiveIslandFieldCaller());
metrics->fieldCycles[callerIndex]+=cycles;
metrics->fieldNanoseconds[callerIndex]+=elapsed>0
?static_cast<std::uint64_t>(elapsed):0;
++metrics->sampledFieldCalls[callerIndex];
}
return cycles;
@@ -141,6 +155,7 @@ namespace VoxelForgeCore::floating_island_detail
FIslandFieldAttributionInstrumentation* metrics=nullptr;
std::size_t bucketIndex=0;
std::uint64_t start=0;
std::chrono::steady_clock::time_point wallStart{};
bool sampled=false;
};
@@ -160,6 +175,7 @@ namespace VoxelForgeCore::floating_island_detail
sampledCaller=caller;
sampled=true;
start=VoxelForgeCoreShared::ReadCpuCycles();
wallStart=std::chrono::steady_clock::now();
++attribution->sampledNativeCalls[callerIndex];
}
~FScopedIslandAttributionSample()
@@ -168,12 +184,17 @@ namespace VoxelForgeCore::floating_island_detail
const std::uint64_t raw=VoxelForgeCoreShared::ReadCpuCycles()-start;
const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead();
const std::uint64_t duration=raw>overhead?raw-overhead:0;
const auto elapsed=std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now()-wallStart).count();
const std::size_t callerIndex=static_cast<std::size_t>(sampledCaller);
sampledAttribution->nativeCycles[callerIndex]+=duration;
sampledAttribution->nativeNanoseconds[callerIndex]+=elapsed>0
?static_cast<std::uint64_t>(elapsed):0;
}
FIslandFieldAttributionInstrumentation* sampledAttribution=nullptr;
EIslandFieldCaller sampledCaller=EIslandFieldCaller::Other;
std::uint64_t start=0;
std::chrono::steady_clock::time_point wallStart{};
bool sampled=false;
};
@@ -0,0 +1,159 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace VoxelForgeCore::Linux77Profiler
{
enum class ERoute : std::uint8_t
{
Dispatch=0,PackedScalar=1,PackedX8=2,AnalyticScalar=3,
AnalyticBatch=4,Native=5,LegacySurface=6,SharedSource=7,Count=8
};
enum class EStage : std::uint8_t
{
Other=0,GridFill=1,MarchingCubes=2,Skirt=3,Refine=4,Count=5
};
enum class EPhase : std::uint8_t
{
Dispatch=0,BaseField=1,CompositionGraph=2,Bands=3,Passage=4,
Support=5,Caps=6,Stamps=7,Edit=8,Other=9,Count=10
};
inline constexpr std::size_t RouteCount=static_cast<std::size_t>(ERoute::Count);
inline constexpr std::size_t StageCount=static_cast<std::size_t>(EStage::Count);
inline constexpr std::size_t PhaseCount=static_cast<std::size_t>(EPhase::Count);
inline thread_local bool ThreadContextActive=false;
struct FThreadContext
{
std::uint8_t lod=0xff;
ERoute route=ERoute::Count;
EStage stage=EStage::Other;
EPhase phase=EPhase::Other;
bool halo=false;
};
#if defined(VF_L77_PROFILE)
bool IsEnabled() noexcept;
void SetEnabled(bool enabled) noexcept;
void RegisterThread();
void Start(std::uint32_t periodMicroseconds=1000);
void StopAndPrint();
inline thread_local FThreadContext CurrentContext{};
struct FScopedRoute
{
explicit FScopedRoute(ERoute value) noexcept
:active(ThreadContextActive),previous(CurrentContext.route)
{if(active)CurrentContext.route=value;}
~FScopedRoute(){if(active)CurrentContext.route=previous;}
bool active=false;ERoute previous=ERoute::Dispatch;
};
struct FScopedStage
{
explicit FScopedStage(EStage value) noexcept
:active(ThreadContextActive),previous(CurrentContext.stage)
{if(active)CurrentContext.stage=value;}
~FScopedStage(){Restore();}
void Restore() noexcept
{if(active){CurrentContext.stage=previous;active=false;}}
bool active=false;EStage previous=EStage::Other;
};
struct FScopedPhase
{
explicit FScopedPhase(EPhase value) noexcept
:active(ThreadContextActive),previous(CurrentContext.phase)
{if(active)CurrentContext.phase=value;}
~FScopedPhase(){if(active)CurrentContext.phase=previous;}
bool active=false;EPhase previous=EPhase::Dispatch;
};
struct FScopedLod
{
explicit FScopedLod(int value) noexcept
:active(IsEnabled()),previous(CurrentContext.lod),previousContextActive(ThreadContextActive)
{
if(active)
{
ThreadContextActive=true;
RegisterThread();
CurrentContext.lod=static_cast<std::uint8_t>(value<0||value>8?0xff:value);
}
}
~FScopedLod()
{if(active){CurrentContext.lod=previous;ThreadContextActive=previousContextActive;}}
bool active=false;std::uint8_t previous=0xff;
bool previousContextActive=false;
};
struct FScopedHalo
{
explicit FScopedHalo(bool value) noexcept
:active(ThreadContextActive),previous(CurrentContext.halo)
{if(active)CurrentContext.halo=value;}
~FScopedHalo(){if(active)CurrentContext.halo=previous;}
bool active=false;bool previous=false;
};
#else
inline bool IsEnabled() noexcept{return false;}
inline void SetEnabled(bool) noexcept{}
inline void RegisterThread(){}
inline void Start(std::uint32_t=1000){}
inline void StopAndPrint(){}
struct FScopedRoute{explicit FScopedRoute(ERoute) noexcept{}};
struct FScopedStage
{explicit FScopedStage(EStage) noexcept{}void Restore() noexcept{}};
struct FScopedPhase{explicit FScopedPhase(EPhase) noexcept{}};
struct FScopedLod{explicit FScopedLod(int) noexcept{}};
struct FScopedHalo{explicit FScopedHalo(bool) noexcept{}};
#endif
struct FDispatchCounter
{
std::uint64_t calls=0;
std::uint64_t points=0;
};
using FDispatchCounters=std::array<std::array<FDispatchCounter,RouteCount>,StageCount>;
inline const char* RouteName(ERoute value) noexcept
{
switch(value)
{
case ERoute::Dispatch:return "dispatch_lookup";
case ERoute::PackedScalar:return "packed_scalar";
case ERoute::PackedX8:return "packed_x8";
case ERoute::AnalyticScalar:return "analytic_scalar";
case ERoute::AnalyticBatch:return "analytic_batch";
case ERoute::Native:return "native";
case ERoute::LegacySurface:return "legacy_surface";
case ERoute::SharedSource:return "shared_source";
default:return "unknown";
}
}
inline const char* StageName(EStage value) noexcept
{
switch(value)
{
case EStage::GridFill:return "grid_fill";
case EStage::MarchingCubes:return "marching_cubes";
case EStage::Skirt:return "skirt";
case EStage::Refine:return "refine";
default:return "other";
}
}
inline const char* PhaseName(EPhase value) noexcept
{
switch(value)
{
case EPhase::Dispatch:return "dispatch_lookup";
case EPhase::BaseField:return "base_field";
case EPhase::CompositionGraph:return "composition_graph";
case EPhase::Bands:return "composition_bands";
case EPhase::Passage:return "passages";
case EPhase::Support:return "support";
case EPhase::Caps:return "caps";
case EPhase::Stamps:return "stamps";
case EPhase::Edit:return "edits";
default:return "other";
}
}
}
@@ -11,6 +11,8 @@
#include <utility>
#include <vector>
#include "VoxelForgeCore/Linux77Profiler.h"
// UBT supplies this export/import macro for the runtime module. The standalone target has no
// module boundary, so leave it empty there. UBT fournit la macro; le viewer autonome reste neutre.
#ifndef VOXELFORGECORE_API
@@ -131,9 +133,57 @@ namespace VoxelForgeCore
std::uint64_t mixedBoxes = 0;
std::uint64_t unknownBoxes = 0;
std::uint64_t fieldCalls = 0;
// Opt-in composer diagnostics. These stay zero during ordinary generation.
bool passageComposerProfiled = false;
std::uint64_t passageTileFeatureCount = 0;
std::uint64_t passageTileRouteSegmentCandidates = 0;
std::uint64_t passageTileMouthSegmentCandidates = 0;
std::uint64_t passageComposerQueries = 0;
std::uint64_t passageIterations = 0;
std::uint64_t passageUpperMouthFilterChecks = 0;
std::uint64_t passageColumnCandidates = 0;
std::uint64_t passageColumnDistanceCalls = 0;
std::uint64_t passageNegativeBoundChecks = 0;
std::uint64_t passageNegativeBoundRejects = 0;
std::uint64_t passagePositiveBoundChecks = 0;
std::uint64_t passagePositiveBoundRejects = 0;
std::uint64_t passageCarveSegmentVisits = 0;
std::uint64_t passageSmoothCarveJoins = 0;
std::uint64_t passageSupportSegmentVisits = 0;
std::uint64_t passageSmoothSupportJoins = 0;
std::uint64_t passageSupportCapsuleChecks = 0;
std::uint64_t passageLaterBoreChecks = 0;
std::uint64_t passageOtherCapsuleChecks = 0;
std::uint64_t passageMouthSupportCalls = 0;
std::uint64_t passageMouthRouteSegmentChecks = 0;
std::uint64_t passageReliefFieldCalls = 0;
std::uint64_t passageSmoothstepEvaluations = 0;
std::uint64_t floatFieldCalls = 0;
std::uint64_t latticeFieldCalls = 0;
std::uint64_t unclampedFieldCalls = 0;
std::uint64_t densityPointCalls = 0;
std::uint64_t densityFloatPointCalls = 0;
std::uint64_t densityLatticePointCalls = 0;
std::uint64_t densityUnclampedPointCalls = 0;
std::uint64_t densityX8PointCalls = 0;
std::uint64_t densityFieldEvalNanoseconds = 0;
std::uint64_t densityFieldEvalSamplePoints = 0;
std::uint64_t legacyFieldEvalCalls = 0;
std::array<std::uint64_t,9> legacyFieldTermCalls{};
std::array<std::uint64_t,9> legacyFieldTermNanoseconds{};
std::array<std::uint64_t,9> legacyFieldTermTimedCalls{};
// Linux77-only mutually-exclusive route counts, flattened as
// (stage * route_count + route) * 2 + {calls,points}.
std::array<std::uint64_t,
VoxelForgeCore::Linux77Profiler::StageCount
*VoxelForgeCore::Linux77Profiler::RouteCount*2> linux77DispatchCounters{};
std::uint64_t linux77GridInteriorSamples = 0;
std::uint64_t linux77GridHaloSamples = 0;
std::uint64_t legacyPrimitiveCandidateVisits = 0;
std::uint64_t legacyPrimitiveDistanceEvaluations = 0;
std::uint64_t legacySmoothMinCalls = 0;
std::uint64_t legacySmoothMinNanoseconds = 0;
std::uint64_t legacySmoothMinTimedCalls = 0;
std::uint64_t columnCacheHits = 0;
std::uint64_t columnCacheMisses = 0;
std::uint64_t columnCacheConflictMisses = 0;
@@ -220,6 +270,9 @@ namespace VoxelForgeCore
std::uint32_t farHorizonVersion = 0;
bool farShellRepairSkipped = false;
bool tileWasClassified = false;
std::uint8_t tileProofVetoReason = 0;
bool tileProofSafe = false;
bool tileLegacyBlockProofRelevant = false;
bool meshingSkipped = false;
bool featureRefinedResolution = false;
bool heightfieldBandLimited = false;
@@ -400,7 +453,10 @@ namespace VoxelForgeCore
// version 169 sank every connector 2 m; it left four FI route support probes low.
// version 170 sinks only connectors touching the anchored root by 1 m.
// Version 208 keeps the crater tread scoped to style-zero FI mouths.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 209;
// Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges.
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 213;
enum class FieldChannel : std::uint32_t
{
@@ -1004,13 +1060,11 @@ namespace VoxelForgeCore
&&stratum.recipeVersion==1;
}
inline constexpr std::uint32_t NativeLatticeFieldRecipeVersion=2;
inline bool IsNativeLatticeFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
{
return (stratum.generatorKind=="Maze"||stratum.generatorKind=="VerticalShafts")
&&stratum.recipeId=="Core.Lattice.PrimitiveField"
&&stratum.recipeVersion==NativeLatticeFieldRecipeVersion;
&&stratum.recipeVersion==1;
}
inline bool HasNativeGraphField(const ResolvedLayoutStratumV2& stratum) noexcept
@@ -1726,6 +1780,9 @@ namespace VoxelForgeCore
std::int32_t bandMinZVoxel = -2147483647 - 1;
std::int32_t bandMaxZVoxel = 2147483647;
bool hashDensity = false;
// Falsifier mode: retain mesh/topology hashes while permitting proven empty tiles
// to skip their density lattice. The density hash of such a skipped tile is empty.
bool allowUniformSkipsWhenHashing = false;
bool captureDensitySamples = false;
// Diagnostic only; materialize the full density lattice even when block proofs
// establish that the tile has no surface.
@@ -1744,9 +1801,13 @@ namespace VoxelForgeCore
// Separate cold pass only; samples the MC inner stages without changing output.
// Passe froide separee seulement; mesure les etapes internes MC sans changer la sortie.
bool profileMesherStages = false;
// Research-only: count passage-composer work per generated tile.
bool profilePassageComposer = false;
// Research-only: collect tile-drain kernel counters and floating-island cache metrics.
// Inactive on ordinary generation so the hot path only pays the option check.
bool instrumentTileDrain = false;
// Linux headless measurement only. Compiled out of ordinary builds.
bool linux77Profile = false;
bool exactMidpointDensityMemo = false;
bool activeLaneBatching = false;
std::uint32_t proofBlockCells = 8;
@@ -1,5 +1,9 @@
#pragma once
// Core implementation TUs opt into the standalone Unreal-shaped compatibility headers. UE's
// Linux-generated module TUs leave this unset so those same header names forward to the engine.
#define VFCORE_USE_COMPAT 1
// Every standard or platform header used by the copied implementation must be included before
// its wrapper opens vfcore::detail. This prevents system declarations from being nested in the
// private namespace when the implementation includes its Unreal-shaped compatibility headers.
+380 -32
View File
@@ -5,6 +5,7 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <iomanip>
#include <limits>
#include <sstream>
@@ -1901,8 +1902,7 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
// The shared JSON value type stores numbers as IEEE-754 doubles. Keep the
// serialized seed in the exact integer range, as the stamp vocabulary does.
room.irregularitySeed=(isNativeLatticeStrate(room.strateIndex)
?LayoutLatticeFeatureSeed(seed,room.strateIndex,
NativeLatticeFieldRecipeVersion,room.stableId)
?LayoutLatticeFeatureSeed(seed,room.strateIndex,1,room.stableId)
:LayoutStampSeed(seed,room.strateIndex,room.stableId))
&0x001fffffffffffffull;
const std::int64_t pad=room.smoothWidthMicrometres
@@ -1919,8 +1919,7 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
{
const bool nativeLattice=isNativeLatticeStrate(edge.strateIndex);
edge.irregularitySeed=(nativeLattice
?LayoutLatticeFeatureSeed(seed,edge.strateIndex,
NativeLatticeFieldRecipeVersion,edge.stableId)
?LayoutLatticeFeatureSeed(seed,edge.strateIndex,1,edge.stableId)
:LayoutStampSeed(seed,edge.strateIndex,edge.stableId))
&0x001fffffffffffffull;
using FCurvePoint=std::array<double,3>;
@@ -2327,8 +2326,22 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
const std::int64_t midRadius=std::max<std::int64_t>(
static_cast<std::int64_t>(std::llround((upperIsSurfaceWorld?3.5:1.5)*qPerMetre)),
cfg?cfg->midRadiusSubvoxels:mouthRadius);
const std::int64_t reachMinimum=cfg?std::max<std::int64_t>(0,cfg->reachMinimumSubvoxels):8*qPerMetre;
const std::int64_t reachMaximum=cfg?std::max<std::int64_t>(reachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre;
const std::int64_t requestedReachMinimum=cfg
?std::max<std::int64_t>(0,cfg->reachMinimumSubvoxels):8*qPerMetre;
const std::int64_t requestedReachMaximum=cfg
?std::max<std::int64_t>(requestedReachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre;
// Keep the landing several metres inside the stratum's authored content.
// Near a band edge, the sampled native terrain can exist at the centerline
// while the full capsule footprint has no supporting ground.
const std::int64_t mouthClearance=std::max(mouthRadius,midRadius)+2*qPerMetre;
const auto usableReach=[&](const ResolvedLayoutStratumV2& stratum)
{
const std::int64_t depth=contentZQ(stratum.content.topMicrometres)
-contentZQ(stratum.content.bottomMicrometres);
return std::max<std::int64_t>(0,depth-mouthClearance);
};
const std::int64_t upperReachLimit=usableReach(upper);
const std::int64_t lowerReachLimit=usableReach(lower);
const std::uint32_t style=cfg?std::min<std::uint32_t>(cfg->style,3u):1u;
const std::uint32_t segments=cfg?std::clamp<std::uint32_t>(cfg->segments,16u,48u):20u;
const std::int64_t wander=cfg?std::max<std::int64_t>(0,cfg->wanderSubvoxels):3*qPerMetre;
@@ -2364,16 +2377,30 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
if(!hasUpperRoute)
{
upperMouth[0]=xy[0];upperMouth[1]=xy[1];
const auto reach=reachValue(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
connection,0x55505248u),reachMinimum,reachMaximum);
const auto requestedReach=reachValue(PassageSeed(seed,upper.strateIndex,
lower.strateIndex,connection,0x55505248u),
requestedReachMinimum,requestedReachMaximum);
const auto reach=std::min(requestedReach,upperReachLimit);
upperMouth[2]=contentZQ(upper.content.bottomMicrometres)+reach;
if(requestedReach>upperReachLimit)
std::printf("[ResolvedPassageReachClamp] id=%s side=upper sampled_m=%.3f usable_max_m=%.3f selected_m=%.3f status=CLAMPED\n",
passage.stableId.c_str(),requestedReach/static_cast<double>(qPerMetre),
upperReachLimit/static_cast<double>(qPerMetre),
reach/static_cast<double>(qPerMetre));
}
if(!hasLowerRoute)
{
lowerMouth[0]=xy[0];lowerMouth[1]=xy[1];
const auto reach=reachValue(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
connection,0x4c4f5752u),reachMinimum,reachMaximum);
const auto requestedReach=reachValue(PassageSeed(seed,upper.strateIndex,
lower.strateIndex,connection,0x4c4f5752u),
requestedReachMinimum,requestedReachMaximum);
const auto reach=std::min(requestedReach,lowerReachLimit);
lowerMouth[2]=contentZQ(lower.content.topMicrometres)-reach;
if(requestedReach>lowerReachLimit)
std::printf("[ResolvedPassageReachClamp] id=%s side=lower sampled_m=%.3f usable_max_m=%.3f selected_m=%.3f status=CLAMPED\n",
passage.stableId.c_str(),requestedReach/static_cast<double>(qPerMetre),
lowerReachLimit/static_cast<double>(qPerMetre),
reach/static_cast<double>(qPerMetre));
}
}
if(!radialWithin(upperMouth,placementRadius)||!radialWithin(lowerMouth,placementRadius))
@@ -2433,18 +2460,29 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
}
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
if(hasUpperRoute)
// Authored tangents and island anchors already determine the phase.
// A generated mouth in a native cave uses its radial approach; an
// island landing uses the tangent fit below to ease off the crater rim.
const bool generatedIslandWormEntry=!hasUpperRoute&&!hasUpperTangent
&&!anchoredSpiral&&upper.generatorKind=="FloatingIslands";
if(hasUpperRoute||(anchoredIslandPassage&&!anchoredSpiral)
||(!hasUpperRoute&&!anchoredIslandPassage&&!anchoredSpiral
&&upper.generatorKind!="FloatingIslands"))
{
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
const std::uint32_t hashedPhase=phase;
const std::int64_t phaseX=style==0&&anchoredIslandPassage
&&!anchoredSpiral&&passageAnchor
?upperMouth[0]-passageAnchor->values[0]
:(hasUpperTangent?upperTangent[0]:upperMouth[0]);
:(hasUpperTangent?upperTangent[0]:upperMouth[0]
-(anchoredIslandPassage&&passageAnchor
?passageAnchor->values[0]:0));
const std::int64_t phaseY=style==0&&anchoredIslandPassage
&&!anchoredSpiral&&passageAnchor
?upperMouth[1]-passageAnchor->values[1]
:(hasUpperTangent?upperTangent[1]:upperMouth[1]);
:(hasUpperTangent?upperTangent[1]:upperMouth[1]
-(anchoredIslandPassage&&passageAnchor
?passageAnchor->values[1]:0));
for(std::uint32_t offset=0;offset<16;++offset)
{
const std::uint32_t candidate=static_cast<std::uint32_t>(
@@ -2464,23 +2502,6 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
{bestAlignment=alignment;phase=candidate;}
}
}
if(hasUpperRoute&&upperIsSurfaceWorld)
{
const auto& directionAtPhase=directions[phase];
const std::int64_t radialAlignment=upperMouth[0]*directionAtPhase[0]
+upperMouth[1]*directionAtPhase[1];
if(radialAlignment<0)
{
std::int64_t bestOutwardAlignment=std::numeric_limits<std::int64_t>::min();
for(std::uint32_t candidate=0;candidate<directions.size();++candidate)
{
const std::int64_t alignment=upperMouth[0]*directions[candidate][0]
+upperMouth[1]*directions[candidate][1];
if(alignment>bestOutwardAlignment)
{bestOutwardAlignment=alignment;phase=candidate;}
}
}
}
bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
connection,0x44495245u)&1u)!=0;
if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor)
@@ -2502,7 +2523,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
clockwise=clockwiseTangentX*upperTangent[0]
+clockwiseTangentY*upperTangent[1]>=0.0;
}
const int direction=clockwise?1:-1;
int direction=clockwise?1:-1;
constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree helix contract.
const std::int64_t wormRadialWobble=std::min<std::int64_t>(
std::max<std::int64_t>(0,wander/3),2*qPerMetre);
@@ -2571,6 +2592,267 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
static_cast<std::int64_t>(std::llround(centerX+radius*directions[directionIndex][0]/65536.0)),
static_cast<std::int64_t>(std::llround(centerY+radius*directions[directionIndex][1]/65536.0))};
};
if(generatedIslandWormEntry)
{
struct FPhaseScore
{
std::uint32_t phase=0;
int direction=1;
std::uint64_t routeLength=0;
double clearance=0.0;
double tangent=0.0;
};
std::array<FPhaseScore,32> phaseScores{};
std::size_t phaseScoreCount=0;
const std::uint32_t hashedPhase=phase;
const int hashedDirection=direction;
const std::int64_t entryVerticalDistance=std::abs(
lowerMouth[2]-upperMouth[2]);
const std::int64_t entryRouteGradeQ16=profiledSurfaceJoin?8000
:style==0&&anchoredIslandPassage&&!anchoredSpiral?15500
:TargetGradeQ16;
const auto candidateRouteLength=[&](std::uint32_t candidate,int candidateDirection)
{
std::vector<std::array<std::int64_t,2>> candidateXY;
const auto append=[&](std::array<std::int64_t,2> point)
{if(candidateXY.empty()||candidateXY.back()!=point)
candidateXY.push_back(point);};
append({upperMouth[0],upperMouth[1]});
append(ringPoint(candidate));
const auto measure=[&]()
{
std::uint64_t length=0;
for(std::size_t i=1;i<candidateXY.size();++i)
length+=distanceQ(candidateXY[i][0]-candidateXY[i-1][0],
candidateXY[i][1]-candidateXY[i-1][1]);
length+=distanceQ(lowerMouth[0]-candidateXY.back()[0],
lowerMouth[1]-candidateXY.back()[1]);
return length;
};
std::uint32_t loops=0;
std::uint64_t length=0;
while(loops<256)
{
length=measure();
if(length*static_cast<std::uint64_t>(entryRouteGradeQ16)
>=static_cast<std::uint64_t>(entryVerticalDistance)*65536ull)break;
for(int step=1;step<=16;++step)
{
int directionIndex=static_cast<int>(candidate)
+candidateDirection*step;
directionIndex=(directionIndex%16+16)%16;
append(ringPoint(static_cast<std::uint32_t>(directionIndex),
loops+(step==16?1u:0u)));
}
++loops;
}
if(loops>=256)return std::uint64_t{0};
const std::int64_t mouthPlanSeparation=distanceQ(
lowerMouth[0]-upperMouth[0],lowerMouth[1]-upperMouth[1]);
if(!hasLowerTangent&&!anchoredSpiral&&pathStyle==1u&&loops<=1
&&mouthPlanSeparation<=2*mouthRadius
&&entryVerticalDistance<=2*mouthRadius+qPerMetre/2)
{
double bestExitScore=-std::numeric_limits<double>::infinity();
std::uint32_t lowerPhase=candidate;
const auto firstRing=ringPoint(candidate);
for(std::uint32_t exitCandidate=0;exitCandidate<16;++exitCandidate)
{
if(exitCandidate==candidate)continue;
const std::uint32_t previous=static_cast<std::uint32_t>(
(static_cast<int>(exitCandidate)-candidateDirection+16)%16);
const auto terminal=ringPoint(exitCandidate,loops);
const auto beforeTerminal=ringPoint(previous,loops);
const double incomingX=static_cast<double>(terminal[0]-beforeTerminal[0]);
const double incomingY=static_cast<double>(terminal[1]-beforeTerminal[1]);
const double outgoingX=static_cast<double>(lowerMouth[0]-terminal[0]);
const double outgoingY=static_cast<double>(lowerMouth[1]-terminal[1]);
const double incomingLength=std::hypot(incomingX,incomingY);
const double outgoingLength=std::hypot(outgoingX,outgoingY);
if(incomingLength<=1.0e-6||outgoingLength<=1.0e-6)continue;
const double alignment=(incomingX*outgoingX+incomingY*outgoingY)
/(incomingLength*outgoingLength);
const double separation=std::hypot(
static_cast<double>(terminal[0]-firstRing[0]),
static_cast<double>(terminal[1]-firstRing[1]));
const double score=alignment+0.20*std::clamp(
separation/(2.0*qPerMetre),0.0,2.0);
if(score>bestExitScore)
{bestExitScore=score;lowerPhase=exitCandidate;}
}
for(int step=1;step<=16;++step)
{
int directionIndex=static_cast<int>(candidate)
+candidateDirection*step;
directionIndex=(directionIndex%16+16)%16;
append(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
}
}
return measure();
};
for(std::uint32_t offset=0;offset<16;++offset)
{
const std::uint32_t candidate=static_cast<std::uint32_t>(
(hashedPhase+16-offset)%16);
const auto point=ringPoint(candidate);
const double entryX=static_cast<double>(point[0]-upperMouth[0]);
const double entryY=static_cast<double>(point[1]-upperMouth[1]);
const double entryLength=std::hypot(entryX,entryY);
if(entryLength<=1.0e-6)continue;
const auto entryDistance=distanceQ(point[0]-upperMouth[0],
point[1]-upperMouth[1]);
for(const int candidateDirection:{hashedDirection,-hashedDirection})
{
const std::uint64_t routeLength=candidateRouteLength(candidate,
candidateDirection);
if(routeLength==0)continue;
const std::uint32_t next=static_cast<std::uint32_t>(
(static_cast<int>(candidate)+candidateDirection+16)%16);
const auto nextPoint=ringPoint(next);
const double tangentX=static_cast<double>(nextPoint[0]-point[0]);
const double tangentY=static_cast<double>(nextPoint[1]-point[1]);
const double tangentLength=std::hypot(tangentX,tangentY);
if(tangentLength<=1.0e-6)continue;
const double tangentAlignment=(entryX*tangentX+entryY*tangentY)
/(entryLength*tangentLength);
const std::int64_t firstAxisZ=upperMouth[2]+mouthRadius
-playerCenterAboveFloorQ;
const std::int64_t secondAxisZ=firstAxisZ+PassageRoundRatio(
lowerMouth[2]-upperMouth[2],static_cast<std::int64_t>(entryDistance),
static_cast<std::int64_t>(routeLength));
double candidateTreadClearance=std::numeric_limits<double>::infinity();
const int entrySamples=std::max(1,static_cast<int>(std::ceil(
entryLength/(0.25*qPerMetre))));
for(int sample=0;sample<=entrySamples;++sample)
{
const double t=static_cast<double>(sample)/entrySamples;
const double qx=upperMouth[0]+t*entryX;
const double qy=upperMouth[1]+t*entryY;
const double qz=firstAxisZ+t*(secondAxisZ-firstAxisZ)
-mouthRadius+0.20*qPerMetre;
for(const auto& previousPassage:layout.passages)
{
if(previousPassage.stableId==passage.stableId
||previousPassage.controlPoints.size()<2)continue;
for(std::size_t previousSegment=0;
previousSegment+1<previousPassage.controlPoints.size();
++previousSegment)
{
const auto& previousA=previousPassage.controlPoints[previousSegment];
const auto& previousB=previousPassage.controlPoints[previousSegment+1];
const double ax=static_cast<double>(previousA.positionSubvoxels[0]);
const double ay=static_cast<double>(previousA.positionSubvoxels[1]);
const double az=static_cast<double>(previousA.positionSubvoxels[2]);
const double dx=static_cast<double>(previousB.positionSubvoxels[0])-ax;
const double dy=static_cast<double>(previousB.positionSubvoxels[1])-ay;
const double dz=static_cast<double>(previousB.positionSubvoxels[2])-az;
const double horizontalLengthSquared=dx*dx+dy*dy;
const double projection=horizontalLengthSquared>0.0
?std::clamp(((qx-ax)*dx+(qy-ay)*dy)
/horizontalLengthSquared,0.0,1.0):0.0;
const double centerX=ax+projection*dx;
const double centerY=ay+projection*dy;
const double centerZ=az+projection*dz
-(previousA.radiusSubvoxels+projection*(
static_cast<double>(previousB.radiusSubvoxels)
-previousA.radiusSubvoxels))
+1.13*qPerMetre;
const double vertical=std::max(0.0,
std::abs(qz-centerZ)-0.54*qPerMetre);
const double clearance=std::sqrt(
(qx-centerX)*(qx-centerX)+(qy-centerY)*(qy-centerY)
+vertical*vertical)-0.34*qPerMetre;
candidateTreadClearance=std::min(candidateTreadClearance,
clearance);
}
}
}
double previousTreadClearance=std::numeric_limits<double>::infinity();
const double entryXQ=entryX,entryYQ=entryY;
const double entryZQ=static_cast<double>(secondAxisZ-firstAxisZ);
const double entryLengthSquared=entryXQ*entryXQ+entryYQ*entryYQ
+entryZQ*entryZQ;
const double mouthStartT=std::clamp(
mouthRadius/entryLength,0.0,1.0);
for(const auto& previousPassage:layout.passages)
{
if(previousPassage.stableId==passage.stableId
||previousPassage.controlPoints.size()<2)continue;
for(std::size_t previousSegment=0;
previousSegment+1<previousPassage.controlPoints.size();
++previousSegment)
{
const auto& previousA=previousPassage.controlPoints[previousSegment];
const auto& previousB=previousPassage.controlPoints[previousSegment+1];
const double ax=static_cast<double>(previousA.positionSubvoxels[0]);
const double ay=static_cast<double>(previousA.positionSubvoxels[1]);
const double az=static_cast<double>(previousA.positionSubvoxels[2]);
const double dx=static_cast<double>(previousB.positionSubvoxels[0])-ax;
const double dy=static_cast<double>(previousB.positionSubvoxels[1])-ay;
const double dz=static_cast<double>(previousB.positionSubvoxels[2])-az;
for(int sample=0;sample<=8;++sample)
{
const double t=static_cast<double>(sample)/8.0;
const double px=ax+t*dx;
const double py=ay+t*dy;
const double pz=az+t*dz
-(previousA.radiusSubvoxels+t*(
static_cast<double>(previousB.radiusSubvoxels)
-previousA.radiusSubvoxels))
+0.20*qPerMetre;
const double projection=entryLengthSquared>0.0
?std::clamp(((px-upperMouth[0])*entryXQ
+(py-upperMouth[1])*entryYQ
+(pz-firstAxisZ)*entryZQ)/entryLengthSquared,
mouthStartT,1.0):mouthStartT;
const double ex=px-(upperMouth[0]+projection*entryXQ);
const double ey=py-(upperMouth[1]+projection*entryYQ);
const double ez=pz-(firstAxisZ+projection*entryZQ);
const double clearance=std::sqrt(ex*ex+ey*ey+ez*ez)
-mouthRadius;
previousTreadClearance=std::min(previousTreadClearance,
clearance);
}
}
}
const double clearanceScore=std::isfinite(candidateTreadClearance)
&&std::isfinite(previousTreadClearance)
?std::min(candidateTreadClearance,previousTreadClearance)/qPerMetre
:0.0;
if(phaseScoreCount<phaseScores.size())
phaseScores[phaseScoreCount++]={candidate,candidateDirection,
routeLength,clearanceScore,tangentAlignment};
}
}
if(phaseScoreCount>0)
{
double bestClearance=-std::numeric_limits<double>::infinity();
for(std::size_t index=0;index<phaseScoreCount;++index)
bestClearance=std::max(bestClearance,phaseScores[index].clearance);
constexpr double ClearanceToleranceMetres=0.10;
const FPhaseScore* selected=nullptr;
for(std::size_t index=0;index<phaseScoreCount;++index)
{
const auto& candidate=phaseScores[index];
if(candidate.clearance+ClearanceToleranceMetres<bestClearance)continue;
const bool shorter=!selected||candidate.routeLength<selected->routeLength;
const bool stableLengthTie=selected
&&candidate.routeLength==selected->routeLength
&&(candidate.clearance>selected->clearance+1.0e-6
||(std::abs(candidate.clearance-selected->clearance)<=1.0e-6
&&candidate.tangent>selected->tangent));
if(shorter||stableLengthTie)selected=&candidate;
}
if(selected)
{
phase=selected->phase;
direction=selected->direction;
clockwise=direction>0;
}
}
}
direction=clockwise?1:-1;
std::vector<std::array<std::int64_t,2>> xy;
const auto pushXY=[&](std::array<std::int64_t,2> p)
{if(xy.empty()||xy.back()!=p)xy.push_back(p);};
@@ -2705,6 +2987,12 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
}
++loops;
}
const std::int64_t mouthPlanSeparation=distanceQ(
lowerMouth[0]-upperMouth[0],lowerMouth[1]-upperMouth[1]);
const bool shortGeneratedClosure=!hasLowerTangent&&!hasAnchorHelix
&&pathStyle==1u&&loops<=1
&&mouthPlanSeparation<=2*mouthRadius
&&verticalDistance<=2*mouthRadius+qPerMetre/2;
if(hasLowerTangent)
{
std::uint32_t lowerPhase=static_cast<std::uint32_t>(PassageSeed(seed,
@@ -2763,6 +3051,48 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
}
pushXY(ringPoint(lowerPhase));
}
else if(shortGeneratedClosure)
{
// A one-turn generated route whose mouths share a plan location
// must leave and rejoin from different ring tangents. Returning on
// the entry chord creates a hairpin that overlaps its own tread.
double bestExitScore=-std::numeric_limits<double>::infinity();
std::uint32_t lowerPhase=phase;
const auto firstRing=ringPoint(phase,loops);
for(std::uint32_t candidate=0;candidate<16;++candidate)
{
if(candidate==phase)continue;
const std::uint32_t previous=static_cast<std::uint32_t>(
(static_cast<int>(candidate)-direction+16)%16);
const auto terminal=ringPoint(candidate,loops);
const auto beforeTerminal=ringPoint(previous,loops);
const double incomingX=static_cast<double>(terminal[0]-beforeTerminal[0]);
const double incomingY=static_cast<double>(terminal[1]-beforeTerminal[1]);
const double outgoingX=static_cast<double>(lowerMouth[0]-terminal[0]);
const double outgoingY=static_cast<double>(lowerMouth[1]-terminal[1]);
const double incomingLength=std::hypot(incomingX,incomingY);
const double outgoingLength=std::hypot(outgoingX,outgoingY);
if(incomingLength<=1.0e-6||outgoingLength<=1.0e-6)continue;
const double tangentAlignment=(incomingX*outgoingX+incomingY*outgoingY)
/(incomingLength*outgoingLength);
const double separation=std::hypot(
static_cast<double>(terminal[0]-firstRing[0]),
static_cast<double>(terminal[1]-firstRing[1]));
const double score=tangentAlignment
+0.20*std::clamp(separation/(2.0*qPerMetre),0.0,2.0);
if(score>bestExitScore)
{bestExitScore=score;lowerPhase=candidate;}
}
for(int step=1;step<=16;++step)
{
int directionIndex=static_cast<int>(phase)+direction*step;
directionIndex=(directionIndex%16+16)%16;
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
}
std::printf("[ResolvedPassageGeneratedExitApproach] id=%s turns=%u phase=%u exit_phase=%u tangent_score=%.3f status=PASS\n",
passage.stableId.c_str(),loops,phase,lowerPhase,bestExitScore);
}
pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
std::size_t lowerMouthIndex=xy.size()-1;
std::vector<std::array<std::int64_t,3>> lowerLandingPoints;
@@ -2841,7 +3171,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
for(std::size_t i=0;i<xy.size();++i)
{
std::int64_t radius=(i<=1||i==lowerMouthIndex
||i+1>=xy.size()-1)?mouthRadius:midRadius;
||(i>lowerMouthIndex&&i+1>=xy.size()-1))?mouthRadius:midRadius;
if(surfaceGroundDeparture&&i<lowerMouthIndex)
{
const double t=std::clamp(static_cast<double>(cumulative[i])
@@ -2850,6 +3180,24 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
radius=surfacePortalRadius+static_cast<std::int64_t>(std::llround(
(midRadius-surfacePortalRadius)*smooth));
}
// Grow the lower bore into its mouth over fixed route distance. Assigning
// the full mouth radius to the penultimate point creates a segment-length
// flare whose cubic-detrended profile can read as a short-period wall beat.
if(i<=lowerMouthIndex)
{
constexpr std::int64_t LowerMouthRadiusBlendLengthMetres=12;
const std::uint64_t blendLength=static_cast<std::uint64_t>(
std::max<std::int64_t>(LowerMouthRadiusBlendLengthMetres*qPerMetre,
2*mouthRadius));
const std::uint64_t distanceToLowerMouth=mouthHorizontal-cumulative[i];
if(distanceToLowerMouth<blendLength)
{
const double t=static_cast<double>(distanceToLowerMouth)/blendLength;
const double smooth=t*t*(3.0-2.0*t);
radius=midRadius+static_cast<std::int64_t>(std::llround(
(mouthRadius-midRadius)*(1.0-smooth)));
}
}
std::int64_t playerZ=0;
if(i>lowerMouthIndex)
playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2];
File diff suppressed because it is too large. Load diff
+179 -7
View File
@@ -1891,6 +1891,17 @@ namespace VoxelForgeCore
std::vector<TileKey> output(tiles.begin(),tiles.end());
std::sort(output.begin(),output.end(),[&](const TileKey& a,const TileKey& b)
{
const auto tier=[&](const TileKey& key)
{
if(key.lod==0&&std::abs(key.coordinate.x-centerChunk.x)<=1
&&std::abs(key.coordinate.y-centerChunk.y)<=1
&&std::abs(key.coordinate.z-centerChunk.z)<=1)return 0;
return key.lod<=2?1:2;
};
const int tierA=tier(a),tierB=tier(b);
if(tierA!=tierB)return tierA<tierB;
if(tierA==1&&a.lod!=b.lod)return a.lod<b.lod;
if(tierA==2&&a.lod!=b.lod)return a.lod>b.lod;
const double da=TileDistanceSquared(a,centerChunk),db=TileDistanceSquared(b,centerChunk);
if(da!=db)return da<db;
if(a.lod!=b.lod)return a.lod<b.lod;
@@ -1997,6 +2008,7 @@ namespace VoxelForgeCore
std::optional<TileMesh> World::generateTile(const TileKey& key,const TileGenerationOptions& options,
std::string& error) const
{
VoxelForgeCore::Linux77Profiler::FScopedLod l77Lod(key.lod);
// Fine owner tiles had zero direct-map conflict evictions in the cold audit. Keep their
// single-probe path and use associativity for the coarse coordinate families that collide.
// Les tuiles proches n'avaient aucun conflit; garder leur acces simple et associer les LOD grossiers.
@@ -2162,6 +2174,44 @@ namespace VoxelForgeCore
TileMesh out;out.key=key;
const auto CopyIslandCacheMetrics=[&]()
{
if(options.linux77Profile)
{
const auto& QueryCounts=FStandaloneShapeWorld::GetThreadClassificationStats();
for(std::size_t Stage=0;Stage<VoxelForgeCore::Linux77Profiler::StageCount;++Stage)
for(std::size_t Route=0;Route<VoxelForgeCore::Linux77Profiler::RouteCount;++Route)
{
const std::size_t Index=(Stage*VoxelForgeCore::Linux77Profiler::RouteCount
+Route)*2;
out.diagnostics.linux77DispatchCounters[Index]=
QueryCounts.Linux77DispatchCounters[Stage][Route].calls;
out.diagnostics.linux77DispatchCounters[Index+1]=
QueryCounts.Linux77DispatchCounters[Stage][Route].points;
}
out.diagnostics.linux77GridInteriorSamples=McStageTimings.DensityGridInteriorSamples;
out.diagnostics.linux77GridHaloSamples=McStageTimings.DensityGridHaloSamples;
}
if(options.instrumentTileDrain)
{
const auto& QueryCounts=FStandaloneShapeWorld::GetThreadClassificationStats();
out.diagnostics.densityPointCalls=QueryCounts.DensityPointCalls;
out.diagnostics.densityFloatPointCalls=QueryCounts.DensityFloatPointCalls;
out.diagnostics.densityLatticePointCalls=QueryCounts.DensityLatticePointCalls;
out.diagnostics.densityUnclampedPointCalls=QueryCounts.DensityUnclampedPointCalls;
out.diagnostics.densityX8PointCalls=QueryCounts.DensityX8PointCalls;
out.diagnostics.densityFieldEvalNanoseconds=QueryCounts.DensityFieldEvalNanoseconds;
out.diagnostics.densityFieldEvalSamplePoints=QueryCounts.DensityFieldEvalSamplePoints;
out.diagnostics.legacyFieldEvalCalls=QueryCounts.LegacyFieldEvalCalls;
out.diagnostics.legacyFieldTermCalls=QueryCounts.LegacyFieldTermCalls;
out.diagnostics.legacyFieldTermNanoseconds=QueryCounts.LegacyFieldTermNanoseconds;
out.diagnostics.legacyFieldTermTimedCalls=QueryCounts.LegacyFieldTermTimedCalls;
out.diagnostics.legacyPrimitiveCandidateVisits=
QueryCounts.LegacyPrimitiveCandidateVisits;
out.diagnostics.legacyPrimitiveDistanceEvaluations=
QueryCounts.LegacyPrimitiveDistanceEvaluations;
out.diagnostics.legacySmoothMinCalls=QueryCounts.LegacySmoothMinCalls;
out.diagnostics.legacySmoothMinNanoseconds=QueryCounts.LegacySmoothMinNanoseconds;
out.diagnostics.legacySmoothMinTimedCalls=QueryCounts.LegacySmoothMinTimedCalls;
}
if(!options.instrumentTileDrain)return;
islandCacheInstrumentation.tileCycles=VoxelForgeCoreShared::ReadCpuCycles()
-islandCacheInstrumentation.tileStartCycles;
@@ -2200,16 +2250,25 @@ namespace VoxelForgeCore
line<<" native_"<<CallerNames[i]<<"="<<a.sampledNativeCalls[i]*256<<"/"
<<a.sampledNativeCalls[i]<<"/"
<<Estimate(a.nativeCycles[i],a.sampledNativeCalls[i]*256,
a.sampledNativeCalls[i])
<<" native_"<<CallerNames[i]<<"_ns_est="
<<Estimate(a.nativeNanoseconds[i],a.sampledNativeCalls[i]*256,
a.sampledNativeCalls[i]);
line<<" field_"<<CallerNames[i]<<"="<<a.sampledFieldCalls[i]*256<<"/"
<<a.sampledFieldCalls[i]<<"/"
<<Estimate(a.fieldCycles[i],a.sampledFieldCalls[i]*256,
a.sampledFieldCalls[i])
<<" field_"<<CallerNames[i]<<"_ns_est="
<<Estimate(a.fieldNanoseconds[i],a.sampledFieldCalls[i]*256,
a.sampledFieldCalls[i]);
}
for(std::size_t i=0;i<BucketNames.size();++i)
line<<" comp_"<<BucketNames[i]<<"="
<<Estimate(a.exclusiveCycles[i],a.scopeSamples[i]*256,
a.scopeSamples[i])
<<" comp_"<<BucketNames[i]<<"_ns_est="
<<Estimate(a.exclusiveNanoseconds[i],a.scopeSamples[i]*256,
a.scopeSamples[i])
<<"/"<<a.scopeSamples[i]<<"/"<<a.scopeSamples[i]*256;
const auto& shared=islandCacheInstrumentation.shared;
line<<" local_probe_calls="<<(islandCacheInstrumentation.localHits
@@ -2323,6 +2382,7 @@ namespace VoxelForgeCore
&&bHasNativeSurfaceHorizonPlan&&!bHasIslandPlanInTile;
const bool bSkipFarShellRepair=key.lod>=7&&bUseNativeSurfaceHorizon;
FStandaloneShapeWorld::ResetThreadClassificationStats();
FStandaloneShapeWorld::SetThreadDensityQueryProfileEnabled(options.instrumentTileDrain);
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(
options.captureColumnCoordinates);
if(options.captureColumnCoordinates)
@@ -2335,6 +2395,7 @@ namespace VoxelForgeCore
static_cast<std::int64_t>(origin.Y+extent)*SubvoxelsPerVoxel);
}
bool bNativeSurfaceTile=false;
bool bTileProofSkipSafe=false;
bool bNativeSurfaceBandSafe=false;
double classificationColumnComputeMilliseconds=0.0;
if(bHasWorldAssembly)
@@ -2342,10 +2403,15 @@ namespace VoxelForgeCore
const double columnMillisecondsBeforeClassification=
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
const auto Classification=tileShape.ClassifyBoxDetailed(origin,step,cells);
out.diagnostics.tileProofVetoReason=Classification.LegacyProofVeto;
out.diagnostics.tileProofSafe=Classification.bSkipSafe;
out.diagnostics.tileLegacyBlockProofRelevant=
Classification.bLegacyBlockProofRelevant;
classificationColumnComputeMilliseconds=std::max(0.0,
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds
-columnMillisecondsBeforeClassification);
bNativeSurfaceTile=Classification.bNativeSurface;
bTileProofSkipSafe=Classification.bSkipSafe;
if(options.profileMesherStages)
out.diagnostics.nativeSurfaceTile=bNativeSurfaceTile;
// NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its
@@ -2353,7 +2419,7 @@ namespace VoxelForgeCore
// unknown; edits and cross-strate tiles are still excluded below.
// NativeSurfaceDensityAt ne consomme que le champ et les volumes bornes du manifeste.
bNativeSurfaceBandSafe=bNativeSurfaceTile;
if(bNativeSurfaceTile)
if(bNativeSurfaceTile||bTileProofSkipSafe)
{
out.diagnostics.tileWasClassified=true;
switch(Classification.bUnknown?EVoxelTileClass::Mixed:Classification.Verdict)
@@ -2362,12 +2428,30 @@ namespace VoxelForgeCore
case EVoxelTileClass::AllAir:out.diagnostics.tileClass=TileClass::allAir;break;
case EVoxelTileClass::Mixed:out.diagnostics.tileClass=TileClass::mixed;break;
}
if((out.diagnostics.tileClass==TileClass::allSolid
if(bTileProofSkipSafe&&(out.diagnostics.tileClass==TileClass::allSolid
||out.diagnostics.tileClass==TileClass::allAir)
&&!options.hashDensity&&!options.captureDensitySamples&&!bNeedsFarShell
&&(!options.hashDensity||options.allowUniformSkipsWhenHashing)
&&!options.captureDensitySamples&&!bNeedsFarShell
&&!options.auditUniformBlockProofs)
{
out.diagnostics.meshingSkipped=true;
if(options.hashDensity)
{
const FVoxelMeshData EmptyMesh;
out.densityHash=14695981039346656037ull;
out.meshHash=HashViewerMesh(EmptyMesh);
out.payloadHash=HashPayload(EmptyMesh);
out.topologyHash=HashTopology(EmptyMesh);
out.materialHash=HashMaterialSamples(out.vertices);
if(key.lod>=4)
{
out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion);
out.payloadHash=SaltFarHorizonHash(out.payloadHash,options.farHorizonVersion);
out.topologyHash=SaltFarHorizonHash(out.topologyHash,options.farHorizonVersion);
}
out.hasDensityHash=out.hasMeshHash=out.hasPayloadHash=out.hasTopologyHash=true;
out.hasMaterialHash=true;
}
const auto Counts=FStandaloneShapeWorld::GetThreadClassificationStats();
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
out.diagnostics.solidBoxes=Counts.SolidBoxes;
@@ -2401,10 +2485,12 @@ namespace VoxelForgeCore
out.densitySampleCount=0;
return out;
}
// Native SurfaceWorld classifies each 8-cell block against its exact sample
// lattice. This lets the mesher avoid filling density grids for proven air/rock.
mesher.bUseBlockEarlyOut=true;
}
// Preserve the committed native-field block path. Legacy block certificates
// remain audit-only until their cost is repaid against the full-grid path.
if(bHasWorldAssembly&&step<=2
&&(bNativeSurfaceTile||options.auditUniformBlockProofs))
mesher.bUseBlockEarlyOut=true;
}
const auto MeshingStart=std::chrono::steady_clock::now();
int32 MeshBandMinZVoxel=options.bandMinZVoxel;
@@ -2620,7 +2706,22 @@ namespace VoxelForgeCore
// GenerateMesh n'échantillonne que origin..origin+extent et la marge de normale d'un pas ;
// queryMinimum/queryMaximum ci-dessus couvrent exactement ce domaine inclusif.
const FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope(
tileShape,queryMinimum,queryMaximum);
tileShape,queryMinimum,queryMaximum,
options.profilePassageComposer?&out.diagnostics:nullptr);
if(options.profilePassageComposer)
{
out.diagnostics.passageComposerProfiled=true;
out.diagnostics.passageTileFeatureCount=passageFeatureScope.featureCount();
if(impl_->resolvedLayout)
{
out.diagnostics.passageTileRouteSegmentCandidates=
passageFeatureScope.routeSegmentCandidateCount();
for(std::size_t passageIndex=0;
passageIndex<impl_->resolvedLayout->passages.size();++passageIndex)
out.diagnostics.passageTileMouthSegmentCandidates+=
passageFeatureScope.mouthSupportSegments(passageIndex).size();
}
}
const bool bDiagnosticColumnSampling=options.diagnosticGlobalOctaveDrop!=0
||options.diagnosticColumnInterpolationStride>1;
const std::uint32_t PreviousOctaveDrop=bDiagnosticColumnSampling
@@ -2716,6 +2817,8 @@ namespace VoxelForgeCore
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-MeshingStart).count();
out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0;
VoxelForgeCore::Linux77Profiler::FScopedStage l77RefineStage(
VoxelForgeCore::Linux77Profiler::EStage::Refine);
const auto FarShellStart=std::chrono::steady_clock::now();
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:
(bShapeReady?*impl_->shape:tileShape);
@@ -2849,6 +2952,7 @@ namespace VoxelForgeCore
AppendStratumBoundaryFloors(world,farShape,nativeKey,step,mesh);
AppendStratumBoundaryCeilings(world,farShape,nativeKey,step,mesh);
}
l77RefineStage.Restore();
out.densitySampleCount=static_cast<std::uint64_t>(std::max<int64>(0,sampleCount));
if(bHasWorldAssembly)
@@ -2959,6 +3063,74 @@ namespace VoxelForgeCore
const auto& values=options.captureDensitySamples?out.debugDensitySamples:densityValues;
for(const float value:values)
{ densityHash^=std::bit_cast<std::uint32_t>(value);densityHash*=1099511628211ull; }
// A stream-hash run is the full-grid reference path. Record the exact sign
// uniformity ceiling from the same captured lattice that feeds Marching Cubes.
// Blocks are disjoint sample cubes; partial cubes at tile edges are included.
const std::size_t gridWidth=static_cast<std::size_t>(
std::clamp(MeshCells,2,CHUNK_SIZE)+3);
const std::size_t xySamples=gridWidth*gridWidth;
const std::size_t zSamples=xySamples>0&&values.size()%xySamples==0
?values.size()/xySamples:0;
bool tileHasSign=false,tileUniform=true;
bool tileAir=false;
for(const float value:values)
{
if(!std::isfinite(value)||value==0.0f)
{tileUniform=false;continue;}
const bool air=value>0.0f;
if(!tileHasSign){tileHasSign=true;tileAir=air;}
else if(tileAir!=air)tileUniform=false;
}
const auto CountUniformBlocks=[&](std::size_t blockWidth)
{
std::uint64_t uniformBlocks=0,uniformSamples=0;
if(zSamples==0)return std::pair{uniformBlocks,uniformSamples};
for(std::size_t z0=0;z0<zSamples;z0+=blockWidth)
for(std::size_t y0=0;y0<gridWidth;y0+=blockWidth)
for(std::size_t x0=0;x0<gridWidth;x0+=blockWidth)
{
const std::size_t z1=std::min(zSamples,z0+blockWidth);
const std::size_t y1=std::min(gridWidth,y0+blockWidth);
const std::size_t x1=std::min(gridWidth,x0+blockWidth);
bool hasSign=false,uniform=true,air=false;
for(std::size_t z=z0;z<z1&&uniform;++z)
for(std::size_t y=y0;y<y1&&uniform;++y)
for(std::size_t x=x0;x<x1;++x)
{
const float value=values[(z*gridWidth+y)*gridWidth+x];
if(!std::isfinite(value)||value==0.0f)
{uniform=false;break;}
const bool sampleAir=value>0.0f;
if(!hasSign){hasSign=true;air=sampleAir;}
else if(air!=sampleAir){uniform=false;break;}
}
if(uniform&&hasSign)
{
++uniformBlocks;
uniformSamples+=static_cast<std::uint64_t>(z1-z0)
*static_cast<std::uint64_t>(y1-y0)
*static_cast<std::uint64_t>(x1-x0);
}
}
return std::pair{uniformBlocks,uniformSamples};
};
const auto [uniformBlocks8,uniformSamples8]=CountUniformBlocks(8);
const auto [uniformBlocks4,uniformSamples4]=CountUniformBlocks(4);
const double centerZ=(static_cast<double>(origin.Z)
+static_cast<double>(extent)*0.5)*static_cast<double>(world.CoreVoxelSizeMicrons)
/10000.0;
const int32 stratum=world.Manager
?world.Manager->GetStrateIndex(static_cast<float>(centerZ)):-1;
std::printf("[UniformGridCeiling] strate=%d lod=%d key=(%d,%d,%d) samples=%zu uniform=%d air=%d blocks8=%llu samples8=%llu blocks4=%llu samples4=%llu proof_safe=%d proof_veto=%u\n",
stratum,key.lod,key.coordinate.x,key.coordinate.y,key.coordinate.z,values.size(),
tileUniform&&tileHasSign?1:0,tileUniform&&tileHasSign&&tileAir?1:0,
static_cast<unsigned long long>(uniformBlocks8),
static_cast<unsigned long long>(uniformSamples8),
static_cast<unsigned long long>(uniformBlocks4),
static_cast<unsigned long long>(uniformSamples4),
out.diagnostics.tileProofSafe?1:0,
static_cast<unsigned>(out.diagnostics.tileProofVetoReason));
out.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh);
out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh);
out.materialHash=HashMaterialSamples(out.vertices);
+72 -3
View File
@@ -20,6 +20,7 @@
#define VF_FIELD_CLAMP(v,a,b) clamp(v,a,b)
#define VF_FIELD_LERP(a,b,t) lerp(a,b,t)
#else
#include <array>
#include <algorithm>
#include <cmath>
#include <cstdint>
@@ -244,11 +245,70 @@ VF_FIELD_INLINE float VF_FieldValueNoise(float x, float y, float z,
return VF_FieldLerp(VF_FieldLerp(z00, z10, ty), VF_FieldLerp(z01, z11, ty), tz);
}
#if !defined(VF_FIELD_HLSL)
struct VF_FieldValueNoiseCacheEntry
{
VF_FieldI32 x=0,y=0,z=0,wavelength=0;
VF_FieldU32 seed=0;
bool valid=false;
std::array<float,8> corners{};
};
// Reuse the eight lattice values while adjacent voxel samples remain in the same
// value-noise cell. Only the integer hash lookups are shared; interpolation keeps
// the same inputs and operation order as VF_FieldValueNoise.
VF_FIELD_INLINE float VF_FieldCachedValueNoise(float x,float y,float z,
VF_FieldI32 wavelength,VF_FieldU32 seed)
{
static thread_local std::array<VF_FieldValueNoiseCacheEntry,64> cache{};
const float inv=1.0f/VF_FIELD_CAST_F32(wavelength);
const float fx=x*inv,fy=y*inv,fz=z*inv;
const VF_FieldI32 x0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fx));
const VF_FieldI32 y0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fy));
const VF_FieldI32 z0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fz));
const VF_FieldU32 cellHash=VF_FieldMix(seed
^VF_FIELD_CAST_U32(x0)*0x68E31DA4u
^VF_FIELD_CAST_U32(y0)*0x1B56C4E9u
^VF_FIELD_CAST_U32(z0)*0x9E3779B9u
^VF_FIELD_CAST_U32(wavelength)*0x85EBCA77u);
auto& entry=cache[cellHash&(cache.size()-1)];
if(!entry.valid||entry.x!=x0||entry.y!=y0||entry.z!=z0
||entry.wavelength!=wavelength||entry.seed!=seed)
{
entry.x=x0;entry.y=y0;entry.z=z0;entry.wavelength=wavelength;
entry.seed=seed;entry.valid=true;
entry.corners={
VF_FieldNoiseValue(x0,y0,z0,seed),
VF_FieldNoiseValue(x0+1,y0,z0,seed),
VF_FieldNoiseValue(x0,y0+1,z0,seed),
VF_FieldNoiseValue(x0+1,y0+1,z0,seed),
VF_FieldNoiseValue(x0,y0,z0+1,seed),
VF_FieldNoiseValue(x0+1,y0,z0+1,seed),
VF_FieldNoiseValue(x0,y0+1,z0+1,seed),
VF_FieldNoiseValue(x0+1,y0+1,z0+1,seed)};
}
const float tx=VF_FieldFade(fx-VF_FIELD_CAST_F32(x0));
const float ty=VF_FieldFade(fy-VF_FIELD_CAST_F32(y0));
const float tz=VF_FieldFade(fz-VF_FIELD_CAST_F32(z0));
const float z00=VF_FieldLerp(entry.corners[0],entry.corners[1],tx);
const float z10=VF_FieldLerp(entry.corners[2],entry.corners[3],tx);
const float z01=VF_FieldLerp(entry.corners[4],entry.corners[5],tx);
const float z11=VF_FieldLerp(entry.corners[6],entry.corners[7],tx);
return VF_FieldLerp(VF_FieldLerp(z00,z10,ty),VF_FieldLerp(z01,z11,ty),tz);
}
#endif
VF_FIELD_INLINE float VF_FieldSurfaceRelief(float x, float y, float z, VF_FieldU32 seed)
{
#if defined(VF_FIELD_HLSL)
const float macro = VF_FieldValueNoise(x, y, z, 32, seed ^ 0xB5297A4Du);
const float mid = VF_FieldValueNoise(x, y, z, 16, seed ^ 0x63D83595u);
const float fine = VF_FieldValueNoise(x, y, z, 4, seed ^ 0xA511E9B3u);
#else
const float macro=VF_FieldCachedValueNoise(x,y,z,32,seed^0xB5297A4Du);
const float mid=VF_FieldCachedValueNoise(x,y,z,16,seed^0x63D83595u);
const float fine=VF_FieldCachedValueNoise(x,y,z,4,seed^0xA511E9B3u);
#endif
return 1.20f * macro + (0.80f + 0.25f * macro) * (0.40f * mid + 0.60f * fine);
}
@@ -335,12 +395,21 @@ VF_FIELD_INLINE float VF_FieldPassageWallRelief(float x,float y,float z,VF_Field
{
// Use long, non-commensurate rotated scales. The shared passage wall keeps broad,
// uneven rock relief without a short wavelength that can read as repeated rings.
#if defined(VF_FIELD_HLSL)
const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
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);
#else
const float broad=VF_FieldCachedValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
seed^0xB5297A4Du);
const float mid=VF_FieldCachedValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
seed^0x63D83595u);
const float fine=VF_FieldCachedValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
seed^0xA511E9B3u);
#endif
return 0.50f*broad+0.35f*mid+0.15f*fine;
}
@@ -394,9 +463,9 @@ VF_FIELD_INLINE float VF_FieldOwnerCellular(float x,float y,float z)
{
const VF_FieldI32 nx=cellX+dx,ny=cellY+dy,nz=cellZ+dz;
const VF_FieldU32 h=VF_FieldVoxelHashMix(
VF_FIELD_CAST_U32(nx+0x7FFFFFFF)
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(ny+0x7FFFFFFF)*2654435761u)
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(nz+0x7FFFFFFF)*374761393u));
(VF_FIELD_CAST_U32(nx)+0x7FFFFFFFu)
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(ny)+0x7FFFFFFFu)*2654435761u)
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(nz)+0x7FFFFFFFu)*374761393u));
const float fpx=VF_FIELD_CAST_F32(dx)
+VF_FIELD_CAST_F32(h&0xFFFFFFu)*(1.0f/16777216.0f)-fracX;
const VF_FieldU32 hy=VF_FieldVoxelHashMix(h^0x12345678u);
@@ -0,0 +1,48 @@
Thank you, that was a strong first pass. Your scope warning about L76 was right and important. I need to correct my
brief: "about 75% of field time is the passage composer" was **wrong**.
- L76's term profiler covered only about 6.0M legacy samples for FI (5.6M for TN).
- L73 counts about 52M density calls per FI or TN whole view: 512 LOD0 tiles × 44,121, 448 LOD1 × 43,322, and so
on.
- The measured passage/support cost is about 6.5 of FI's ~40 field worker-seconds (~15%), not 75%. Roughly
30 worker-seconds of field time is not attributed to any term we've measured.
Your gain arithmetic for recommendation 1 inherited that error. Please redo it.
## Deeper pass (you now have more reasoning budget)
Examine **the whole generation chain**, end to end, for one cold whole view on the legacy FI/TN path. For each
stage, say what it does, how often it runs, what it costs per call (from code structure and the measured numbers),
and whether its work is necessary:
- world/plan resolution;
- tile key selection;
- `ClassifyBoxDetailed`;
- the dispatch into `AnalyticDensityAt` (and whatever runs for the ~46M calls the term profiler did not see);
- the composer;
- grid fill;
- MC;
- normals;
- skirts;
- ground/cap refinement;
- payload;
- Unreal publication.
The central question: **where do the unattributed ~30 worker-seconds most plausibly go?** For example:
- a different evaluation path the profiler doesn't instrument;
- per-call overhead outside the timed terms (dispatch, stratum lookup, virtual calls, allocation, cache misses);
- border/halo samples evaluated twice;
- normals sampling the field again;
- duplicated work between neighbouring tiles or LODs.
Give file:line for each. Then name the cheapest measurement that would confirm your top explanation, so the next
implementation round can verify it first.
Then **revise your ranked recommendations** in the light of the whole chain. Keep what still holds and drop what
doesn't. Add anything the chain review reveals.
Same rules as before:
- read-only;
- no edits anywhere, including temp files;
- web allowed;
- labels [CODE]/[MEASURED]/[PRIOR ART]/[INFERENCE].
- At most ~3,500 words.
- **Lead with the stage-by-stage cost map**, then the revised ranking.
@@ -0,0 +1,133 @@
# VoxelForge — design review: how do we get a cold whole view from ~2.5–4.6 s to ~1 s?
You're reviewing a voxel terrain generator's performance architecture. **Read and reason only.** Don't edit,
build, run tests, commit or stash. You may read any file in this repository and search the web.
We've spent about 15 rounds finding and fixing hot spots, and the gains are shrinking. We want fresh eyes on the
*approach*, not another micro-optimisation. Question our framing where it's wrong.
## The system (summary, so you don't have to rediscover it)
**VoxelForge** is a UE5 plugin for underground density-field terrain. Generation lives in a standalone C++
library, "Core" (`Standalone/Core/`), that Unreal calls through an adapter
(`Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`).
- **The field.** Density is a *function*, not stored data: negative = solid. Core voxels are 25 cm. Tiles hold
32³ voxels.
- **Clipmap.** LOD0–8; each LOD doubles the voxel step. A "whole view" is about 2,250–2,850 tiles across all LODs
around one camera.
- **Meshing.** Marching cubes per tile (about 35³ samples per LOD0/1 tile with borders), plus normals and skirts.
- **The world is a vertical stack of "strates".** Each is ceiling → content → bedrock, with its own generator:
- SurfaceWorld (SW): heightfield, native path;
- FloatingIslands (FI), TunnelNetwork (TN) and VerticalShafts (VS): the "legacy compiled-analytic" path.
- **The owner's test world** stacks four of them: SW 512 m, FI 240 m, TN 128 m, VS 128 m tall.
- **Passages.** Walkable, winding, natural-looking tunnels near (0,0) connect each strate to the next. They're
carved by a shared "composer" that also adds support treads, boundary bands and mouth shaping.
- **Multiplayer.** No geometry is replicated. Every peer regenerates from the seed, the layout and a diff of
player edits. That's why peer determinism is mandatory.
## The goal and constraints
- **Goal:** about 1 s cold whole view at any strate arrival on the owner's PC (Ryzen 5800X3D, 8 cores/16 threads,
7900 XTX). Streaming must stay smooth in VR (Quest 3) and at high travel speed.
- **Must hold:**
- peer determinism (same inputs → same world on every machine);
- no visible quality regression;
- passages stay walkable from arrival mouth to departure mouth (capsule, support and grade audits exist);
- no game-thread hitches.
- **Not required:** byte-identical output with earlier versions. The owner accepts that perf passes change the
world's shape slightly, as long as every peer gets the same result and quality holds. (We have tended to treat
hash equality as a gate anyway; it's a convenience, not a rule.)
- **On the shelf, not a speed excuse:** a disk mesh cache is an accepted feature, but perf is judged cold.
## Measurements (owner world, measured on an i9-12900H with 8 workers, headless unless stated)
**Whole view now** (it was 2.6 / 7.2 / 6.0 / 2.7 s a week ago):
| Strate | Whole view |
|---|---:|
| SW | 2.4 s |
| FI | 4.6 s |
| TN | 4.3 s |
| VS | 2.5 s |
**In Unreal (SW arrival):**
- Core work equals headless;
- initial set ready at 3.74 s vs headless 2.41 s, the extra being scheduling;
- walkable ring 0.4–1.8 s.
**Where the time goes:**
- **L73:** field evaluation inside grid fill is about 40 of FI's 46 worker-seconds (TN 33 of 39). LOD0/1 tiles
evaluate about 44k density samples each.
- **L74/L75:** sound whole-tile skip proofs now certify about 22–26% of tile visits.
- Ceiling: 46–82% of LOD0–2 tiles are truly uniform, and 90–97% of samples lie in uniform 8³ blocks.
- Dominant remaining veto: tiles crossing a strate boundary.
- Legacy sub-tile (block) proofs were unsound once (missed passage wall/floor relief), then sound but slower
than they saved.
- **L76, per legacy sample:**
- passage/support composer ≈ **1.05 µs**;
- primitive scan ≈ 0.19 µs (0.25 primitive distance evaluations per sample, binned);
- boundary band ≈ 0.06 µs;
- noise ≈ 0.01 µs.
**About 75% of field time is the passage composer, even far from passages.**
- Per-tile passage culling was tried and changed 612 density samples / 24 mesh tiles. It was rejected only for
breaking hash equality, which is a convenience, not a rule; nobody checked whether that change was harmless.
**Tried and rejected:**
- the native FI generator (fails passage walk audits on this plan);
- deeper Unreal queues (done: 4.78 → 3.74 s).
**Trap we fell into once:** a diagnostic hash mode silently disabled the skips and doubled the timings. Measure the
production path.
## What to reason about (open; don't limit yourself to these)
1. **Is "evaluate a density function at ~44k points per tile, then march cubes" the right shape** for this world?
Consider, for example:
- sparse/hierarchical evaluation (octree or brick refinement driven by interval or Lipschitz bounds, as in
libfive/Fidget/MPR-style implicit rendering);
- reusing samples across nested clipmap LODs;
- evaluating smooth large-scale terms coarsely and interpolating;
- narrow-band evaluation near the surface only.
2. **The passages are a few winding tubes.** Why does every sample pay 1 µs for them? What representation would
make "far from any passage" free and "near a passage" cheap? For example:
- bake each passage once at plan time into a sparse distance/brick structure;
- a spatial index of segments with a conservative distance early-out;
- a per-tile passage candidate list built once.
What would a correct per-tile culling need so that it doesn't change the result? And if it changes the result,
how should we judge whether that change is acceptable?
3. **Which work is needed for "ready in 1 s"?** Maybe the target should be redefined: walkable ring first,
progressive LOD refinement, far LODs from cheaper representations or impostors, fewer tiles. What do comparable
engines do (Voxel Plugin, Teardown, Dreams, No Man's Sky, Minecraft-style streaming, Nanite-era voxel work)?
4. **Hardware.** 8 CPU cores plus a 7900 XTX that's mostly idle during load. An earlier GPU port was closed, and
determinism across GPU vendors is a concern. Is there a deterministic way to use it, e.g. integer/fixed-point
evaluation or a CPU-authoritative plus GPU-display split? Is SIMD (AVX2) evaluation of the legacy field
realistic?
5. **Anything in our process or measurement** that hides the real bottleneck.
## How to work efficiently
- **Trust the numbers above; don't re-measure or re-derive them.** Spend your effort on thinking.
- **Read code only to check whether a specific idea fits.** Entry points:
- `CODEMAP.md` (navigation) and `ARCHITECTURE.md` §8.10 (performance invariants);
- `Standalone/Core/src/ShapeWorldInternal.h`: composer, `AnalyticDensityAt`, `ClassifyBoxDetailed`, passage
terms;
- `Standalone/Core/src/VoxelForgeCore.cpp`: tile generation and skips;
- reports in `Standalone/Research/WorldAssemblerM3/`: `Linux73`–`Linux76`, `Research36`, and R38–R40 notes
inside `Linux73/` and `Linux76/`.
- **Use the web** for prior art and techniques, and cite them.
- **Go broad first** (many approaches, one or two lines each), then deep on the best three.
## Output
At most ~2,500 words, plain language, **ranked recommendations first**. For each of the top three:
- what changes, and where in this codebase (file and function);
- expected gain, with arithmetic from the numbers above;
- what it risks: determinism, quality, walkability;
- effort;
- **a cheap experiment that would prove it wrong** before anyone builds it fully.
Then the shorter list of other ideas, then any questions only the owner can answer (e.g. what "ready" must mean).
Mark claims as [CODE] (read in this repo), [MEASURED] (from the numbers above), [PRIOR ART] (cited) or
[INFERENCE].
@@ -0,0 +1,130 @@
# Astra 1, pass 2 — whole-chain review (gpt-6-astra, effort medium, read-only), 2026-10-27
Claude verified: PackedDensityAt (ShapeWorldInternal.h ~5909) applies ApplyResolvedStrateComposition outside the L76 term scope; FScopedDensityQueryProfile times whole invocations crossing a 256-point boundary (batch-biased).
**Stage-by-stage cost map**
[MEASURED] The L73 stage timings below are historical, before the later skips. They describe where that run spent time; they cannot be subtracted from L76 term timings to produce a current residual. I also inspected the saved L76 results without executing generation.
[CODE] A major architectural detail: the “compiled-analytic” world **already contains precomputed packed signs and quantized surface-distance proxies**. Many lattice queries never enter `AnalyticDensityAt`.
| Stage | Work and frequency | Cost and necessity |
|---|---|---|
| **World/plan resolution** | [CODE] Headless loads the owner export, resolves/validates its layout, constructs a snapshot, then constructs `World`. Once per cold run. UE’s serialization entry is [`VoxelForgeCoreAdapter.cpp:2062`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:2062); headless startup begins at [`StreamBenchmarkHeadless.cpp:1659`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkHeadless.cpp:1659). | [MEASURED] Saved L76 FI owner-data loading takes about **25 ms**. No complete isolated resolution cost is supplied. [INFERENCE] Necessary, but not a plausible home for worker time measured inside tile-field queries. |
| **Shape compilation** | [CODE] `World::compile` builds primitives/bins, packed rock intervals, candidate bands, detailed distance proxies, sign-edge repairs, passage validation and far parents. Once for the base shape; additional pages when needed. [`VoxelForgeCore.cpp:1394`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1394), [`ShapeWorldInternal.h:2724`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2724). | [MEASURED] Saved L76 FI: **313 ms wall**, including approximately 48 ms shapes, 64 ms intervals/signs, 62 ms surface band and 119 ms far parents. [INFERENCE] Important to a one-second cold budget, but separate from the missing tile-field attribution. It already amortizes substantial base-field work. |
| **Tile-key selection** | [CODE] `desiredTiles` builds clipmap keys, vertical bounds and additional far surface columns. Once initially, then on streaming-set changes; about 2,250 tiles for FI/TN. [`VoxelForgeCore.cpp:1904`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1904). | [INFERENCE] Thousands of key/set operations, not millions of field evaluations. No measured isolated cost. Necessary selection; exact tile count remains a design choice. |
| **Whole-tile classification** | [CODE] Each assembly tile calls `ClassifyBoxDetailed`, including a one-step halo. It delegates to `ClassifyBoundsDetailed`, with conditional slicing and conservative vetoes. [`VoxelForgeCore.cpp:2377`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2377), [`ShapeWorldInternal.h:6385`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6385). | [MEASURED] L73: only **4/3 ms total FI/TN**, roughly **2/1 μs per tile**, but ineffective legacy skips then. These are not current classifier costs. [INFERENCE] Necessary only when avoided work pays for it; retain current profitable proofs. |
| **Density dispatch and base evaluation** | [CODE] Scalar lattice queries probe native/legacy-surface paths, then choose packed lookup or analytic evaluation. Batches have additional fast paths. [`ShapeWorldInternal.h:3475`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3475), [`:3602`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3602), [`:5909`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5909). | [MEASURED] L73’s approximately 52M calls and 39.9/33.2 field worker-seconds imply roughly **0.77/0.64 μs per counted point**, averaged over heterogeneous paths. [INFERENCE] Fetching/evaluating the base value is necessary; repeated dispatch is not inherently necessary. |
| **Composer** | [CODE] Packed and scalar analytic paths both call `ApplyResolvedStrateComposition`: cavern graph → boundary → passage/support → stamps → compatibility floor caps. [`ShapeWorldInternal.h:1993`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1993). Frequency depends on the actual route, not just the strate label. | [MEASURED] L76 measures approximately **6.5/5.5 worker-seconds** only within its eligible analytic scopes. [CODE] Its passage/support bucket also includes `ApplyResolvedCavernFloorCaps` at [`:2053`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2053). [INFERENCE] Composition is necessary where influential; universal invocation is not. |
| **Grid fill** | [CODE] One cached lattice per generated tile: typically **35³ = 42,875** points at LOD0/1, including halo. Scalar, X8 or Z-run traversal fills it. Storage is thread-local and reused. [`Standalone mesher:430`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:430), [`:957`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:957), [`:1296`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1296). | [MEASURED] L73: **36.14/29.37 s**, approximately **16.1/13.2 ms per visited tile**, including evaluation. [INFERENCE] Dense fill is avoidable in proven-uniform regions. This is the primary workload, not merely array writes. |
| **Marching cubes** | [CODE] Classifies cells using cached corners, computes geometry only for surface cells, interpolates edges and deduplicates vertices. LOD0 midpoint correction can issue extra unclamped queries. [`Mesher:1879`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1879), [`:1943`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1943). | [MEASURED] L73 MC aggregate: **40.58/32.63 s**, including grid work. Subtracting grid gives about **4.44/3.26 s**, not another 40 seconds. [INFERENCE] Necessary extraction, but replacing MC is not the first lever. |
| **Normals** | [CODE] Central differences normally read the existing grid. At Step 1, proxy-band fallback values trigger `DensityAtUnclamped`; optional gradient memoization avoids repeated corner gradients. [`Mesher:1654`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1654). | [MEASURED] L73: **0.306/0.246 s**, about **0.14/0.11 ms per visited tile**. [INFERENCE] Some repeated field queries exist, but six new field evaluations per vertex is not the general path. Normals cannot plausibly explain 30 seconds under these measurements. |
| **Skirts** | [CODE] Boundary-edge detection, extrusion and density probes prevent unsuitable skirt geometry. Work scales with boundary geometry, not tile volume. [`Mesher:2111`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:2111). | [MEASURED] L73: **1.079/1.091 s**, approximately **0.48/0.49 ms per visited tile**. [INFERENCE] Necessary for the current seam solution; bounded opportunity, not the missing dominant cost. |
| **Ground/cap refinement and far shells** | [CODE] Far stratum/connector repairs and native ground/cap refinement run under eligibility gates, with refinement at LOD≥4 and no overlapping edits. [`VoxelForgeCore.cpp:2792`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2792), [`:2875`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2875). | [MEASURED] L73’s combined “Other,” including these and assembly, is **2.86/2.78 s**. [INFERENCE] Required by the current far representation, but partly corrective work that a better far representation could avoid. Not an explanation for missing grid time. |
| **Payload** | [CODE] Core assembles material/vertex/index outputs; the headless wrapper converts vertices and moves indices/masks. Per output vertex/index, once per tile. [`VoxelForgeCore.cpp:2969`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2969), [`StreamingWorld.h:934`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamingWorld.h:934). | [MEASURED] The separately timed headless conversion is **12 ms total**, about **5 μs per tile**. [INFERENCE] This does not include all Core assembly or Unreal upload/collision costs. It is not a useful primary target. |
| **Unreal publication** | [CODE] Worker generation → prepared streams → result queue → budgeted game-thread component/upload/section work → asynchronous collision completion. [`VoxelWorld.cpp:13303`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13303), [`:9400`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:9400), [`:14010`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:14010), [`:14279`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:14279). | [MEASURED] SW has **1.33 s additional readiness latency**. [INFERENCE] That is not 1.33 s of CPU publication work. Per-tile apply/cook costs are not established here. Essential to the target, but cannot explain headless field time. |
**Where the “unattributed 30 seconds” most plausibly went**
[CODE] **First: packed-field composition is outside L76’s measurement scope.** The relevant chain is:
`DensityAtLattice` → `PackedDensityAt` → `ApplyResolvedStrateComposition`.
`PackedDensityAt` reads `RockWords`, `CandidateWords` and possibly `SurfaceDistanceQ`, then applies composition. It never enters the `FScopedLegacyFieldTermScope` created later inside `AnalyticDensityAt` at [`ShapeWorldInternal.h:5967`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5967).
[CODE] Term timers immediately return when that scope is inactive: [`ShapeWorldInternal.h:2275`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2275). Thus an expensive passage query can be counted by the outer field profiler but entirely absent from L76’s passage timing.
[CODE] `DensityDispatchPathAt` returns “compiled-analytic/FloatingIslands” from the bound generator kind without distinguishing packed from analytic execution: [`ShapeWorldInternal.h:3455`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3455). That label is insufficient for performance attribution.
[CODE] There are further differences:
- X8 can return packed proxy values directly without invoking the scalar composer; other conditions make it fall back to scalar calls. [`ShapeWorldInternal.h:3675`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3675)
- FI LOD0 preferentially uses scalar vertical traversal. [`Mesher:1288`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1288)
- Z-run dispatch can fall back to scalar lattice queries, including packed queries. Its analytic term scope is reached only after those fallbacks. [`ShapeWorldInternal.h:11460`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:11460)
- Native/surface branches and non-FI/TN samples are excluded from the legacy-term population.
[INFERENCE] Therefore, the correction establishes that **6.5 seconds is measured passage cost within one subset**, not that total passage cost is only 15%. Packed composition is my leading explanation for substantial uninstrumented work. I cannot assign it 30 seconds from source alone.
[MEASURED] **Second: the residual mixes different generations of the workload.** Existing [L76 saved results](/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l76/term-profile-before/summary.json) contain:
| Same saved L76 run | FI | TN |
|---|---:|---:|
| Density point calls | 26.99M | 25.13M |
| Eligible legacy points | 6.01M | 5.56M |
| Reported field estimate | 18.19 worker-s | 15.49 worker-s |
| Tile-worker total | 33.80 worker-s | 28.56 worker-s |
| Whole-view wall | 4.621 s | 3.977 s |
[INFERENCE] The approximately 46M unseen calls belong to subtracting L76’s eligible count from L73’s pre-skip population. They are not an established current population. Likewise, `40−measured_terms` is not a coherent current residual.
[CODE] **Third: the aggregate field estimator can be biased by batching.** `FScopedDensityQueryProfile` samples when the point sequence crosses a multiple of 256, timing the entire invocation and recording its point count. The reducer pools sampled time/points and scales by all points. See [`ShapeWorldInternal.h:2355`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2355) and [`StreamBenchmarkPipeline.h:5611`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkPipeline.h:5611).
[INFERENCE] A batch of eight has approximately eight times a scalar invocation’s chance of being selected, and contributes eight sampled points when selected. Pooling both without inclusion-probability correction overrepresents batch points. If packed batches are cheap and scalar composition is expensive, the estimate can substantially understate field time. Periodic sampling also risks correlation with spatial traversal.
[MEASURED] This deserves attention: L76 FI LOD0 records **12.70 s grid fill but only 5.96 s estimated field evaluation**. Conversely, LOD3 estimates **3.04 s field time against 2.58 s total tile-worker time**. Those are diagnostic inconsistencies, not proof of where the discrepancy lies. [Saved log](/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l76/term-profile-before/strate1-w8.log:12880)
[INFERENCE] **Lower-ranked explanations:**
- **Dispatch/lookups:** real repeated work. Stratum lookup uses binary search and coordinate conversion, sometimes repeatedly per sample: [`ShapeWorldInternal.h:437`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:437). But attributing 30 seconds to this alone means roughly **0.65 μs per purported 46M calls**. That needs evidence; hidden composer execution is more concrete.
- **Allocation/cache misses:** possible contributors, but grid allocation is reused and packed access is direct indexing. There is no evidence of one heap allocation per ordinary density point.
- **Halos/neighbor duplication:** significant but bounded. `35³−33³ = 6,938`, or **16.2%** of a dense tile’s samples, is the gradient halo. Including shared faces, an ideal large same-LOD region could reduce `35³` toward `32³` unique samples per tile: approximately **23.6%** savings before edges and synchronization. This multiplies existing evaluation cost; it does not create a separate unmeasured stage.
- **Normals/midpoints/skirts:** issue additional calls, but their measured stage budgets exclude them as the dominant explanation. Cross-LOD duplication remains unquantified.
**Cheapest decisive measurement**
[INFERENCE] First take a sampling call-stack profile of the clean production run, focused on grid fill. It requires no new per-sample timers and should immediately distinguish packed composition from dispatch or memory stalls.
[INFERENCE] The next instrumentation change should be a **mutually exclusive dispatch census**, not another individual-term profiler. Count and sample time for packed-scalar, packed-X8, analytic-scalar, analytic-batch, native and legacy-surface routes, split by LOD and caller stage. Measure composition inside packed-scalar explicitly.
Use equal-probability invocation sampling with correct expansion, or stratify by route **and batch length**. Avoid nested double-counting; compare the resulting total against directly timed grid fill. Separate passage carving from compatibility floor caps.
[INFERENCE] This falsifies my leading explanation if packed-scalar composition occupies little time. Only then prioritize dispatch, cache misses or other overhead.
**Revised ranked recommendations**
**1. Establish complete attribution, then specialize the dominant lattice route.**
[INFERENCE] This replaces my unconditional “passages first” ranking. The first implementation round should produce a closed cost account and identify which route dominates LOD0/1.
If packed composition dominates, give each tile/brick a stable local composition program and an identity fast path. Extend `FScopedPassageTileQueries`, `PackedDensityAt` and `ApplyResolvedStrateComposition`. Hoist Z-only dispatch decisions into plane/run contexts. Before copying the X8 shortcut into scalar traversal, establish why their composition behavior differs and which semantics are required.
[INFERENCE] Effort: small for attribution, medium for specialization. Risks: incorrect support envelopes, changed composition order, scalar/batch disagreement. Preserve peer determinism and audit clearance, support and grade; old-version hashes remain an exactness diagnostic.
[MEASURED → INFERENCE] **Corrected passage arithmetic:** using your historical budget strictly as an illustration:
- FI, removing 80–90% of **6.5/46** worker-seconds: `4.6 × (1−0.8…0.9 × 6.5/46)` = **4.08–4.02 s**.
- TN, removing 80–90% of **5.5/39**: approximately **3.81–3.75 s**.
These replace my erroneous approximately two-second estimates. They are not forecasts because the operands span rounds. Using the contemporaneous saved L76 totals instead gives approximately **3.91–3.83 s FI**, **3.37–3.29 s TN**, under proportional scaling.
[INFERENCE] Total passage savings could be larger if the missing packed-route work is substantial. Measure it before assigning a percentage.
**2. Remove repeated composed sampling; retain MC.**
[INFERENCE] Keep the retained-proof/local-program recommendation, but build on the existing packed representation. Priorities:
- Retain successful slice/brick proofs rather than requiring a uniform whole-tile verdict.
- Reuse final composed lattice samples across neighboring tiles where inputs and semantics match.
- Evaluate exact surface endpoints and normal stencils; leave certified interiors implicit.
The relevant changes remain `ClassifyBoundsDetailed`, `World::generateTile` and the standalone mesher’s grid fill. Sample-cache keys must include evaluator version, edits and relevant resolution/context—not coordinates alone.
[INFERENCE] An alternative worth testing is applying composition once to a shared near-region lattice. Count its cold construction cost: moving the same work earlier is not a speedup. It wins through deduplication, localization or cheaper batching.
[INFERENCE] Effort: medium-to-large. Risks: stale edit dependencies, seams and false uniform proofs. Cheap falsifier: one representative region spanning adjacent tiles and a strate boundary; compare total construction plus meshing cost against independent tiles.
[INFERENCE] Withdraw the earlier combined one-second projection. At unchanged throughput, FI’s current 4.6 seconds requires approximately **78% critical-path reduction**. Border sharing alone cannot supply that; broad sampling elimination still has to prove its economics.
**3. Preserve a separate arrival deadline and reduce end-to-end latency.**
[INFERENCE] Keep dependency-aware scheduling through collision publication, rather than merely increasing queue depth. The existing near-priority and hot-swap mechanisms already provide the foundation.
Shape compilation’s approximately 0.31 seconds matters once generation approaches one second. Far repair/refinement becomes material too. Optimize these after the dominant grid route is understood; do not hide their cost outside the cold timer.
[MEASURED → INFERENCE] SW’s scheduling gap remains a possible **0–1.33-second** latency opportunity, not a guaranteed CPU saving. Scheduling cannot remove required whole-view work.
[INFERENCE] Effort: medium. Risks: starving far coverage or publishing visuals before safe collision. Cheap falsifier: an arrival-only admission experiment plus prerequisite/publication tracing. If its uncontended critical path exceeds one second, scheduling alone cannot meet arrival readiness.
[INFERENCE] GPU porting, SIMD, native-generator replacement and a new mesher stay below these three. The newly visible distinction is between **cheap cached base density and potentially expensive repeated composition**, not simply “52M analytic evaluations.”
Read-only inspection completed; no edits, builds, tests, commits or stashes.
@@ -0,0 +1,137 @@
# Astra 1 — design review (gpt-6-astra, effort low, read-only), 2026-10-27
Brief: see BRIEF.md. Note from Claude: the brief's "~75% of field time is the passage composer" was WRONG — L76's term profiler covered ~6.0M of ~52M density calls per view (FI); measured passage cost is ~6.5 of ~40 worker-s (~15%). Astra flagged the scope mismatch itself. Its gain arithmetic in rec. 1 inherits the wrong share.
**[INFERENCE] Ranked recommendations:**
1. **Compile passages into spatially local operations.** Make most samples bypass the composer entirely; simplify the remaining work.
2. **Replace repeated block classification with a retained hierarchy of proofs and local field programs.** Keep marching cubes; stop densely evaluating regions that cannot produce geometry.
3. **Give arrival readiness its own deadline-driven pipeline.** Preserve whole-view completion as a separate benchmark; scheduling improvements alone cannot meet it.
**[INFERENCE]** These are complementary. Passage localization is the strongest first investment, but does not plausibly deliver one second alone. A wholesale mesher replacement, GPU rewrite, or native-generator migration is premature.
### 1. Make passage influence local by construction
**[CODE]** There is already a spatial index: `BuildPassageFeatureTileList` queries passage feature bounds. `FScopedPassageTileQueries` turns route candidates into per-passage flags, while retaining segment lists for mouth support. Nevertheless, `ApplyResolvedStratePassageCarves` still iterates all passages and scans crossings. After finding a route distance, it calculates relief and mouth-related terms. See [ShapeWorldInternal.h](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:989).
**[INFERENCE] What changes:** compile a small, immutable passage program for each spatial region:
- Empty region: no passage invocation.
- Route region: ordered candidate segments, precomputed geometry and distances along the route.
- Mouth/support region: explicit support, transition-fade and later-bore dependencies.
- Overlapping features: retain the original composition order.
Use `BuildPassageComposerConstants`, `BuildPassageFeatureTileList`, `FScopedPassageTileQueries`, and `ApplyResolvedStratePassageCarves`; connect the result through `World::generateTile` in [VoxelForgeCore.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1997). This is stronger than filtering one loop: the region program should remove entire categories of work.
**[INFERENCE] Exact culling requires an identity proof**, not merely “the tube does not intersect this tile.” For every omitted operation, prove that it leaves the incoming density unchanged over the query domain. Include:
- Spline extrema, variable radius, wall/floor relief and smooth-blend reach.
- Mouth shaping, support treads, columns, boundary fade and dependencies on other passages.
- All sample positions used by normals, borders and skirts.
- Deterministic ordering and tie-breaking among retained features.
A feature can leave density signs unchanged while altering marching-cubes interpolation or normals. Conversely, changing far-away density magnitudes may be harmless. These are different correctness claims.
**[CODE]** L75 already corrected bounds for passage relief and spline extrema. The composer also conditionally restores support and defers support composition. Therefore, baking one distance to the nearest tube would not reproduce its semantics. [L75 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux75/REPORT.md:22)
**[INFERENCE]** If exact influence is awkward or effectively nonlocal, introduce a versioned, compact-support passage operator: identical in the protected walkable corridor and mouth regions, smoothly becoming identity outside an explicit envelope. That is a legitimate world-generation change under your constraints.
**[MEASURED → INFERENCE] Expected gain:** passage work is approximately `0.75 × 40 = 30` of FI’s 46 worker-seconds, and `0.75 × 33 = 24.75` of TN’s 39. Removing 80–90% gives proportional wall-time scenarios:
| View | 80% passage reduction | 90% passage reduction |
|---|---:|---:|
| FI | `4.6 × (46−24)/46 = 2.20 s` | `4.6 × (46−27)/46 = 1.90 s` |
| TN | `4.3 × (39−19.8)/39 = 2.12 s` | `4.3 × (39−22.275)/39 = 1.84 s` |
These assume critical-path time follows work reduction; they are not predictions for the Ryzen. Even eliminating passage cost entirely leaves approximately **1.6 seconds** in both views.
**[INFERENCE] Risks and effort:** medium effort for regional programs; larger for a new passage representation. Stable candidate ordering and versioned arithmetic protect determinism. Underestimated envelopes threaten clearance and support; overly generous envelopes merely lose speed. Run capsule, grade and support audits across entire passages, including landings and seams.
**[INFERENCE] Cheap falsifier:** resurrect the rejected culling candidate as an experiment. First inspect all 612 changed samples and 24 changed mesh tiles: location, zero crossings, surface displacement, normals, support and audit outcomes. Then time the production path including candidate construction. Reject this implementation if conservative lists remain large or composer savings are small. Hash differences alone are neither acceptance nor rejection.
### 2. Retain spatial proofs and simplify the field inside each region
**[INFERENCE]** A continuous density function remains a good world definition. Evaluating its complete program at every lattice point is the questionable implementation. Marching cubes is not currently the dominant problem.
**[CODE]** `ClassifyBoxDetailed` delegates to `ClassifyBoundsDetailed`. The latter already performs conditional strate slicing, but combines slices into a whole-box verdict and abandons the skip when a slice is unknown or has a different sign. L75’s legacy block certificates became audit-only after timing regressions. Simply enabling more block checks repeats a failed approach. [Classifier](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6385), [L75 final disposition](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux75/REPORT.md:64)
**[INFERENCE] What changes:** introduce a compact region tree or brick table whose entries retain:
- A solid/air certificate, or an unresolved range.
- Relevant primitive and passage candidates.
- Terms proven inactive or dominated.
- Child regions requiring further evaluation.
Split first at known strate and transition-band boundaries, then refine unresolved regions spatially. Keep successful proofs even if a neighboring slice fails. Share candidate filtering and bounds down the hierarchy; do not repeatedly invoke the full classifier from scratch.
Start tile-local and retain the regular marching-cubes lattice. Extend `ClassifyBoundsDetailed`, the evaluation feeding `AnalyticDensityAt`, `World::generateTile`, and the grid/proof path in `UVoxelMarchingCubesMesher::GenerateMesh` in [VoxelMarchingCubesMesher.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:102).
**[PRIOR ART]** Fidget combines interval evaluation with regional expression simplification: even unresolved regions can use shorter programs. That second benefit matters here as much as empty-space skipping. It also supports bulk SIMD evaluation. [Fidget’s author documentation](https://www.mattkeeter.com/projects/fidget/)
**[INFERENCE]** Do not assume the composed density is a signed distance field with Lipschitz constant one. Warps, blends and relief require their actual bounds. Uniform corners do not prove a uniform interior.
Similarly, do not fill certified interiors with arbitrary constants and then use those constants at surface edges or in normal stencils. Evaluate the exact endpoint and halo values wherever geometry or gradients consume them. A narrow band must be *discovered conservatively*, not guessed from sparse signs.
**[MEASURED → INFERENCE] Expected gain:** the 90–97% uniform-block sample census is an opportunity ceiling, not a realizable skip rate. A useful experiment target is **50–70% reduction of remaining field work, net of classification cost**.
After a 90% passage reduction, FI has approximately 13 field-work units plus 6 others. Reducing those 13 by 50–70% gives:
`4.6 × [6 + 13 × (0.5 to 0.3)] / 46 = 1.25 to 0.99 s`.
TN similarly gives approximately **1.25–1.02 seconds**. Thus the combination approaches the headless target, with little budget left for Unreal overhead. Do not multiply independent headline speedups: both techniques remove overlapping work.
**[INFERENCE] Risks and effort:** medium-to-large effort, substantially less than adaptive meshing with new topology. Incorrect proofs can silently delete geometry; edits must invalidate affected certificates, including their dependency halos. Quality and walkability can remain unchanged if surface samples and stencils remain exact. Unknown bounds must fall back.
**[INFERENCE] Cheap falsifier:** implement one tile-local hierarchy for representative boundary-crossing and passage-adjacent tiles, retaining the existing mesher. Compare total classification-plus-fill cost against dense evaluation, after passage localization. Reject expansion if net field savings fall well below 50%, or bookkeeping and conservative halos consume the benefit. The prior slow block experiment makes this an essential early gate.
### 3. Schedule for an explicit arrival deadline
**[MEASURED]** SW’s Unreal overhead is `3.74−2.41 = 1.33 s`; walkable readiness ranges from 0.4 to 1.8 seconds.
**[INFERENCE]** Keep two separate commitments:
- **Arrival ready:** safe collision and sufficient visible terrain for immediate movement.
- **Whole view complete:** the full requested set at final quality.
If the owner insists both happen within one second, say so explicitly. Progressive refinement cannot be counted as meeting the second commitment.
**[CODE]** §8.10 already specifies occupied/support-tile priority, heading priority, dynamic near reserve, independent region admission, cancellation, bounded result backlog and hot-swap. “Make nearby tiles higher priority” is therefore not a new recommendation. [Performance invariants](/home/jahni/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:572)
**[INFERENCE] What changes:** make readiness a dependency graph with deadlines: required region compilation → field/mesh → collision preparation → bounded publication. Prioritize prerequisites of the arrival set, not just its tile jobs. Reserve capacity through publication, and schedule travel-ahead work by time-to-arrival.
Inspect and change `AVoxelWorld::UpdateChunksAroundPosition`, `LoadTile`, and `ProcessPendingChunks` in [VoxelWorld.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:9400). Preserve the existing epoch, cancellation and collision-handoff rules. Refinement should retain covering geometry until replacements are ready.
**[PRIOR ART]** Voxel Plugin separates collision/navmesh demand through invokers. Geometry clipmaps use incremental updates, coarse-to-fine processing and continuity between levels. These support separate readiness domains, not a universal one-second guarantee. [Voxel Plugin](https://docs.voxelplugin.com/2.0p6/knowledgebase/blueprints/collision-navmesh-and-invokers), [Geometry clipmaps](https://developer.nvidia.com/gpugems/gpugems2/part-i-geometric-complexity/chapter-2-terrain-rendering-using-gpu-based-geometry)
**[MEASURED → INFERENCE] Expected gain:** removing half the observed scheduling gap saves about **0.67 seconds**; removing all of it still leaves SW at 2.41 seconds before Core changes. Improving a 1.8-second walkable arrival to one second requires eliminating or deferring 0.8 seconds from that critical path.
**[INFERENCE] Risks and effort:** small-to-medium for dependency scheduling; larger for new far representations. Timing must never change generation inputs. Coarse collision can violate walkability; coarse visuals can pop, particularly in VR. Preserve authoritative near collision and accept temporary visual substitutes only if they satisfy the owner’s quality criterion.
**[INFERENCE] Cheap falsifier:** first replay existing job timings against the dependency graph, then try an arrival-only admission experiment. If prerequisites plus bounded publication already exceed one second without competing work, scheduling cannot solve arrival latency. If the coarse cover visibly degrades the arrival, it cannot redefine “ready.”
**Other approaches, in priority order**
- **[INFERENCE] Bake passage geometry, not necessarily final density.** Precompute spline coefficients, arc lengths, frames and conservative segment bounds first. Sparse sampled bricks are a later option; retain separate carve/support operands and conditions. Count baking in cold latency. Interpolated distance alone can erase treads or alter mouths.
- **[INFERENCE] SIMD after localization.** AVX2 evaluation of batches sharing a local program is realistic; divergent all-passage loops are a poor starting point. Eight lanes do not imply eightfold speedup. Specify arithmetic and reduction order across scalar/vector paths.
- **[INFERENCE] Sample sharing.** Share same-LOD borders first; cross-LOD reuse requires identical coordinates *and field semantics*. Nested lattices overlap, but clipmap shells need not cover the same volume. Compare synchronization cost against duplicate evaluation before building a global cache.
- **[INFERENCE] Coarse smooth terms.** Interpolate bounded macro terms while evaluating fine relief near surfaces. Density error does not imply equal position error where gradients are weak. L76’s 0.01-µs noise row is too small to justify making noise approximation the main project.
- **[INFERENCE] GPU.** A specified integer/fixed-point evaluator can be deterministic: define rounding, overflow, square roots, interpolation and stable emission order. Quantizing floating-point results afterward is insufficient. CPU-authoritative collision plus GPU display is easier, provided visual disagreement is bounded and display results never affect gameplay. GPU readbacks and VR contention can erase gains.
- **[PRIOR ART → INFERENCE]** HLSL `precise` controls arithmetic refactoring; it is not a complete cross-vendor world-generation specification. Prototype one bounded integer passage kernel before reopening a full port. [Microsoft documentation](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/precise)
- **[PRIOR ART → INFERENCE] Alternative renderers.** Dreams’ published architecture evaluates CSG into multiresolution point clouds; Teardown uses a voxel renderer without polygons. These demonstrate representation choices, not transferable cold-generation timings. Nanite addresses rendering downstream of field generation. [Dreams presentation](https://www.advances.realtimerendering.com/s2015/index.html), [Teardown FAQ](https://www.teardowngame.com/faq.html)
- **[PRIOR ART → INFERENCE] Continuous streaming.** No Man’s Sky’s architecture is explicitly organized around continuous generation. That supports travel-ahead planning, but does not prove cold teleport latency. Disk caching likewise remains useful without counting toward this target. [Hello Games’ GDC talk](https://www.youtube.com/watch?v=sCRzxEEcO2Y)
**Process changes that matter**
**[INFERENCE]** Separate three gates: peer determinism for the *new version*; gameplay/visual acceptance against the previous version; and performance on the production path. Old-version hash identity should only gate changes advertised as exact.
**[CODE]** L76’s term profiler covers selected legacy evaluations; its passage total is about 6.3 worker-seconds, whereas L73 reports broader grid-field totals. Those scopes must not be silently equated. The arithmetic above uses your supplied aggregate attribution as a scenario, not reconciled absolute profiling data. [L76 profiler scope](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux76/REPORT.md:5)
**[INFERENCE]** Report cold-start boundaries, critical-path stages, worker occupancy, publication latency and worst frame time alongside whole-view time. Include plan compilation and fresh in-memory structures. Validate the winning prototype on the 5800X3D; no reliable conversion follows from the laptop measurements alone.
**Questions only the owner can settle**
- Must every final-quality tile exist at one second, or can safe arrival precede complete refinement?
- What travel speed, teleport distance and VR frame budget must streaming sustain?
- Does “cold” include world planning, and must supported peers reproduce density bits, mesh bits, or an explicitly defined gameplay representation?
Read-only review completed; no edits, builds, tests, commits or stashes.
@@ -4,9 +4,11 @@ import argparse
import importlib.util
import math
from collections import defaultdict, deque
import os
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")) / "baselines"
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH)
PROFILE = importlib.util.module_from_spec(SPEC)
@@ -502,8 +504,8 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--out", type=Path, default=ROOT / "Saved/Linux/l37")
parser.add_argument("--baseline-out", type=Path,
default=ROOT / "Saved/Linux/l37/no-tread-control")
parser.add_argument("--control-out", type=Path, default=ROOT / "Saved/Linux/l36")
default=BASELINES / "mouth/l37-no-tread")
parser.add_argument("--control-out", type=Path, default=BASELINES / "mouth/l36")
parser.add_argument("--scene", choices=tuple(SCENES) + ("all",), default="all")
args = parser.parse_args()
out = args.out if args.out.is_absolute() else ROOT / args.out
@@ -15,6 +15,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT",
os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")))
BASELINES = OUT / "baselines"
BIN = OUT / "bin"
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
OUT / "captures"))
@@ -23,10 +24,12 @@ HEADLESS = Path(os.environ.get("VOXELFORGE_L37_HEADLESS",
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
BIN / "VoxelForgeOffscreenCapture-l37-final-clang"))
PNG_DIR = OUT / "capture-png"
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
ROOT / "Standalone/data/owner_world.json"))
RECIPE_INPUTS = (
("owner-fi", ROOT / "Standalone/data/owner_world.json",
("owner-fi", OWNER,
"floating-islands", "FloatingIslands", "upper"),
("owner-sw", ROOT / "Standalone/data/owner_world.json",
("owner-sw", OWNER,
"surface-world", "SurfaceWorld", "lower"),
("style-override-upper-sw-fixture-style0",
OUT / "fixtures/style-override-upper-sw-fixture-style0.json",
@@ -136,7 +139,7 @@ def fmt(values):
def load_reference_cameras():
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
log = BASELINES / "capture/l34-standard-set-cameras.baseline"
pattern = re.compile(
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
@@ -2,6 +2,7 @@
"""Run five interleaved whole-view cost pairs against the L36 Clang binary."""
import csv
import math
import os
import re
import statistics
import subprocess
@@ -10,6 +11,8 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l37"
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
ROOT / "Standalone/data/owner_world.json"))
BINARIES = {
"l36": ROOT / "Saved/Linux/l36/bin/VoxelForgeCoreHeadless-l36-final-clang",
"l37": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
@@ -30,7 +33,7 @@ for recipe in ("surface-world", "floating-islands"):
command = [
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"timeout", "900s", str(BINARIES[build]),
str(ROOT / "Standalone/data/owner_world.json"),
str(OWNER),
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
"--stream-pipeline", "--force-generate", "--clip-radius=3",
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Check L25, 24 m crossing and upper-SW fixture determinism for L37."""
import csv
import os
import re
import subprocess
import time
@@ -9,7 +10,8 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l37"
OWNER = ROOT / "Standalone/data/owner_world.json"
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
ROOT / "Standalone/data/owner_world.json"))
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
BINARIES = {
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
import re
import os
import subprocess
import time
from pathlib import Path
@@ -48,7 +49,8 @@ def run(name, binary, data, recipe, extra, checks):
failed |= not ok
owner = ROOT / "Standalone/data/owner_world.json"
owner = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
ROOT / "Standalone/data/owner_world.json"))
for recipe in ("surface-world", "floating-islands"):
for compiler, binary in BINARIES.items():
name = f"invariant-route-proof-{recipe}-{compiler}"
@@ -8,9 +8,11 @@ import json
import math
import re
import argparse
import os
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")) / "baselines"
BASE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37/RunMouthFragmentAuditL37.py"
SPEC = importlib.util.spec_from_file_location("mouth_fragment_l37", BASE_PATH)
BASE = importlib.util.module_from_spec(SPEC)
@@ -184,8 +186,8 @@ def main():
parser.add_argument("--capture-log-sw", type=Path)
args = parser.parse_args()
paths = {
"L35": ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh",
"L36": ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh",
"L35": BASELINES / "mouth/l35/owner-fi-ground-mouth.vfmesh",
"L36": BASELINES / "mouth/l36/owner-fi-ground-mouth.vfmesh",
"L37": args.candidate if args.candidate.is_absolute()
else ROOT / args.candidate,
}
@@ -11,9 +11,11 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37"))
BASELINES = OUT / "baselines"
BIN = Path(os.environ.get("VOXELFORGE_VERIFY_HEADLESS",
OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang"))
OWNER = ROOT / "Standalone/data/owner_world.json"
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
ROOT / "Standalone/data/owner_world.json"))
TERRACE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
RING_SET = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py"
STAMP = re.compile(
@@ -176,9 +178,9 @@ for label, recipe, surface, window_min, window_max, camera0, camera1, centers in
# Keep the established L30 terrace and L31 flat-surface negative controls live.
controls = (
("l30-terrace", ROOT / "Saved/Linux/l32/fi-floor-l30-camera-negative.vfmesh",
("l30-terrace", BASELINES / "ring/l32-fi-floor-l30-camera-negative.vfmesh",
-130.0, -110.0),
("l31-flat", ROOT / "Saved/Linux/l31/fi-floor-v19.vfmesh", -122.0, -117.0),
("l31-flat", BASELINES / "ring/l31-fi-floor-v19.vfmesh", -122.0, -117.0),
)
for name, mesh, height_min, height_max in (() if OPTIONS.skip_controls else controls):
result, output, _ = run(f"control-{name}", ["python3", TERRACE, mesh,
@@ -14,6 +14,7 @@ import time
ROOT = Path(__file__).resolve().parents[4]
BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l47")) / "baselines"
VERIFY = ROOT / "Standalone/Tools/verify"
LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37"
sys.path.insert(0, str(VERIFY))
@@ -177,8 +178,8 @@ if not mesh.is_file() or mesh.stat().st_size == 0:
entrance_audit = ROOT / "Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py"
run_logged("entrance-floor-control-l46", [sys.executable, str(entrance_audit),
"--mesh", str(ROOT / "Saved/Linux/l46/landmark-l46.vfmesh"),
"--layout", str(ROOT / "Saved/Linux/l46/layouts/landmark-reachability-clang.json"),
"--mesh", str(BASELINES / "landmarks/l46-entrance-control.vfmesh"),
"--layout", str(BASELINES / "landmarks/l46-entrance-control-layout.json"),
"--out", str(ARTIFACT_DIR / "entrance-profile-control-l46.json"), "--expect=fail"])
run_logged("entrance-floor-audit", [sys.executable, str(entrance_audit),
"--mesh", str(mesh), "--layout", str(LAYOUT_PATH),
@@ -6,11 +6,13 @@ import importlib.util
import argparse
import json
import math
import os
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parents[4]
BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l47")) / "baselines"
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
spec = importlib.util.spec_from_file_location("ring_profile", PROFILE_PATH)
if spec is None or spec.loader is None:
@@ -21,9 +23,9 @@ spec.loader.exec_module(PROFILE)
ARTIFACTS = ROOT / "Saved/Linux/l47"
PARSER = argparse.ArgumentParser(description=__doc__)
PARSER.add_argument("--layout", type=Path,
default=ROOT / "Saved/Linux/l46/layouts/landmark-reachability-clang.json")
default=BASELINES / "landmarks/l46-entrance-control-layout.json")
PARSER.add_argument("--mesh", type=Path,
default=ROOT / "Saved/Linux/l46/landmark-l46.vfmesh")
default=BASELINES / "landmarks/l46-entrance-control.vfmesh")
PARSER.add_argument("--out", type=Path, default=ARTIFACTS / "entrance-profile-before.json")
PARSER.add_argument("--expect", choices=("pass", "fail", "measure"), default="measure")
PARSER.add_argument("--lod", default="0", help="LOD level, or 'all' to include every exported level")
@@ -9,7 +9,8 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OWNER = ROOT / "Standalone/data/owner_world.json"
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
ROOT / "Standalone/data/owner_world.json"))
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l56"))
@@ -1,259 +0,0 @@
#!/usr/bin/env python3
"""Run L54's triangle audit over L59's open-shaft lattice and controls."""
from __future__ import annotations
import argparse
import importlib.util
import json
import math
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
AUDIT54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/AuditWholeSurfaceL54.py"
spec = importlib.util.spec_from_file_location("whole_surface_l54", AUDIT54)
audit54 = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(audit54)
original_make_route = audit54.make_route
def build_segment_bvh(segments: list[tuple]) -> dict:
def build(indices: list[int]) -> dict:
minimum = tuple(min(min(segments[index][0][axis], segments[index][1][axis])
for index in indices) for axis in range(3))
maximum = tuple(max(max(segments[index][0][axis], segments[index][1][axis])
for index in indices) for axis in range(3))
if len(indices) <= 8:
return {"minimum": minimum, "maximum": maximum,
"segments": tuple(sorted(indices)), "left": None, "right": None}
axis = max(range(3), key=lambda value: maximum[value] - minimum[value])
indices.sort(key=lambda index: (segments[index][0][axis]
+ segments[index][1][axis]) * 0.5)
middle = len(indices) // 2
return {"minimum": minimum, "maximum": maximum, "segments": None,
"left": build(indices[:middle]), "right": build(indices[middle:])}
if not segments:
raise ValueError("lattice audit route has no measurable segments")
return build(list(range(len(segments))))
def make_lattice_route(layout: dict, phase_m: float = 0.0) -> dict:
edges = sorted((edge for edge in layout.get("cavern_edges", [])
if edge["stable_id"] in ("lattice/spine/00-arrival-portal-v1",
"lattice/spine/01-helix-ramp-v1", "lattice/spine/99-departure-portal-v1")
or edge["stable_id"].startswith("lattice/spine/maze-edge-")),
key=lambda edge: (0 if edge["stable_id"].startswith("lattice/spine/00-") else
1 if edge["stable_id"].startswith("lattice/spine/01-") else
2 if edge["stable_id"].startswith("lattice/spine/maze-edge-") else 3,
edge["stable_id"]))
if not edges:
raise ValueError("lattice audit needs its resolved mandatory spine edges")
segments = []
routes = []
for edge_index, edge in enumerate(edges):
points = [[value / 1_000_000.0 for value in row]
for row in edge.get("polyline_um", [])]
if len(points) < 2:
raise ValueError(f"{edge['stable_id']}: explicit path is missing")
radius = edge["bore_radius_um"] / 1_000_000.0
height = edge["bore_height_um"] / 1_000_000.0
support_depth = edge.get("floor_support_depth_um", 0) / 1_000_000.0
horizontal_s = [0.0]
for a, b in zip(points, points[1:]):
horizontal_s.append(horizontal_s[-1] + ((b[0] - a[0]) ** 2
+ (b[1] - a[1]) ** 2) ** 0.5)
first = points[1]
first_dx, first_dy = first[0] - points[0][0], first[1] - points[0][1]
first_length = max(1.0e-9, (first_dx * first_dx + first_dy * first_dy) ** 0.5)
extension = (points[0][0] - first_dx / first_length * audit54.OPEN_GROUND_EXTENSION_M,
points[0][1] - first_dy / first_length * audit54.OPEN_GROUND_EXTENSION_M,
points[0][2] - (first[2] - points[0][2]) / first_length
* audit54.OPEN_GROUND_EXTENSION_M)
route_points = [extension, *points]
route_s = [-audit54.OPEN_GROUND_EXTENSION_M, *horizontal_s]
for index, (a, b) in enumerate(zip(route_points, route_points[1:])):
segments.append((a, b, route_s[index], route_s[index + 1],
radius, height, edge_index))
routes.append({"edge": edge, "points": points, "radius": radius,
"height": height, "support_depth": support_depth,
"length": horizontal_s[-1], "index": edge_index})
# The shaft core is an open cylindrical volume, not a walkable room. Its
# axis-aligned bounds include unrelated rock outside the cylinder and
# above the ramp headroom; the helix corridor already includes the inner
# wall, lip, foot, and underside where those surfaces meet the route.
rooms = [{"room": room,
"center": tuple(value / 1_000_000.0 for value in room["center_um"]),
"half": tuple(value / 1_000_000.0 for value in room["half_extents_um"]),
"floor": room["floor_um"] / 1_000_000.0}
for room in sorted(layout.get("cavern_rooms", []), key=lambda row: row["stable_id"])
if room.get("stable_id") != "lattice/spine/shaft-core-v2"]
return {"segments": segments, "edges": routes, "rooms": rooms, "phase_m": phase_m,
"segment_bvh": build_segment_bvh(segments)}
def make_route(layout: dict, phase_m: float = 0.0) -> dict:
if any(edge.get("stable_id", "").startswith("lattice/")
for edge in layout.get("cavern_edges", [])):
return make_lattice_route(layout, phase_m)
return original_make_route(layout, phase_m)
def lattice_route_nearest(point: tuple[float, float, float], route: dict):
"""Resolve stacked helix turns in 3D, then measure corridor radius in XY."""
best = None
def consider(segment_index: int) -> None:
nonlocal best
a, b, s0, s1, radius, height, edge_index = route["segments"][segment_index]
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
length_squared = dx * dx + dy * dy + dz * dz
t = 0.0 if length_squared <= 1.0e-12 else max(0.0, min(1.0,
((point[0] - a[0]) * dx + (point[1] - a[1]) * dy
+ (point[2] - a[2]) * dz) / length_squared))
x, y, z = (a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
distance3d_squared = ((point[0] - x) ** 2 + (point[1] - y) ** 2
+ (point[2] - z) ** 2)
if (best is None or distance3d_squared < best[0]
or (distance3d_squared == best[0] and segment_index < best[1])):
horizontal_length = math.hypot(dx, dy)
lateral = ((dx * (point[1] - y) - dy * (point[0] - x))
/ horizontal_length if horizontal_length > 1.0e-9 else 0.0)
best = (distance3d_squared, segment_index,
math.hypot(point[0] - x, point[1] - y),
s0 + (s1 - s0) * t, z, lateral, radius, height, edge_index,
route["edges"][edge_index]["length"])
def lower_bound_squared(node: dict) -> float:
distance_squared = 0.0
for axis in range(3):
gap = (node["minimum"][axis] - point[axis]
if point[axis] < node["minimum"][axis]
else point[axis] - node["maximum"][axis]
if point[axis] > node["maximum"][axis] else 0.0)
distance_squared += gap * gap
return distance_squared
def visit(node: dict) -> None:
if best is not None and lower_bound_squared(node) > best[0]:
return
if node["segments"] is not None:
for segment_index in node["segments"]:
consider(segment_index)
return
left, right = node["left"], node["right"]
left_bound, right_bound = lower_bound_squared(left), lower_bound_squared(right)
if left_bound <= right_bound:
visit(left)
visit(right)
else:
visit(right)
visit(left)
visit(route["segment_bvh"])
if best is None:
raise ValueError("lattice audit route has no measurable segments")
return best[2:]
def mark_lattice_region(triangles: list, route: dict) -> None:
margin = audit54.CORRIDOR_MARGIN_M
extension = audit54.OPEN_GROUND_EXTENSION_M
bounds = []
for edge in route["edges"]:
points = edge["points"]
radius, height = edge["radius"], edge["height"]
support_depth = edge["support_depth"]
bounds.append((
min(point[0] for point in points) - radius - margin,
max(point[0] for point in points) + radius + margin,
min(point[1] for point in points) - radius - margin,
max(point[1] for point in points) + radius + margin,
min(point[2] for point in points) - support_depth - margin,
max(point[2] for point in points) + height + margin))
for room in route["rooms"]:
center, half, floor = room["center"], room["half"], room["floor"]
bounds.append((center[0] - half[0] - margin, center[0] + half[0] + margin,
center[1] - half[1] - margin, center[1] + half[1] + margin,
floor - margin, floor + 2.0 * half[2] + margin))
for triangle in triangles:
x, y, z = triangle.center
if not any(low_x <= x <= high_x and low_y <= y <= high_y
and low_z <= z <= high_z
for low_x, high_x, low_y, high_y, low_z, high_z in bounds):
triangle.in_region = False
triangle.reference = False
continue
nearest = lattice_route_nearest(triangle.center, route)
distance, distance_s, floor_z, lateral, radius, height, edge_index, edge_length = nearest
support_depth = route["edges"][edge_index]["support_depth"]
corridor = (-extension <= distance_s <= edge_length
and distance <= radius + margin
and floor_z - support_depth - margin <= z <= floor_z + height + margin)
room_box = any(abs(x - room["center"][0]) <= room["half"][0] + margin
and abs(y - room["center"][1]) <= room["half"][1] + margin
and room["floor"] - margin <= z
<= room["floor"] + 2.0 * room["half"][2] + margin
for room in route["rooms"])
triangle.in_region = corridor or room_box
inside_room = any(abs(x - room["center"][0]) <= room["half"][0] + 0.45
and abs(y - room["center"][1]) <= room["half"][1] + 0.45
for room in route["rooms"])
outside_bore = distance > radius + 0.45 and not inside_room
triangle.reference = (triangle.in_region and outside_bore
and triangle.smooth[2] >= 0.45)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--case", nargs=5, action="append", required=True,
metavar=("LABEL", "MESH", "LAYOUT_OR_DASH", "EXPECT", "FOCUS"))
parser.add_argument("--json", type=Path, required=True)
parser.add_argument("--svg-dir", type=Path)
parser.add_argument("--phase", type=float, choices=(-0.5, 0.0, 0.5), default=0.0)
args = parser.parse_args()
audit54.make_route = make_route
results = []
for label, mesh, layout, expect, focus in args.case:
if expect not in ("pass", "fail", "measure") or focus not in ("whole", "floor"):
parser.error("EXPECT must be pass/fail/measure and FOCUS must be whole/floor")
native_lattice = layout != "-" and any(
edge.get("stable_id", "").startswith("lattice/")
for edge in json.loads(Path(layout).read_text(encoding="utf-8")).get(
"cavern_edges", []))
original_margin = audit54.CORRIDOR_MARGIN_M
original_extension = audit54.OPEN_GROUND_EXTENSION_M
original_nearest = audit54.route_nearest
original_mark_region = audit54.mark_region
if native_lattice:
# Keep the whole-surface test on native records: one core voxel of
# tolerance captures their meshed walls while excluding the separate
# legacy transition above the resolved mouth.
audit54.CORRIDOR_MARGIN_M = 0.25
audit54.OPEN_GROUND_EXTENSION_M = 0.0
audit54.route_nearest = lattice_route_nearest
audit54.mark_region = mark_lattice_region
try:
results.append(audit54.audit_case(label, Path(mesh), Path(layout), expect,
focus, args.svg_dir, args.phase))
finally:
audit54.CORRIDOR_MARGIN_M = original_margin
audit54.OPEN_GROUND_EXTENSION_M = original_extension
audit54.route_nearest = original_nearest
audit54.mark_region = original_mark_region
if native_lattice:
print("[L59LatticeAuditScope] margin_m=0.250 ramp_underside_depth_m=1.000 "
"outside_mouth_extension_m=0.000 nearest_centerline=3D "
"radius_measure=horizontal status=PASS")
args.json.parent.mkdir(parents=True, exist_ok=True)
args.json.write_text(json.dumps({"audit": "whole-surface-open-shaft-l59-v1",
"cases": results}, indent=2) + "\n", encoding="utf-8")
failures = [result for result in results if result["status"] != "PASS"]
print(f"[L59WholeSurfaceAuditSummary] cases={len(results)} failed={len(failures)} "
f"json={args.json} status={'FAIL' if failures else 'PASS'}")
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,83 +0,0 @@
#!/usr/bin/env python3
"""Split the owner surface strate around the L59 native lattice fixture."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--owner", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
root = json.loads(args.owner.read_text(encoding="utf-8"))
strates = root.get("strates")
if not isinstance(strates, list):
raise ValueError("owner input has no strates array")
by_index = {int(row.get("index", -1)): row for row in strates if isinstance(row, dict)}
if set(by_index) != {0, 1} or len(strates) != 2:
raise ValueError("fixture requires the reviewed two-strate owner input")
upper, surface = by_index[0], by_index[1]
if (int(upper.get("asset", {}).get("GeneratorType", -1)) != 0
or int(surface.get("asset", {}).get("GeneratorType", -1)) != 4):
raise ValueError("fixture requires TunnelNetwork over SurfaceWorld")
if int(upper.get("passage_config", {}).get("Connections", -1)) != 1:
raise ValueError("owner arrival passage count differs from the reviewed input")
# Keep both resolved mouths in the small synthetic Maze layer. The authored
# owner reach ranges are intentionally broad and can place a synthetic mouth
# hundreds of metres beyond a four-chunk prototype layer.
upper["passage_config"].update({"ReachMin": 32, "ReachMax": 32})
upper["asset"]["PassageConfig"].update({"ReachMin": 32, "ReachMax": 32})
maze = json.loads(json.dumps(surface))
maze.update({"index": 1, "top_chunk_z": -16, "bottom_chunk_z": -19,
"height_chunks": 4})
maze["asset"].update({"StrateName": "L59 Native Lattice Maze",
"GeneratorType": 3, "StrateHeightInChunks": 4})
maze["asset"].setdefault("MazeParams", {})["BoundarySealThickness"] = 8
maze["passage_config"]["Connections"] = 1
maze["passage_config"].update({"ReachMin": 32, "ReachMax": 32})
maze["asset"]["PassageConfig"].update({"Connections": 1, "ReachMin": 32, "ReachMax": 32})
lower = json.loads(json.dumps(surface))
lower.update({"index": 2, "top_chunk_z": -20, "bottom_chunk_z": -23,
"height_chunks": 4})
lower["asset"].update({"StrateName": "L59 Lower Surface", "GeneratorType": 4,
"StrateHeightInChunks": 4})
lower["passage_config"]["Connections"] = 1
lower["asset"]["PassageConfig"]["Connections"] = 1
root["total_strates"] = 3
root["strates"] = [upper, maze, lower]
# Keep the legacy passage portals proportional to this compact native
# prototype; the owner world remains at its authored scale.
root.update({"owner_voxel_size_cm": 6.25,
"owner_voxel_size_microns": 62500,
"core_voxel_size_cm": 25,
"core_voxel_size_microns": 250000,
"owner_to_core_length_scale": 0.25})
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(root, indent=2, sort_keys=True) + "\n",
encoding="utf-8")
print(f"[LatticeFixtureBuild] source={args.owner} source_sha256={sha256(args.owner)} "
f"fixture={args.out} fixture_sha256={sha256(args.out)} "
"strates=0:TunnelNetwork,1:Maze[4 chunks],2:SurfaceWorld[60 chunks] "
"owner_input_untouched=true status=PASS")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,179 +0,0 @@
#!/usr/bin/env python3
"""Export the L59 open-shaft lattice and render four reviewed viewpoints."""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[4]
CAPTURE54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/CaptureNativeCavesL54.py"
spec = importlib.util.spec_from_file_location("capture_native_caves_l54", CAPTURE54)
capture54 = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(capture54)
run_step = capture54.run_step
camera_angles = capture54.camera_angles
filter_mesh_y_leq = capture54.filter_mesh_y_leq
fmt = capture54.fmt
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def midpoint(points: list[list[float]]) -> tuple[float, float, float]:
return tuple(sum(point[axis] for point in points) / len(points) for axis in range(3))
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--fixture", type=Path, required=True)
parser.add_argument("--layout", type=Path, required=True)
parser.add_argument("--headless", type=Path, required=True)
parser.add_argument("--renderer", type=Path, required=True)
parser.add_argument("--artifact-dir", type=Path, required=True)
args = parser.parse_args()
out = args.out.resolve()
artifact_dir = args.artifact_dir.resolve()
capture_dir = out / "lattice-capture-png"
mesh_dir = out / "lattice-meshes"
for directory in (capture_dir, mesh_dir, artifact_dir):
directory.mkdir(parents=True, exist_ok=True)
layout = json.loads(args.layout.read_text(encoding="utf-8"))
edges = sorted(layout.get("cavern_edges", []), key=lambda row: row["stable_id"])
helix = next((edge for edge in edges
if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1"), None)
shaft_core = next((room for room in layout.get("cavern_rooms", [])
if room["stable_id"] == "lattice/spine/shaft-core-v2"), None)
maze_edges = [edge for edge in edges
if edge["stable_id"].startswith("lattice/spine/maze-edge-")]
if helix is None or shaft_core is None or len(maze_edges) < 4:
raise ValueError("layout is missing the resolved open shaft, helix, or maze spine")
helix_points = [[value / 1_000_000.0 for value in point]
for point in helix["polyline_um"]]
core_center = [value / 1_000_000.0 for value in shaft_core["center_um"]]
shaft_center = (core_center[0], core_center[1], core_center[2])
top = helix_points[0]
bottom = helix_points[-1]
whole_mesh = mesh_dir / "lattice-whole-view.vfmesh"
export_layout = out / "layouts" / "lattice-capture-layout.json"
env = os.environ.copy()
env["LP_NUM_THREADS"] = "12"
env["XDG_CACHE_HOME"] = str(out / "xdg-cache")
export_camera = (top[0], top[1], top[2] + 1.5, 0.0, -8.0)
run_step("lattice-whole-view-export", [args.headless, args.fixture,
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
"--stream-pipeline", "--force-generate", "--clip-radius=4",
"--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0",
"--recipe=lattice", "--lattice-strate=1",
f"--camera={fmt(export_camera)}", f"--resolved-layout-output={export_layout}",
f"--mesh-export={whole_mesh}"], out / "capture-lattice-export.log",
env=env, stamped=True)
if not whole_mesh.is_file() or whole_mesh.stat().st_size == 0:
raise RuntimeError("whole-lattice export produced no mesh")
cutaway_mesh = mesh_dir / "lattice-cutaway-y-center.vfmesh"
cutaway_triangles = filter_mesh_y_leq(whole_mesh, cutaway_mesh, shaft_center[1])
print(f"[L59LatticeCutaway] plane_y_m={shaft_center[1]:.3f} "
f"retained_triangles={cutaway_triangles} mesh={cutaway_mesh}", flush=True)
# Start inside the open shaft, offset toward the first tread so the view
# follows the well down while keeping the helical ledges in frame.
start_dx = top[0] - shaft_center[0]
start_dy = top[1] - shaft_center[1]
start_radius = max(1.0e-9, math.hypot(start_dx, start_dy))
top_camera = (shaft_center[0] + start_dx / start_radius * 3.5,
shaft_center[1] + start_dy / start_radius * 3.5,
top[2] + 1.6)
top_target = (shaft_center[0], shaft_center[1], bottom[2] + 1.3)
eye_index = len(helix_points) // 2
eye_floor = helix_points[eye_index]
eye_dx = eye_floor[0] - shaft_center[0]
eye_dy = eye_floor[1] - shaft_center[1]
eye_radius = max(1.0e-9, math.hypot(eye_dx, eye_dy))
# Put the eye on the walk line, looking across the open core at the far
# wall and the ledges above and below.
eye_camera = (eye_floor[0], eye_floor[1], eye_floor[2] + 1.35)
eye_target = (shaft_center[0], shaft_center[1], eye_camera[2] + 0.35)
maze_edge = next(edge for edge in maze_edges
if edge["stable_id"] == "lattice/spine/maze-edge-02-v1")
maze_points = [[value / 1_000_000.0 for value in point]
for point in maze_edge["polyline_um"]]
maze_a, maze_b = maze_points[0], maze_points[-1]
maze_t = 0.5
maze_floor = tuple(maze_a[axis] + (maze_b[axis] - maze_a[axis]) * maze_t
for axis in range(3))
maze_horizontal = max(1.0e-9, math.hypot(maze_b[0] - maze_a[0],
maze_b[1] - maze_a[1]))
maze_camera = (maze_floor[0] - (maze_b[1] - maze_a[1]) / maze_horizontal * 0.2,
maze_floor[1] + (maze_b[0] - maze_a[0]) / maze_horizontal * 0.2,
maze_floor[2] + 1.4)
maze_target = (maze_camera[0] + (maze_b[0] - maze_a[0]) / maze_horizontal * 8.0,
maze_camera[1] + (maze_b[1] - maze_a[1]) / maze_horizontal * 8.0,
maze_camera[2] + (maze_b[2] - maze_a[2]) / maze_horizontal * 8.0)
overview_camera = (shaft_center[0], shaft_center[1] + 12.0, top[2] + 4.0)
overview_target = (shaft_center[0], shaft_center[1],
(top[2] + bottom[2]) * 0.5 - 2.0)
views = (
("shaft-top-down", whole_mesh, top_camera, top_target, 22.0, 78.0, None),
("ramp-eye-across", whole_mesh, eye_camera, eye_target, 15.0, 100.0, None),
("maze-corridor", whole_mesh, maze_camera, maze_target, 8.0, 45.0, None),
("shaft-overview", cutaway_mesh, overview_camera, overview_target, 40.0, 80.0, None),
)
quality = ROOT / "Standalone/Tools/verify/capture_quality.py"
for name, render_mesh, camera, target, focus_radius, fov, clip_plane in views:
yaw, pitch = camera_angles(camera, target)
if name == "ramp-eye-across":
yaw, pitch = camera_angles(camera, target)
png = capture_dir / f"lattice-{name}.png"
image = artifact_dir / f"lattice-{name}.jpg"
height_tint = "off" if name == "maze-corridor" else "on"
render_args = [args.renderer, render_mesh, png,
"--size=1280x720", f"--fov-deg={fov:.1f}",
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}",
f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable",
"--headlamp=on", f"--height-tint={height_tint}",
f"--height-center-m={(top[2] + bottom[2]) * 0.5:.6f}",
"--height-range-m=24", "--tile-edges=off",
f"--focus-region={fmt((*target, focus_radius))}"]
if clip_plane is not None:
render_args.append(f"--clip-plane={fmt(clip_plane)}")
run_step(f"{name}-render", render_args,
out / f"capture-{name}-render.log", env=env, stamped=True)
run_step(f"{name}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel",
"error", "-y", "-i", png, "-frames:v", "1", "-q:v", "5", image],
out / f"capture-{name}-jpeg.log", env=env)
quality_output = run_step(f"{name}-quality", [sys.executable, quality, image],
out / f"capture-{name}-quality.log", env=env)
quality_lines = [line for line in quality_output.splitlines()
if line.startswith("[CaptureQuality]")]
if not quality_lines or not quality_lines[-1].endswith("status=PASS"):
raise RuntimeError(f"capture quality gate failed for {name}")
size = image.stat().st_size
if size > 300_000:
raise RuntimeError(f"capture exceeds 300 KB: {image} ({size} bytes)")
print(f"[L59LatticeCapture] view={name} file={image} bytes={size} "
f"sha256={sha256(image)} status=PASS", flush=True)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as error:
print(f"[L59LatticeCapture] status=FAIL error={error}", file=sys.stderr, flush=True)
raise SystemExit(1)
@@ -1,49 +0,0 @@
# Linux59 report
## Geometry and route
Resolved shaft: 21.0 m outer envelope; 4.5 m core radius; four-turn ramp at 6.0 m centerline radius, 3.0 m clear width, and 5.0 m pitch. The ramp spans radii 4.5–7.5 m and has 1.0 m floor support. Its inner edge is open to the core. The route keeps a 1.5 m margin from that edge (1.16 m capsule drop clearance). No raised lip was added.
Maximum route grade is 10.028° at the inner edge; minimum measured headroom is 2.20 m. Arrival-to-departure reachability passes all 883 stations, 44,150 capsule samples, and 4,415 support/headroom samples. The orphan-shaft control is disconnected as expected.
## Audits and determinism
The final-field interval oracle checked 2,058 samples in six boxes with zero misses. It proves shaft-core air, returns a mixed result at the core wall, rejects false all-air and all-solid proofs, and blocks the occupied maze tile.
The whole-surface audit passes with zero flagged triangles and zero clusters. Its scope includes a 0.25 m ramp-edge margin and the 1.0 m underside. The L45 ramp, L46 entrance, and L47 wall-foot controls fail as expected; the plain-native control passes. GCC and Clang camera peers at the shaft center `(11.790, 4.090, -130.630)` match across W1/W8/W12.
## Speed
| Measurement | L58 instrumented | L59 final |
|---|---:|---:|
| First tile | 0.222 s | 0.210 s |
| Near ring | 0.364 s | 0.328 s |
| Whole view | 1.625 s (1.69 s cited) | 1.605 s |
| Moving p50 / p95 | 90.567 / 165.304 ms | 479.544 / 963.764 ms |
The roughly 1 s whole-view target was not reached; the measured whole view improved only slightly, and moving latency regressed. L59 tile-drain instrumentation recorded 972 stage rows and 28,507 ms summed worker time: 23,332 ms in marching cubes and 717 ms in column work. Marching cubes dominates the measured worker time.
## Hashes, tiers, and captures
Source stamp: `389ac77f5d35328a68e4639cac4f7d03ad5ab13b72d4832e89459437cf960ca4`
Resolved layout: `resolved-layout-v3-fnv64:ab2f330ada023393`
Headless SHA-256: Clang `9fc1ac6c4c5a18b6bbdc0d907d26fcaaa63a49c163a68365f3cc79726db1c474`; GCC `47b37a28a7f132af2a3f91cdd61693fb8ac519e2f23f4064b48bec0b7f8ad964`.
Fast passed in 174.7 s; medium passed in 1,140.1 s; full passed in 1,206.4 s with its own 7,200 s timeout. Trial count: 15/15. Earlier failed trials exposed ramp-surface spikes and an orphan-control interval false proof; the final surface and interval controls pass.
All captures are JPEG and under 300 KB:
- [Shaft top-down](captures/lattice-shaft-top-down.jpg) — 31,500 B — SHA-256 `83e38fce238ddbde84fe3ee13867f5722eac30fe177c44e746eb06b7ae341783`
- [Ramp eye across shaft](captures/lattice-ramp-eye-across.jpg) — 24,409 B — SHA-256 `985a16fbd45c986c5bcdd0974e1cc32aac82a06a84b2df98a7b6a358f930cf14`
- [Maze corridor](captures/lattice-maze-corridor.jpg) — 15,746 B — SHA-256 `fe72cdd5a3907f8b9071784b08889719593d8839f69a863aa45dc9ac547e771d`
- [Shaft overview](captures/lattice-shaft-overview.jpg) — 39,081 B — SHA-256 `6394f0656200f91e77a23aed9c135e2e913dd0053c301921dff24336713c52d2`
## Orchestrator review (Claude)
**Parked on `wip/l59-open-shaft`, not merged.** The top-down capture finally reads as an open shaft with a ledge
spiralling down the wall, so the concept is right. But the ramp's **inner (open) edge is severely jagged:** shard-like
torn scallops along the whole edge (`lattice-ramp-eye-across.jpg`, and the top-down rim). The whole-surface audit
passed only because its ROI stops 0.25 m in from the ramp edge: the blind-spot pattern again. Speed moved only
1.69 → 1.61 s, at 15 of 15 trials. `assembler/world` keeps L58. Likely cause, to verify: a 1 m-thick ledge with a free
edge is a thin feature meshed at 25 cm. The edge needs a bullnose/fillet radius of at least 2 voxels and a thickness
the sampler resolves, and the audit ROI must include the edge itself.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""Render cave, motif, and lattice features from meshes streamed at LOD4-8."""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import re
import struct
import sys
ROOT = Path(__file__).resolve().parents[4]
CAPTURE54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/CaptureNativeCavesL54.py"
spec = importlib.util.spec_from_file_location("capture_native_caves_l54", CAPTURE54)
capture54 = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(capture54)
run_step = capture54.run_step
camera_angles = capture54.camera_angles
fmt = capture54.fmt
QUALITY = ROOT / "Standalone/Tools/verify/capture_quality.py"
Q = 1_000_000.0
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def midpoint(points: list[tuple[float, float, float]]) -> tuple[float, float, float]:
return tuple(sum(point[axis] for point in points) / len(points) for axis in range(3))
def read_feature(name: str, layout_path: Path):
layout = json.loads(layout_path.read_text(encoding="utf-8"))
if name == "caves":
edges = sorted(layout.get("cavern_edges", []), key=lambda row: row["stable_id"])
if len(edges) != 2:
raise ValueError("cave layout must have two resolved spine edges")
points = [[value / Q for value in point]
for point in edges[0]["control_points_um"]]
target = tuple(points[-1][axis] for axis in range(3))
return target, target[2] + 2.0, 28.0, "cave mouth"
if name == "motifs":
monolith = next(row for row in layout.get("stamps", []) if row.get("kind") == 5)
target = tuple(value / Q for value in monolith["position_um"])
floor = monolith["floor_um"] / Q
return (target[0], target[1], floor + 12.0), floor + 12.0, 32.0, "monolith"
edges = layout.get("cavern_edges", [])
helix = next((edge for edge in edges
if edge.get("stable_id") == "lattice/spine/01-helix-ramp-v1"), None)
if helix is None:
raise ValueError("lattice layout has no resolved helix")
points = [tuple(value / Q for value in point) for point in helix["polyline_um"]]
target = midpoint(points)
return target, target[2] + 4.0, 36.0, "helix"
def audit_roi_lods(mesh: Path, center: tuple[float, float, float], radius: float):
roi_counts: dict[int, int] = {}
roi_skirts: dict[int, int] = {}
with mesh.open("rb") as stream:
prefix = stream.read(60)
if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
raise ValueError("far-LOD audit requires a VFMSH001 Core mesh")
version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
if version != 1 or voxel_um <= 0:
raise ValueError("mesh version or voxel size is invalid")
for _ in range(tile_count):
header = stream.read(36)
if len(header) != 36:
raise ValueError("truncated far-LOD tile header")
key = struct.unpack_from("<4i", header, 0)
surface_count, skirt_count, vertex_count, index_count, mask_count = \
struct.unpack_from("<5I", header, 16)
positions = stream.read(vertex_count * 12)
normals = stream.read(vertex_count * 12)
index_bytes = stream.read(index_count * 4)
masks = stream.read(mask_count)
if (len(positions) != vertex_count * 12
or len(normals) != vertex_count * 12
or len(index_bytes) != index_count * 4
or len(masks) != mask_count):
raise ValueError("truncated far-LOD tile payload")
lod = key[3]
coords = list(struct.iter_unpack("<3f", positions))
indices = struct.unpack(f"<{index_count}I", index_bytes) if index_count else ()
for triangle in range(index_count // 3):
ids = indices[triangle * 3:triangle * 3 + 3]
if max(ids) >= vertex_count:
raise ValueError("far-LOD triangle references an invalid vertex")
is_skirt = bool(masks and triangle < len(masks) and masks[triangle])
if is_skirt:
continue
centroid = tuple(sum(coords[index][axis] for index in ids) / 3.0
for axis in range(3))
distance_squared = sum((centroid[axis] - center[axis]) ** 2
for axis in range(3))
if distance_squared <= radius * radius:
roi_counts[lod] = roi_counts.get(lod, 0) + 1
if skirt_count:
roi_skirts[lod] = roi_skirts.get(lod, 0) + skirt_count
if stream.read(1):
raise ValueError("far-LOD mesh has trailing bytes")
return roi_counts, roi_skirts
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--fixture-dir", type=Path, required=True)
parser.add_argument("--layout-dir", type=Path, required=True)
parser.add_argument("--headless", type=Path, required=True)
parser.add_argument("--renderer", type=Path, required=True)
parser.add_argument("--artifact-dir", type=Path, required=True)
args = parser.parse_args()
out = args.out.resolve()
fixture_dir, layout_dir = args.fixture_dir.resolve(), args.layout_dir.resolve()
artifact_dir = args.artifact_dir.resolve()
mesh_dir, png_dir = out / "far-lod-meshes", out / "far-lod-png"
for directory in (mesh_dir, png_dir, artifact_dir):
directory.mkdir(parents=True, exist_ok=True)
env = os.environ.copy()
env["LP_NUM_THREADS"] = "12"
env["XDG_CACHE_HOME"] = str(out / "xdg-cache")
cases = (
("caves", fixture_dir / "caves.json",
layout_dir / "native-caves-field-clang.json", "caves", []),
("motifs", fixture_dir / "style-override-motifs-slice1.json",
layout_dir / "motif-slice1-capsule-reachability-clang.json", "surface-world", []),
("lattice", fixture_dir / "lattice-l58.json",
layout_dir / "native-lattice-field-clang.json", "lattice", ["--lattice-strate=1"]),
)
quality_rows = []
for name, fixture, layout, recipe, extra_flags in cases:
if not fixture.is_file() or not layout.is_file():
raise RuntimeError(f"missing {name} fixture or layout")
target, target_height, radius, label = read_feature(name, layout)
stream_camera = (target[0] + 1024.0, target[1], target[2], 0.0, 0.0)
stream_yaw, stream_pitch = camera_angles(stream_camera[:3], target)
stream_camera = (*stream_camera[:3], stream_yaw, stream_pitch)
mesh = mesh_dir / f"{name}-lod4-8.vfmesh"
mesh_layout = layout_dir / f"{name}-far-lod-layout.json"
command = [args.headless, fixture, "--workers=12", "--world-assembler-prototype",
"--stream-benchmark", "--stream-pipeline", "--force-generate",
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
"--render-distance-chunks=0", f"--recipe={recipe}",
f"--camera={fmt(stream_camera)}", *extra_flags,
f"--resolved-layout-output={mesh_layout}", f"--mesh-export={mesh}"]
output = run_step(f"{name}-far-lod-export", command,
out / f"capture-{name}-far-lod-export.log", env=env, stamped=True)
worker_rows = {int(match.group(1)): (int(match.group(2)), int(match.group(3)))
for match in re.finditer(
r"^\[WorldAssemblerLODWorker\] lod=(\d+) tiles=(\d+) .*?skirt_triangles=(\d+)",
output, re.M)}
if (set(worker_rows) != set(range(9))
or any(worker_rows[level][0] <= 0 for level in range(9))
or sum(worker_rows[level][1] for level in range(4, 9)) <= 0):
raise RuntimeError(f"{name} far view lacks LOD0-8 tile/skirt work: {worker_rows}")
if not mesh.is_file() or mesh.stat().st_size == 0:
raise RuntimeError(f"{name} far LOD export produced no mesh")
roi, roi_skirts = audit_roi_lods(mesh, target, radius)
feature_lods = sorted(level for level, count in roi.items() if count > 0)
far_feature_lods = [level for level in feature_lods if level >= 4]
if not far_feature_lods:
raise RuntimeError(f"{name} {label} has no non-skirt surface triangles in LOD4-8 ROI: {roi}")
camera = (target[0] + 18.0, target[1] - 28.0, target_height + 10.0)
yaw, pitch = camera_angles(camera, target)
png = png_dir / f"{name}-far-lod.png"
image = artifact_dir / f"{name}-far-lod.jpg"
render = [args.renderer, mesh, png, "--size=1280x720", "--fov-deg=76",
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}",
f"--pitch-deg={pitch:.6f}", "--tile-skirts=on", "--shade=readable",
"--headlamp=on", "--height-tint=on", f"--height-center-m={target_height:.6f}",
"--height-range-m=42", "--tile-edges=off",
f"--focus-region={fmt((*target, radius))}"]
run_step(f"{name}-far-lod-render", render,
out / f"capture-{name}-far-lod-render.log", env=env, stamped=True)
run_step(f"{name}-far-lod-jpeg", ["ffmpeg", "-nostdin", "-hide_banner",
"-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "5", image],
out / f"capture-{name}-far-lod-jpeg.log", env=env)
quality_output = run_step(f"{name}-far-lod-quality", [sys.executable, QUALITY, image],
out / f"capture-{name}-far-lod-quality.log", env=env)
quality = [line for line in quality_output.splitlines()
if line.startswith("[CaptureQuality]")]
if not quality or not quality[-1].endswith("status=PASS"):
raise RuntimeError(f"{name} far LOD capture failed its image quality gate")
size = image.stat().st_size
if not 0 < size <= 300_000:
raise RuntimeError(f"{name} far LOD capture exceeds 300 KB: {size}")
print(f"[L60FarLODCapture] generator={name} feature={label} file={image} "
f"bytes={size} sha256={sha256(image)} feature_lods={feature_lods} "
f"feature_triangles={sum(roi.values())} far_skirt_triangles="
f"{sum(value for lod, value in roi_skirts.items() if lod >= 4)} status=PASS",
flush=True)
quality_rows.append({"generator": name, "feature": label, "file": str(image),
"bytes": size, "sha256": sha256(image), "feature_triangles_by_lod": roi,
"feature_skirts_by_lod": roi_skirts})
(out / "far-lod-captures.json").write_text(json.dumps(quality_rows, indent=2) + "\n",
encoding="utf-8")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as error:
print(f"[L60FarLODCapture] status=FAIL error={error}", file=sys.stderr, flush=True)
raise SystemExit(1)
@@ -0,0 +1,77 @@
# Linux 60 report
## Result and root cause
The continuation is green. The full tier and speed matrix passed after fixing signed-overflow undefined behavior in the shared voxel hash.
The first differing density was LOD1 tile `(0,64,-512)`, step 2, sample order 23,658 at `(64,82,-476)`. Before the fix, Clang returned `-4.0078125` (`0xc0804000`) and GCC returned `-4.125` (`0xc0840000`). The sample follows the mesher's LOD X8/boundary scalar path into `DensityAtLattice` and the compiled analytic `TunnelNetwork` field. Its upstream cave passage placement comes from `UVoxelStrateManager::GeneratePassages` → `PassageRandom01` → `VoxelHash::Cell(i,c,seed)`.
`VoxelHash::Cell` added `0x7fffffff` to a signed cell coordinate before casting to `uint32`; connection `c=1` overflowed `int32`, so the compiler-dependent hash changed the passage coordinates and cave geometry. The trace showed passage pair `(0,1)` diverging, followed by all 21 control points of passage 1. The fix performs the bias in unsigned arithmetic in `Cell`, `Pair`, legacy `Cellular3D`, and the shared Core cellular hash. It does not quantize density. After the fix both compilers produced the same pair `(0,1)` coordinates, the same 44 passage points, and `0xc0804000` at the first sample. The evaluator version is now 211.
Before/after traces are in `Saved/Linux/l60/diag/caves-{clang,gcc}-probe.log` and `Saved/Linux/l60/diag/cache/fullcache-{clang,gcc}.log`.
## Far LOD support and seam checks
All full-view fixtures use max level `-1`, clip radius 3, and max clip level 8. All three generators streamed LOD0–8 with skirts.
- **Caves:** X8 and Z-run gates detect caves in manager and resolved stratum mappings, then use scalar lattice sampling for cave samples. Clang/GCC cave tile hashes match at W1/W8/W12, including random views at LOD1–2. The cave-mouth transition crossed levels 0–8 (432 entering tiles).
- **Motifs:** The fixed motif field streams through the existing rings, caps, and skirts. The monolith transition crossed levels 0–8 (295 entering tiles); whole-surface foot, cap, and platform-edge audits passed.
- **Lattice:** The fixture selects native lattice stratum 1 and uses the existing ring and skirt path. The helix transition crossed levels 0–8 (380 entering tiles); field, interval, reachability, and shape-free audits passed.
Hard, gradient, and interleaved stratum transitions passed with zero hole columns and zero air samples. Far-LOD cave, motif, and lattice JPEG captures passed their audits and are under 300 KB.
## Comparable speed matrix
`verify.py full --speed-matrix` ran five interleaved repetitions per generator, rotating generator order. Every run used camera `(-12,-118,-20 m)`, rotation `(0°,0°)`, clip radius 3, max clip level 8, max level `-1`, camera step 24 m, 12 workers, and shared column cache. The stage asserted the shared HeadlessConfig fields, whole-view and per-LOD tile-count ranges, and stable counts across repetitions. It passed for all eight generators. Every listed tile count occurred in all five runs; timings are medians from the final full run.
| Generator | Tiles | Whole view (s) | First tile (s) | Near ring (s) | Moving p50 (ms) | Moving p95 (ms) |
|---|---:|---:|---:|---:|---:|---:|
| Natural SW | 2,130 | 1.669 | 0.502 | 0.519 | 63.971 | 119.891 |
| periodicHills | 1,986 | 1.311 | 0.454 | 0.479 | 60.267 | 115.648 |
| mirroredRelief | 1,986 | 1.317 | 0.471 | 0.490 | 60.568 | 115.474 |
| flatField | 1,986 | 1.303 | 0.465 | 0.485 | 60.627 | 117.144 |
| Floating Islands | 2,130 | 4.926 | 0.817 | 0.977 | 528.939 | 871.290 |
| Caves | 2,130 | 1.408 | 0.489 | 0.507 | 61.099 | 118.508 |
| Motifs | 1,986 | 1.326 | 0.476 | 0.493 | 60.107 | 116.104 |
| Lattice | 2,130 | 1.701 | 0.539 | 0.561 | 69.572 | 137.585 |
Full run records and common config are in `Saved/Linux/l60/speed-matrix.json`.
## Captures
- `Saved/Linux/l60/captures/caves-far-lod.jpg` — 16,642 bytes; SHA-256 `55f8d3d2de76541e9f1c1d09b771af40bfbb37b1ce5afc0b2f70419b84ed71a3`
- `Saved/Linux/l60/captures/motifs-far-lod.jpg` — 24,926 bytes; SHA-256 `bfc5d8ddebc3706fedd6108653b4777fac4bac44e71f60c8e58b3508c14c3aa6`
- `Saved/Linux/l60/captures/lattice-far-lod.jpg` — 30,714 bytes; SHA-256 `c261728e48975f01c712e31594b4284eede289b73e793e6204b78cb8ff2a2947`
## Hashes and tiers
Clang 19 and GCC 14 builds used FP contraction off and 12 jobs. Final source stamp and source-manifest SHA-256: `902a0d8e45f895d1b53c0a86999adee5598ff77e1752aa7ea90397520cf7b82d`.
- Clang headless: `dcf6286bcecac382f9cce2458e7c327485e9b8b993c3d544e627a8fc964947fa`
- GCC headless: `be86f90f993ba27bfde235e0850ea783298396dc03456360cf4bb3fd4815988b`
- Offscreen capture: `b8f7a441c0a41be6ebd61721ff903832a51291df7a886e669b9084d84441d4c5`
| Tier | Status | Checks / runs | Elapsed |
|---|---|---:|---:|
| Fast | PASS before hash fix | 453 / 113 | 335.7 s |
| Medium | PASS before hash fix | 968 / 289 | 1,559.1 s |
| Full continuation trial 1 | FAIL: stale binary stamp; no checks ran | — | 0.0 s |
| Full continuation trial 2 | PASS on source stamp `914af3...` | 2,304 / 660 | 3,256.9 s |
| Full continuation trial 3 | PASS on final source stamp, including speed matrix | 2,304 / 660 | 3,260.6 s |
Fast and medium were not rerun as standalone tiers after the hash change; the passing full run executed their checks at the current source stamp.
## Trials and failed runs
The prior L60 work used 15 trials. This continuation used 3 of its 10-trial budget. Trial 1 stopped before checks because it found stale Clang/GCC executables. Trial 2 passed, then the final source review restored the mesher's existing `<bit>` include and changed the source stamp, so trial 3 rebuilt and reran full against the exact final source. The pre-fix trial 15 had failed peer determinism on the same six LOD1 and eight LOD2 tile keys across four seeded views. Trial 3 had no failed checks; negative controls reported expected failures.
## Orchestrator review (Claude)
Accepted. The stamp matches the final Clang and GCC builds (`902a0d8e…`); full PASS, including the speed matrix.
**The root cause is a real cross-platform determinism bug:** `VoxelHash::Cell`/`Pair`/`Cellular3D` added
`0x7FFFFFFF` to a signed `int32` (signed overflow is undefined behaviour), so passage placement became
compiler-dependent. **The same code lives in the Unreal plugin** (`Source/VoxelForge/Public/VoxelCaveMorphology.h`,
identical to the standalone copy before the fix), and only the standalone copy had been fixed. I applied the identical
fix to the plugin header and confirmed the two copies match. MSVC typically wraps there, so the owner's existing
worlds are most likely unchanged, but this needs his Windows build. No other signed-bias pattern remains in `Source/`
or Core. The comparable speed matrix is now a standing `verify.py full --speed-matrix` stage.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Loaded 100 of 264 files, more files were not shown because too many files have changed in this diff. Show more