Author SHA1 Message Date
Fr0zkaandClaude Opus 5.5 8d62ba57ed research: post-swept-tube near-ready cost account (L103, measurement only)
FI/TN LOD0-2 W12 worker CPU 13.9/12.2 s: passages 40/35%, far-shell refinement (HasDownwardHit/
HasUpwardHit ~1.7-1.9 s) in 'other' 27/30%, support 14/13%, dispatch 8%, base field 6%, bands 6%.
Workers 28%/24% idle during LOD0-1. Near-ready W12: SW 0.77, FI 1.51, TN 1.38, VS 0.96 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 19:39:47 +01:00
Fr0zkaandClaude Opus 5.5 c38e683292 research: Unreal arrival traces with swept tunnels (L102)
Six null-RHI traces on 55e20f4, no address-space cap. FI: first admission 0.385 s, LOD0-2 settled
1.943 s (1.555 s from admission; L97 2.109 s), UWorld::Tick max 6.7 ms (L97 21 ms). SW: 0.415 /
1.706 s (1.292 s from admission), max 12.0 ms. EndPhysics gap <= 2 ms. Peak RSS ~2.32 GiB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 19:10:19 +01:00
Fr0zkaandClaude Opus 5.5 55e20f4a7e passages: memoized exact MC midpoints + wider conservative sweep bounds (L101, exact vs L100)
Off-grid census by caller (MC midpoints, skirts, far shells); all 63.0M sweep misses accounted
(1.9M exact-route fallbacks, 61.1M counted bound skips). Exact MC midpoint queries memoized by
float bits (Unreal enables it too); feature bounds expanded. L100/L101 whole-view hashes equal in
all four strata (v216 unchanged); 0 added/removed crossings; 15 capture pairs pixel-identical.
Near-ready @W12 L100 -> L101: FI 1.77 -> 1.65, TN 1.66 -> 1.54, SW 0.85 -> 0.84, VS 1.03 -> 1.03 s
(L98 2.28/2.23/1.02/1.11). Fast tier PASS; Linux UE host builds. Unreal arrival traces not obtained
(the runner wrongly applied the 6 GiB address-space cap to Unreal).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 18:57:56 +01:00
Fr0zkaandClaude Opus 5.5 2d5822cdf7 passages: swept tube evaluates the exact route on in-bound cache misses (L100, v216)
L99 treated a missing exact-grid sweep sample as "no route here", so off-grid queries exposed the
FI/TN boundary band inside passage 1-2 (flat ceiling/floor). In-bound misses now evaluate the
exact route; a conservative route-feature bound still skips out-of-extent misses. Composition
order unchanged (graph -> bands -> passage -> stamps -> floor caps).
Comparison now counts added/removed crossings and triangles: 0/0 crossings on all four strates
(FI +56 triangles). 1-2 mid-tunnel: L98->L100 changes 32k px (L99 changed 1.14M).
Near-ready @W12 (L98/L99/L100): FI 2.62/1.78/2.05, TN 2.36/1.55/1.80, SW 1.17/0.95/1.04,
VS 1.16/1.03/1.08 s. All walk audits pass; 24/24 Clang/GCC W1/W8/W12 determinism; fast tier PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 16:49:46 +01:00
Fr0zkaandClaude Opus 5.5 fa6363b31a passages: two-pass swept-tube tunnels (owner design, L99, v215) - pending owner visual review
Route plan unchanged. Per tile, route segments whose bounded influence (tube + 4 m) touches the
tile write min-combined tube distance (base sweep); relief/floor/mouth shaping only in the shell
(detail sweep); 0.5 m edge blend into the strate field. Near-ready @W12: SW 1.01->0.80,
FI 2.23->1.43, TN 2.18->1.37, VS 1.09->0.98 s; whole view FI 3.38->2.37 s. All passage walk
audits pass (21,425/21,425 canonical, 3/3 interfaces); 24-run Clang/GCC W1/W8/W12 determinism;
0 sign flips. Change: mean 0.02 cm / 0.05 deg; max 25.9 cm (passage walls/roofs ~3.5 m from
route, LOD2) and 49.8 deg (rock walls 13-25 m from routes, LOD2). Captures with diffs in
Linux99/captures. Fast tier PASS (v215 pins).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 14:38:45 +01:00
Fr0zkaandClaude Opus 5.5 36140b573f passages: prepared per-segment data + per-column XY projection cache (L98, exact)
Per-world prepared chord/length/radius/Catmull-Rom/floor-profile data (310 segments, ~66 KB,
<0.04 ms); same expression grouping, divisions and square roots, so output is bit-identical
(zero density/tile mismatches, Clang vs GCC equal). No measurable readiness gain - the cost is
the number of samples running passage work, not per-sample arithmetic. Note: the report's
world labels are shifted by one row (the 2,850-tile row is SurfaceWorld). 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-28 12:50:19 +01:00
Fr0zkaandClaude Opus 5.5 9d9504ae4e unreal: tile generation at BackgroundLow priority (L97); FI arrival world-tick max 93 -> 21 ms
The 50-90 ms UWorld::Tick spikes were the TG_EndPhysics barrier waiting for physics-scene
completion while all 8 TaskGraph workers ran BackgroundHigh tile tasks. Near and horizon tile
generation now run at ETaskPriority::BackgroundLow (helper lanes background); queue depth and
the 8 workers are kept. FI: near set 2.11 s (10 runs), max UWorld::Tick 21 ms, max
EndPhysics->PostPhysics gap 16 ms; SW: 1.32 s, 11 ms. A hard 7-task cap removed spikes but
doubled settle time. World unchanged. Fast tier PASS; Linux UE host builds.
R44 notes (exact passage restructuring: only a full-composer z-column SIMD kernel could
plausibly reach 3x; smaller exact pieces ~1.1-1.2x each) attached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 12:03:52 +01:00
Fr0zkaandClaude Opus 5.5 94f9aa6790 unreal: remove 121 ms per-frame overlap scoring and 162 ms world-lock wait; keep support tiles (L96)
- Per-frame passage-overlap scoring over outstanding tiles (added in L85) measured 121 ms for
  407 LOD2 candidates; removed (deterministic desired/travel order kept).
- Visibility reconciliation read-locked the Core world per candidate while the far-world build
  held the writer lock (162 ms scan, 123 ms lock wait); the immutable render distance is cached.
  Visibility now <= 0.17 ms.
- Ready player-support collision tiles stay in the collision-desired set: fixes the L95 SW outlier
  (LOD0 (3,0,-47/-48/-50) re-admitted); 6 SW runs without re-admission.
- AVoxelWorld::Tick during the arrival burst: FI p99 3.7-4.3 ms (max 6.0), SW p99 8.2-8.9 ms
  (max 11.2). FI still shows rare 40-60 ms world ticks outside our actor scope (engine tick group).
- Near-set times kept: FI 2.06-2.12 s, SW median 1.30 s after first admission. World unchanged.
  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-28 10:28:28 +01:00
Fr0zkaandClaude Opus 5.5 5389abed2e unreal: deeper ready queue keeps the 8 tile workers fed (L95); SW near set 2.37 -> 1.31 s
Unreal per-tile timeline: per-tile Core work is not slower than headless (FI 11.2 vs 16.2 ms
per shared key); the gap was tick-paced admission with a small queue (FI ~5.2 of 8 slots busy).
voxel.StreamingReadyQueueDepth default 16 -> 64 (effective 32 with 8 slots, total cap 40);
result backlog cap 256 unchanged. First admission -> LOD0-2 settled: FI 2.72 -> 2.10 s,
SW 2.37 -> 1.31 s (3 of 4 runs; one 6.9 s outlier re-admitting 3 LOD0 keys, cause open).
apply_ms max 4.2 ms (14 ms budget). Frame-interval max 118-145 ms both before and after (open).
World unchanged. 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-28 08:51:25 +01:00
Fr0zkaandClaude Opus 5.5 35f0a67fe3 unreal: parallel sign-edge repair + cheap SW route prefilter (L94); SW startup 2.75 -> 0.43 s
- Unreal requests Core's deterministic parallel sign-edge repair (UE ParallelFor, capped at
  10 workers); headless defaults to parallel (--serial-sign-edge-repair for A/B). SW
  sign_edge_distances 1,942 -> 37 ms, ShapeStage 2,156 -> 198 ms.
- SW fallback route planner checks the exact mouth-radius endpoint constraints before sampling
  terrain (58,480 of 78,408 candidates skip heightfield work); same 35 eligible, same chosen route.
  SerializeResolvedWorld 468 -> 115 ms; BeginPlay block 0.58 -> 0.22 s.
- Unreal SW: first admission 2.75 -> 0.43 s, LOD0-2 settled 5.20 -> 2.81 s; FI 3.40 -> 3.12 s.
- World identical: assembly/recipe/plan hashes unchanged; serial vs parallel per-tile hashes equal.
  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-28 08:03:28 +01:00
Fr0zkaandClaude Opus 5.5 1e111cc042 diag: bounded passage carve reach rejected (L93); R43 Unreal SW startup notes
Explicit carve reach R (diagnostic, default unbounded): R=2.5 m -> 1.56 cm max displacement,
106 deg max normal change; R=4 m -> 0.63 cm but 53 deg normals. Fails the bar; readiness gain
small and inconsistent (TN -5%, VS +6-7%). No world change (v214). Fast tier PASS.
R43: Unreal SW bReady 2.16 s = serial sign-edge repair (1.94 s, 10.9M corrections;
parallelSignEdgeRepair exists but off); ~0.45 s of SerializeResolvedWorld = adapter SW route search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 07:33:53 +01:00
Fr0zkaandClaude Opus 5.5 70c37aaab3 unreal: startup boost spans epoch churn; empty collision tiles stay classified (L92)
- Startup apply boost (24 meshes / 14 ms) now ends on initial LOD0-2 + collision readiness or
  the 30 s timeout, not on the first desired-epoch change: FI LOD0-2 settled 3.49 -> 3.40 s.
- Known-empty LOD0 collision classifications persist across unloads within a modification epoch
  and are invalidated on edits; commandlet readiness accepts CollisionNotRequired tiles. The 30 m/s
  movement trace now passes (no re-requests of the 4 support tiles).
- Startup trace events: SerializeResolvedWorld ~466 ms synchronous in BeginPlay (unchanged);
  SW bReady at 2.74 s vs FI 0.75 s (to investigate). Redundant second parse removed.
- Core output identical. Fast tier PASS; Linux UE builds. R42 notes attached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 06:54:59 +01:00
Fr0zkaandClaude Opus 5.5 8805291d3e diag: passage source-floor clamp rejected (L91); field has steep non-distance tails
Clamping the passage-composition source density at -c (c=2..6 voxels, diagnostic, default off)
changes the surface far beyond the bar: c=6 still 65 sign flips, up to 165 cm displacement, 120 deg
normal change (downstream composition consumes deep values). Source gradient at crossings is
distance-like at the median but has huge tails (FI p90 |grad| 512, max 894 vox/vox), so the
density-dependent passage reach is inflated by non-distance values. No world change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 05:50:11 +01:00
Fr0zkaandClaude Opus 5.5 a7a03d42b4 diag: passage candidate compiler census (L90, opt-in); falsifier failed at 10 m reach
54,324 sparse 4 m bricks, uncapped carve/tread/clearance/mouth lists, built in 26-28 ms,
~12 MiB. Weighted over 30.7M near-LOD sample hits: active carve p90 13, mouth p90 13; reach
fallback keeps 18.3% of passage time (FI 12%, TN 19%) - all because the density-dependent reach
(-d*Q + 0.24 m) exceeds 10 m for deeply negative source samples. Not wired into production.
R41 notes (Unreal LOD0-2 gap: ~0.7 s pre-request in BeginPlay, LOD2 tail, apply boost reset;
L80's 4 missing tiles = re-requested empty collision-support tiles) attached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 05:28:27 +01:00
Fr0zkaandClaude Opus 5.5 04cdd197fb research: Astra pass 3, compiled spatial passage evaluator design (read-only)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 04:46:50 +01:00
Fr0zkaandClaude Opus 5.5 fa78192c86 diag: passage vs boundary-band ceiling split (L88); coarse passage cache rejected
Verified ablations (zero passage work / zero band calls). LOD0-2 @W12: exact FI 2.27 / TN 2.25 s;
passages off FI 0.81 / TN 0.85; bands off FI 2.11 / TN 2.06. Passages cost ~1.4-1.6 s of near
readiness, bands ~0.1-0.2 s. A tile-local 4-voxel trilinear passage-delta cache increased carve
segment visits (FI 34.5M -> 56.2M) and slowed LOD0-2 (FI 2.58 -> 3.77 s); removed. No world change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 03:37:24 +01:00
Fr0zkaandClaude Opus 5.5 0fe7f371f7 diag: corrected passage-off ceiling + cold worker/tile timelines (L87); passages ARE the gate
L86's switch left ApplyResolvedStratePassageCarves running (near-tile service unchanged at ~21
worker-s), so its "no change" ceiling was wrong. Corrected (bands + carves off): FI LOD0-2
2.34 -> 0.66 s, whole 3.47 -> 1.49 s @W12; TN 2.27 -> 0.74 / 3.28 -> 1.52 s. Near-tile service
15.6-22.5 -> 2.6-3.8 worker-s. Critical chain: shape/base compile -> LOD0-1 -> far-parent build
(107-153 ms, pipeline thread) -> LOD2. Workers 81-85% busy in the direct window.
No production scheduling change; hashes equal L85. Fast tier PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 03:07:30 +01:00
Fr0zkaandClaude Opus 5.5 4f5f6667ae diag: passage-disabled ceiling (L86); readiness unchanged without passages
Environment-only diagnostic hook disables passage composition (never a world setting).
Cold LOD0-2 / whole @W12 WITHOUT passages: SW 1.12/2.25, FI 2.18/3.34, TN 2.15/3.15, VS 1.12/2.03 s
- the same as with passages (FI 2.16 s). Passages are ~58% of sampled CPU but not the
readiness bottleneck. A lazy exact passage-result cache was +47% slower (24% hit rate, ~700 MB)
and was removed. No world change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 02:13:43 +01:00
Fr0zkaandClaude Opus 5.5 06f558dc91 streaming: split dense passage-junction grid fills; heavy-first order + far-parent deferral in Unreal (L85)
Tiles with passage-overlap score >= 9 split grid fill over up to 4 helper lanes (disjoint rows,
same per-sample order, join before MC) when workers are idle. FI junction tile fill 173 -> 99 ms;
estimated serial floor ~0.55-0.60 s. Readiness essentially unchanged (FI/TN LOD0-2 ~2.15 s @W12):
the limit is now aggregate work, not single tiles.
Unreal: LOD0-2 ordered by passage overlap before travel/distance (L80 modes kept); far parents
built after LOD0-1, LOD2 gated on them; helper budget via ParallelFor. Unreal null-RHI FI
arrival: LOD0-2 resident at 3.65 s.
All tile hashes identical to L84 (four whole views). 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-28 02:02:32 +01:00
Fr0zkaandClaude Opus 5.5 0a98da6fd0 streaming: passage-heavy tiles first, far parents after LOD0-1 (L84, headless only)
Predicted passage-overlap tiles are scheduled earlier (FI worst tile rank 116 -> 69, dispatch
0.95 -> 0.82 s); LOD0-1 publish from the base shape before far parents, which are built before
LOD2 is queued. Every tile hash identical to L83 (all four whole views). Fast tier PASS.
Cold LOD0-2 W8 / W12: SW 1.02 / 0.99, FI 2.26 / 2.18, TN 2.27 / 2.15, VS 1.13 / 1.09 s.
Heaviest tiles (FI/TN junction band, z=-93) still take 0.5-0.7 s each, mostly full-grid fill;
idealized floor ~0.92 s. Unreal scheduler not yet updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 01:02:59 +01:00
Fr0zkaandClaude Opus 5.5 a81b5b7d06 passages: contiguous BVH node ranges + child-bound reuse (L83); scaling and serial floor measured
Exact through LOD2 (all near tile hashes equal L82). W8 LOD0-2: SW 1.09->1.06, FI 2.47->2.34,
TN 2.39->2.43 (noise), VS 1.20->1.18 s. Fast tier PASS.
Scaling W4/W8/W12/W16 (LOD0-2, FI): 3.59 / 2.48 / 2.33 / 2.34 s - flattens past W8-W12.
Estimated infinite-worker floor ~1.0 s FI/TN = shape compile (0.35-0.41 s, not overlapped:
no shape-free tiles in these views) + slowest single LOD0 tile service (0.51-0.59 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-28 00:24:49 +01:00
Fr0zkaandClaude Opus 5.5 f572108dc4 passages: exact relief skip + per-tile BVH node masks (L82); FI/TN LOD0-2 -12%
- Wall relief noise is skipped when its hard-bounded contribution cannot change the final max;
  mouth surface/tread noise only on paths that use it.
- Exact active BVH-node masks cached per tile/passage/segment range; no per-sample lower_bound.
- Full-view audit imports the exported Core plan (not the stale 26-point root route): 21,425/21,425
  walkability samples pass.
Cold (L80 protocol, medians) LOD0-2: SW 1.16->1.11, FI 2.86->2.51, TN 2.88->2.54, VS 1.23->1.22 s.
Passage share FI 64.6->58.7%, TN 65.4->58.4%. Hashes identical (v214). TN "slowdown" from L81
not reproduced (5 pairs, 3.08 -> 2.75 s). 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 23:46:15 +01:00
Fr0zkaandClaude Opus 5.5 9cb5e53b17 passages: exact density-aware outer bound skips passage work beyond its reach (L81, v214)
The carve is max(source, -roughSDF/Q), so its reach grows with -source; the outer test uses
-d*Q + 0.24 m (wall relief 0.21 + margin) beyond the route radius, disabled in mouth-support
and column-overlap tiles. Skips 16% of passage iterations on the highest-activity tiles and
all of them on ordinary tiles. 84 candidate/full pairs identical in every hash; 0 changed samples.
Evaluator 214 for cache invalidation only. Cold LOD0-2 (L80 protocol): SW 1.30->1.25,
FI 3.59->2.93, TN 3.18->3.51 (noise?), VS 1.33->1.22 s. Fast tier PASS.

Note: the export's legacy 26-point root route owner_world.passages[0] fails a full-view
pre-audit (65 solid samples, unchanged since L70); the canonical 211-point route used by the
plan passes. Stale data to clean up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 22:58:34 +01:00
Fr0zkaandClaude Opus 5.5 b06397df9c streaming: speed-aware scheduler with hysteresis (L80); arrival near-first, fast travel keeps the horizon
Policy 3 (headless + Unreal): walkable ring always first; at rest/arrival LOD0-2 before far;
walking interleaves near:far ~4:1 toward heading; fast travel admits far coarse tiles right after
the walkable ring. Hysteresis: walking 0.75/0.35 m/s, fast 12/8 m/s.
Headless, far holes while moving 30 m/s: L77 9.0% / L79 77.2% / L80 8.9%. Cold LOD0-2 ready
(one clean protocol, medians): SW 1.30, FI 3.61, TN 3.07, VS 1.43 s.
Unreal 30 m/s traces end 2,233/2,237 loaded (commandlet readiness exit 1) for all policies.
Passage zero-padding cut: ~50% fewer passage iterations on active tiles, max 6.6 cm surface
displacement, no sign flips; not adopted yet (padding sweep next). World unchanged (v213).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-27 21:39:21 +01:00
Fr0zkaandClaude Opus 5.5 2c8a26c0d0 streaming: far LODs keep a share while near work is pending (L79); no more horizon starvation
L78's strict near-first order starved far LODs at 30 m/s (100% far holes, never completed).
Unreal policy 2: once the collision ring is ready, LOD0-2 keep their reserve and far tiles are
admitted as low-priority Horizon work; headless gives far work 1 of every 16 requests.
Moving camera, far holes while moving: 1.4 m/s L77 8% / L78 29% / L79 31%; 30 m/s L77 8% /
L78 100% / L79 72% (completes on dwell). Near readiness while moving at 30 m/s: 52 / 51 / 67%.
Still worse than L77 for fast travel -> speed-adaptive policy next. World unchanged (v213).
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 20:17:35 +01:00
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
501 changed files with 38336 additions and 2114 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 L43's report records the off/on hashes, walk and interface audits, outside-stamp timing, tier
results, captures and any failed runs. 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 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, dispatch stay unchanged. Planning first binds the actual incoming lower mouth and outgoing upper
then creates arrival/exit overlap corridors, an open vertical shaft and the maze spine; two mouth from the resolved passage records, then creates arrival/exit overlap corridors, a mandatory
deterministic dead ends are added after that route. Resolved room and edge seeds use four-turn helix and the maze spine; two deterministic dead ends are added after that route. The
`(world seed, strate, recipe version, stable ID)`. The orphan control translates the shaft 20 m resolved room and edge seeds use `(world seed, strate, recipe version, stable ID)`. An audit control
away from both route junctions and must fail reachability. 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 The prototype helix uses a 6 m centreline radius, 3 m clear width, 5 m pitch, four turns and 2.6 m
top tread and 2 m below the bottom tread. The ramp winds around the core at a 6 m centreline radius, finished floor-to-soffit height. Its centreline descends 20 m over roughly 151 m; the inner-edge
with 3 m clear width, 5 m pitch and four turns. The open annular shaft air joins the core air, while grade is about 10.0 degrees. The 2.4 m floor-wall smooth-max gives an isolated ideal fillet radius
one-metre support slabs follow the helix under each tread. This leaves a 4 m vertical gap between of 0.8485 m. A separate 0.4 m feature-union blend prevents adjoining ramps from erasing the tread.
one tread and the underside of the next. The ledge's inner edge is open to the well; the route The swept-clear diameter is 15 m; the conservative padded feature envelope is about 20.6 m (the
centreline stays 1.5 m from the drop and leaves 1.16 m beyond the 0.34 m capsule radius. Its inner approved approximately 21 m site envelope).
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 layout reuses the resolved cavern graph vocabulary. A per-strate opt-in selects the native 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 negative-solid/positive-air field; canonical feature order is stable-ID order. Room/corridor bounds
air and its support layers use an analytic helix lookup instead of testing every polyline segment are indexed by the graph BVH and polyline segments have a nested deterministic BVH for density and
per density sample. The core cylinder and annular wall have conservative interval bounds; false interval queries. Box classification stays conservative, and `isTileShapeFree` refuses any tile
all-air and all-solid controls remain rejected, and `isTileShapeFree` refuses occupied tiles. The that intersects the graph. The L58 pipeline audits the composed field from actual mouth to mouth
L59 pipeline checks the composed field from actual mouth to mouth with a 0.34 m by 1.76 m capsule, with a 0.34 m by 1.76 m capsule, per-station support, 2.2 m clearance and a 15 degree grade limit;
support at every station, 2.2 m clearance and a 15 degree grade limit. It also checks the orphan it also checks the orphan control, brute-force interval samples and the composed wall-foot fillet.
control, whole-surface ramp lip/wall-foot/underside, brute-force interval samples and the composed The Linux58 report contains the measured reachability, surface, peer-determinism, timing and capture
wall-foot fillet. See the Linux58 and Linux59 reports for evidence. This code path is not promoted results; the code path is not promoted to legacy Maze/VerticalShafts generation.
to legacy Maze/VerticalShafts generation.
### 8.55 Unreal CoreGeometry asset bridge and U1 asset version
`UVoxelStrateDefinition::CoreGeometry` is optional and defaults to recipe `None` with no stamps,
rooms, or tunnels. The Unreal adapter preserves the established passage planner, then resolves
authored stamps and cavern graph records, then finalizes the layout. Stable names are required and
are scoped by strate in Core; landmarks may target a stamp, room, or tunnel by that stable name.
Core profile heightfields are restricted to SurfaceWorld. Native caves require the TunnelNetwork or
Underwater archetype; native lattice requires Maze or VerticalShafts. Core recipe/version fields
enter the resolved layout identity when selected. An imported manifest cannot be mixed with a
per-strate profiled heightfield.
The checked-out Core API contains stamp vocabularies V1, V2, and V3. Unreal uses V1 for the original
stamp kinds and V3 for monolith/platform motifs, then combines and strictly validates the result.
Research17 names V2, but the source API and motif vocabulary require V3; source was followed.
Research27 water remains out of scope for U1.
The shared VoxelForge custom asset version is 2: version 1 records the tunnel-floor slope migration;
version 2 adds load-time conversion for inter-strate gap, strate height, and transition width. The
saved strate-layout width is used, currently 8 m. Old packages are converted in memory and marked
dirty, but U1 does not save them. This does not yet migrate the rest of Research8's reflected
spatial-property inventory: legacy chunk distances, owner/Core voxel lengths and frequencies,
placement vectors, and other centimetre fields remain in their prior units. The U1 report records
the five BuildHost asset values, hashes, and remaining work.
## 9. Multiplayer model (listen-server first, dedicated-friendly) ## 9. Multiplayer model (listen-server first, dedicated-friendly)
+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 at 25 cm those tiles happen to span 8 m, but they are not the layout unit. The gameplay capsule remains
**0.34 m radius / 1.76 m tall** (**2.72 / 14.08 legacy voxels**). Research-field dimensions are authored **0.34 m radius / 1.76 m tall** (**2.72 / 14.08 legacy voxels**). Research-field dimensions are authored
in metres and converted at runtime. The live in metres and converted at runtime. The live
strate generator continues to read voxel-valued parameters from its assets, so those authored strate generator continues to read voxel-valued shape parameters from its assets, so those authored
dimensions become half as large in metres unless their asset values are doubled. dimensions become half as large in metres unless their asset values are doubled. U1 adds a
versioned in-memory conversion for strate height, transition width, and inter-strate gap only; the
broader Research8 voxel/chunk/centimetre inventory is not yet migrated.
The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells The intended metre-scale families remain: room diameters **8-20 m**, origin hub **24 m**, room cells
**32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m** **32 m**, round tunnel bores **3-4 m**, maze cells **16 m** with a **6.25 m nominal bore** (**3.125 m**
radius; **3.0625 m** worst-case usable floor after roughness), slab open span **22.4 m**, shaft radius; **3.0625 m** worst-case usable floor after roughness), slab open span **22.4 m**, shaft
diameters **4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. Source diameters **4-7 m**, island diameters **12-24 m**, and ordinary inter-strate bores **3-4 m**. Source
new fixed physical dimensions are authored in metres and converted to voxel counts at the boundary. new fixed physical dimensions are authored in metres and converted to voxel counts at the boundary.
The owner's voxel-valued live strate assets are not migrated by C++: unchanged saved values therefore The owner's voxel-valued live strate shape settings are not migrated by U1: unchanged saved values
produce half-size rooms, tunnels, features and noise periods in metres. See the step-3 migration list therefore produce half-size rooms, tunnels, features and noise periods in metres. See the U1 report
in the handoff report for the exact `DA_Settings` fields and values. for the conversion coverage and remaining Research8 fields.
**Owner asset migration (no packages were edited):** preserve `DA_Settings` distances by changing **Prior reauthoring recommendation (not applied in U1; no packages were edited):** preserve
`DA_Settings` distances by changing
`ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`, `ViewDistanceXY 32→64`, `ViewDistanceUp/Down 10→20`, `PawnCollisionRadiusChunks 2→4`,
`StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`, `StrateViewMarginChunks 6→12`, `ClipRadius 6→12`, `RenderDistanceChunks 1536→3072`,
`EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region `EdgeSealThickness 128→256 voxels`, decoration radius/near radius `12/6→24/12 chunks`, region
@@ -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 `12→24 cells`; it is currently disabled. Keep `MaxClipLevel=6` and zero-valued disabled/unbounded
limits unchanged; double finite world radii and gaps, and multiply finite voxel-volume caps by 8. limits unchanged; double finite world radii and gaps, and multiply finite voxel-volume caps by 8.
The loaded `Crystal Cave` slot in `DA_Strate3` is 64 chunks tall; use 128 chunks to preserve its The loaded `Crystal Cave` slot in `DA_Strate3` is 64 chunks tall; use 128 chunks to preserve its
former 512 m span. Its voxel-authored live-generator lengths remain half-size until that asset is former 512 m span. U1 automatically converts only strate height and transition widths and the
re-authored; the editor-only research field is the step-3 target. settings gap at load; the remaining recommendations above are pending. Its voxel-authored
live-generator lengths remain half-size until their asset fields are migrated.
Step-3 six-worker scale benchmark (idle machine, `Saved/Research/step3/scale/step3_scale_cacheperf_v1`): Step-3 six-worker scale benchmark (idle machine, `Saved/Research/step3/scale/step3_scale_cacheperf_v1`):
the 519³ field took **7.133834–7.825292 s** across three runs (median **7.347496 s**); the 259³ the 519³ field took **7.133834–7.825292 s** across three runs (median **7.347496 s**); the 259³
@@ -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. | | `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. | | `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. | | `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. |
| `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. | | `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::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. | | `FCavernRuntimeFeature::FSegmentNode`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | A deterministic nested segment BVH accelerates native graph distance and conservative interval evaluation; the outer graph BVH still supplies tile candidates and protects `isTileShapeFree`. The 0.4 m feature-union width is separate from the floor fillet width. §8.54. |
| `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. | | `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval. | `ApplyOwnerFourStratePlan` | `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` | BuildHost-only transient fixed-slot plan from the four staged owner strate assets; leaves the authored settings and strate packages untouched. |
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|standalone-export|compile-only|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, allows the owner's interactive editor while refusing overlapping `UnrealEditor-Cmd` launches, runs owned processes at Normal priority with a 12-logical-CPU cap, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. | | Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. |
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). | | `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; schema-v1 owner-asset export check and BuildHost Core actor tests. `-coreworldassembly=PATH` supplies a reviewed manifest transiently to `-coreworldtest=1`; it does not save a data asset. `FStandaloneCoreAdapterScheduler` runs Core work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. | | `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; schema-v1 owner-asset export check and BuildHost Core actor tests. `-coreworldassembly=PATH` supplies a reviewed manifest transiently to `-coreworldtest=1`; it does not save a data asset. `FStandaloneCoreAdapterScheduler` runs Core work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. |
@@ -1310,19 +1315,42 @@ See ARCHITECTURE §8.53 and
[`Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md) [`Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md)
for layout identities, owner hashes, outside-box timing, audits and captures. 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 | | 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)`. | | `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 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. | | `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 every-station support, headroom, grade, orphan rejection, composed fillet, shaft air/wall intervals and tile exclusion. | | `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. |
| `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. | | `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 See ARCHITECTURE §8.54 and
[`Linux59 report`](Standalone/Research/WorldAssemblerM3/Linux59/REPORT.md) for the open shaft's [`Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md)
measured geometry, final-field reachability, controls, timing, hashes and captures. The L58 report for resolved records, final-field reachability, controls, timing, hashes and captures.
retains the first-slice evidence.
### 3.37 Unreal CoreGeometry assets and partial metre upgrade — World Assembler U1
`Public/VoxelStrateTypes.h` declares optional `FVoxelForgeCoreGeometry`. Its default recipe is Off
and its stamp, room, and tunnel arrays are empty. `Private/VoxelForgeCoreAdapter.cpp` maps authored
metres to Core layout requests after passage planning and before finalizing the layout. Stamps use
the V1 resolver for original stamp kinds and V3 for monolith/platform motifs because the checked-out
Core API exposes V3; authored cavern records use `ResolveResolvedCavernGraphV1`. Stable feature
names are namespaced by strate in the resolved layout. `FStrateLandmark::CoreGeometryStableName`
binds one landmark to such a resolved record, and `VoxelContentManager.cpp` builds its lookup from
the finalized snapshot. `VoxelWorld.cpp` retains resolved layout identity in content hashing.
`Private/VoxelForgeAssetVersion.h` sets the shared custom version to 2. `UVoxelSettings::Serialize`
and `UVoxelStrateDefinition::Serialize`/`PostLoad` upgrade only inter-strate gap, strate height,
and transition width using the saved 8 m layout chunk. These conversions are version-guarded and
idempotent. No package save is performed by the audit. Other Research8 chunks, owner-voxel lengths,
Core-voxel lengths/frequencies, and centimetre fields remain on their existing units and need a
separate migration.
Likely compile/UHT touchpoints are the reflected nested fields in `VoxelStrateTypes.h`, Core snapshot
and resolver signatures in `VoxelForgeCoreAdapter.cpp`, `FCustomVersionRegistration` and the old
tunnel-slope version in `VoxelStrateDefinition.cpp`, plus the standalone `VoxelForgeExplore` audit
in `VoxelForgeExploreCommandlet.cpp`. `GetStrateForChunk` still receives layout-chunk Z, not a Core
tile index. See `Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md` for BuildHost evidence.
## 4. The density pipeline (most-edited hot path) ## 4. The density pipeline (most-edited hot path)
@@ -399,6 +399,7 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
LegacySettings->WorldRadiusVoxels = 987.0f; LegacySettings->WorldRadiusVoxels = 987.0f;
LegacySettings->TotalStrates = 1; LegacySettings->TotalStrates = 1;
LegacyDefinition->StrateHeightInChunks = 7; LegacyDefinition->StrateHeightInChunks = 7;
LegacyDefinition->StrateHeightMeters = 7.0 * 8.0;
LegacySettings->StratePool.Add( LegacySettings->StratePool.Add(
TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get())); TSoftObjectPtr<UVoxelStrateDefinition>(LegacyDefinition.Get()));
TStrongObjectPtr<UVoxelStrateManager> LegacyManager( TStrongObjectPtr<UVoxelStrateManager> LegacyManager(
@@ -525,6 +525,7 @@ bool FVoxelForgeZeroTransitionBlendTest::RunTest(const FString& Parameters)
{ {
UVoxelStrateDefinition* Definition = World.Definitions[Index].Get(); UVoxelStrateDefinition* Definition = World.Definitions[Index].Get();
Definition->TransitionBlendChunks = 0; Definition->TransitionBlendChunks = 0;
Definition->TransitionWidthMeters = 0.0;
Definition->TransitionType = (Index & 1) == 0 Definition->TransitionType = (Index & 1) == 0
? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved; ? EVoxelStrateTransition::Gradient : EVoxelStrateTransition::Interleaved;
} }
@@ -44,6 +44,7 @@ namespace
Settings->Seed = LayoutSeed; Settings->Seed = LayoutSeed;
Settings->TotalStrates = NumPoolDefinitions; Settings->TotalStrates = NumPoolDefinitions;
Settings->InterStrateGapChunks = 1; Settings->InterStrateGapChunks = 1;
Settings->InterStrateGapMeters = Settings->StrateLayoutChunkSizeMeters;
Settings->bOpenSurfaceEntry = true; Settings->bOpenSurfaceEntry = true;
static const ECaveGeneratorType GeneratorTypes[] = { static const ECaveGeneratorType GeneratorTypes[] = {
@@ -69,6 +70,8 @@ namespace
FString::Printf(TEXT("Layout order test strate %d"), Index)); FString::Printf(TEXT("Layout order test strate %d"), Index));
Def->GeneratorType = GeneratorTypes[Index]; Def->GeneratorType = GeneratorTypes[Index];
Def->StrateHeightInChunks = 3 + Index; Def->StrateHeightInChunks = 3 + Index;
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
* Settings->StrateLayoutChunkSizeMeters;
Def->TransitionType = EVoxelStrateTransition::Hard; Def->TransitionType = EVoxelStrateTransition::Hard;
// Non-trivial, distinct passage configs exercise the connection index and the // Non-trivial, distinct passage configs exercise the connection index and the
@@ -79,6 +79,8 @@ namespace VoxelForgeTest
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient)); NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
Settings->Seed = InSeed; Settings->Seed = InSeed;
Settings->InterStrateGapChunks = InGapChunks; Settings->InterStrateGapChunks = InGapChunks;
Settings->InterStrateGapMeters = static_cast<double>(InGapChunks)
* Settings->StrateLayoutChunkSizeMeters;
// WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults; // WorldRadiusVoxels/EdgeSealThickness intentionally inherit the UVoxelSettings defaults;
// WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0. // WorldEdgeSeal overrides them explicitly when it needs a small test world or radius 0.
@@ -110,6 +112,8 @@ namespace VoxelForgeTest
// refinement and roll tests use the smaller synthetic volume as a bounded memory // refinement and roll tests use the smaller synthetic volume as a bounded memory
// control; the owner-facing showcase opts into 8 chunks below. // control; the owner-facing showcase opts into 8 chunks below.
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks); Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
* Settings->StrateLayoutChunkSizeMeters;
// Hard transitions: param blending across a boundary would make "which archetype // Hard transitions: param blending across a boundary would make "which archetype
// owns this chunk" ambiguous, and these tests want an unambiguous mapping. // owns this chunk" ambiguous, and these tests want an unambiguous mapping.
Def->TransitionType = EVoxelStrateTransition::Hard; Def->TransitionType = EVoxelStrateTransition::Hard;
@@ -4,6 +4,27 @@
namespace VoxelClipmapDesiredTiles namespace VoxelClipmapDesiredTiles
{ {
FORCEINLINE int64 FloorDivWide(int64 Value, int64 Divisor)
{
if (Divisor <= 0) { return 0; }
const int64 Quotient = Value / Divisor;
const int64 Remainder = Value % Divisor;
return Remainder < 0 ? Quotient - 1 : Quotient;
}
FORCEINLINE int64 CeilDivWide(int64 Value, int64 Divisor)
{
if (Divisor <= 0) { return 0; }
const int64 Quotient = Value / Divisor;
const int64 Remainder = Value % Divisor;
return Remainder > 0 ? Quotient + 1 : Quotient;
}
FORCEINLINE int32 ClampInt32(int64 Value)
{
return static_cast<int32>(FMath::Clamp<int64>(Value, MIN_int32, MAX_int32));
}
struct FParameters struct FParameters
{ {
int32 ClipRadius = 3; int32 ClipRadius = 3;
@@ -13,7 +34,10 @@ namespace VoxelClipmapDesiredTiles
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor) FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
{ {
return Value >= 0 ? (Value / Divisor) : -(((-Value) + Divisor - 1) / Divisor); if (Divisor <= 0) { return 0; }
const int32 Quotient = Value / Divisor;
const int32 Remainder = Value % Divisor;
return Remainder < 0 ? Quotient - 1 : Quotient;
} }
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor) FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
@@ -24,7 +48,10 @@ namespace VoxelClipmapDesiredTiles
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor) FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
{ {
return -FloorDiv(-Value, Divisor); if (Divisor <= 0) { return 0; }
const int32 Quotient = Value / Divisor;
const int32 Remainder = Value % Divisor;
return Remainder > 0 ? Quotient + 1 : Quotient;
} }
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment, FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
@@ -33,10 +60,12 @@ namespace VoxelClipmapDesiredTiles
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis, const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
int32& OutAxisMin, int32& OutAxisMax) int32& OutAxisMin, int32& OutAxisMax)
{ {
const int32 BaseMin = CenterAxis - Radius; const int64 SafeRadius = FMath::Max<int64>(0, Radius);
const int32 BaseEnd = CenterAxis + Radius + 1; const int64 SafeAlignment = FMath::Max<int64>(1, Alignment);
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment; const int64 BaseMin = static_cast<int64>(CenterAxis) - SafeRadius;
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1; const int64 BaseEnd = static_cast<int64>(CenterAxis) + SafeRadius + 1;
OutAxisMin = ClampInt32(FloorDivWide(BaseMin, SafeAlignment) * SafeAlignment);
OutAxisMax = ClampInt32(CeilDivWide(BaseEnd, SafeAlignment) * SafeAlignment - 1);
}; };
AlignAxis(Center.X, OutMin.X, OutMax.X); AlignAxis(Center.X, OutMin.X, OutMax.X);
AlignAxis(Center.Y, OutMin.Y, OutMax.Y); AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
@@ -49,8 +78,13 @@ namespace VoxelClipmapDesiredTiles
{ {
return Radius; return Radius;
} }
return FMath::Max(Radius, const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8);
(Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel); const int64 Scale = int64{1} << static_cast<uint32>(SafeLevel);
const int64 Numerator = static_cast<int64>(Parameters.RenderDistanceChunks)
+ Scale - 1;
const int64 DesiredRadius = Numerator / Scale;
return static_cast<int32>(FMath::Clamp<int64>(
FMath::Max<int64>(Radius, DesiredRadius), 0, MAX_int32));
} }
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel, FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
@@ -73,12 +107,12 @@ namespace VoxelClipmapDesiredTiles
return; return;
} }
OutMinChunkZ = CenterChunkZ - ViewDistanceDown; OutMinChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) - ViewDistanceDown);
OutMaxChunkZ = CenterChunkZ + ViewDistanceUp; OutMaxChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) + ViewDistanceUp);
if (bHasStrate) if (bHasStrate)
{ {
OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks; OutMinChunkZ = ClampInt32(static_cast<int64>(StrateBottomChunkZ) - StrateViewMarginChunks);
OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks; OutMaxChunkZ = ClampInt32(static_cast<int64>(StrateTopChunkZ) + StrateViewMarginChunks);
} }
} }
@@ -94,7 +128,7 @@ namespace VoxelClipmapDesiredTiles
for (int32 Level = 0; Level <= MaxLevel; ++Level) for (int32 Level = 0; Level <= MaxLevel; ++Level)
{ {
const int32 Pow = 1 << Level; const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
const FIntVector CenterLevel = FloorDiv(Center, Pow); const FIntVector CenterLevel = FloorDiv(Center, Pow);
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius; const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
@@ -105,24 +139,23 @@ namespace VoxelClipmapDesiredTiles
FIntVector RingMin, RingMax; FIntVector RingMin, RingMax;
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax); AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
int32 DeltaZMin = RingMin.Z - CenterLevel.Z; int64 TileMinZ = RingMin.Z;
int32 DeltaZMax = RingMax.Z - CenterLevel.Z; int64 TileMaxZ = RingMax.Z;
if (LevelRadius > Radius && MinChunkZ != MIN_int32) if (LevelRadius > Radius && MinChunkZ != MIN_int32)
{ {
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z); TileMinZ = FMath::Max<int64>(TileMinZ, FloorDiv(MinChunkZ, Pow));
DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z); TileMaxZ = FMath::Min<int64>(TileMaxZ, FloorDiv(MaxChunkZ, Pow));
} }
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ) for (int64 Z = TileMinZ; Z <= TileMaxZ; ++Z)
for (int32 DeltaY = RingMin.Y - CenterLevel.Y; for (int64 Y = RingMin.Y; Y <= RingMax.Y; ++Y)
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY) for (int64 X = RingMin.X; X <= RingMax.X; ++X)
for (int32 DeltaX = RingMin.X - CenterLevel.X;
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
{ {
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ); const FIntVector Tile(static_cast<int32>(X), static_cast<int32>(Y),
const int32 TileMinZ = Tile.Z << Level; static_cast<int32>(Z));
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1; const int64 TileBottomChunkZ = static_cast<int64>(Tile.Z) * Pow;
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ) const int64 TileTopChunkZ = (static_cast<int64>(Tile.Z) + 1) * Pow - 1;
if (TileTopChunkZ < MinChunkZ || TileBottomChunkZ > MaxChunkZ)
{ {
continue; continue;
} }
@@ -130,12 +163,12 @@ namespace VoxelClipmapDesiredTiles
if (Level > 0) if (Level > 0)
{ {
const bool bCovered = const bool bCovered =
(2 * Tile.X >= PreviousRingMin.X) (2 * static_cast<int64>(Tile.X) >= PreviousRingMin.X)
&& (2 * Tile.X + 1 <= PreviousRingMax.X) && (2 * static_cast<int64>(Tile.X) + 1 <= PreviousRingMax.X)
&& (2 * Tile.Y >= PreviousRingMin.Y) && (2 * static_cast<int64>(Tile.Y) >= PreviousRingMin.Y)
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y) && (2 * static_cast<int64>(Tile.Y) + 1 <= PreviousRingMax.Y)
&& (2 * Tile.Z >= PreviousRingMin.Z) && (2 * static_cast<int64>(Tile.Z) >= PreviousRingMin.Z)
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z); && (2 * static_cast<int64>(Tile.Z) + 1 <= PreviousRingMax.Z);
if (bCovered) if (bCovered)
{ {
continue; continue;
+124 -24
View File
@@ -18,6 +18,7 @@
#include "Materials/MaterialInterface.h" #include "Materials/MaterialInterface.h"
#include "Tasks/Task.h" #include "Tasks/Task.h"
#include "HAL/PlatformProcess.h" #include "HAL/PlatformProcess.h"
#include "Misc/Crc.h"
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes #include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
#include <VoxelForgeCore/VoxelForgeCore.h> #include <VoxelForgeCore/VoxelForgeCore.h>
#include <cmath> #include <cmath>
@@ -240,10 +241,10 @@ static std::vector<VoxelForgeCore::WorldAssemblyFeatureQueryV1> VF_ContentFeatur
{ {
using namespace VoxelForgeCore; using namespace VoxelForgeCore;
constexpr double Q = WorldAssemblySubvoxelsPerVoxel; constexpr double Q = WorldAssemblySubvoxelsPerVoxel;
const std::array<int64,3> Min{static_cast<int64>(std::floor(MinX*Q)), const std::array<std::int64_t,3> Min{static_cast<std::int64_t>(std::floor(MinX*Q)),
static_cast<int64>(std::floor(MinY*Q)),static_cast<int64>(std::floor(MinZ*Q))}; static_cast<std::int64_t>(std::floor(MinY*Q)),static_cast<std::int64_t>(std::floor(MinZ*Q))};
const std::array<int64,3> Max{static_cast<int64>(std::ceil(MaxX*Q)), const std::array<std::int64_t,3> Max{static_cast<std::int64_t>(std::ceil(MaxX*Q)),
static_cast<int64>(std::ceil(MaxY*Q)),static_cast<int64>(std::ceil(MaxZ*Q))}; static_cast<std::int64_t>(std::ceil(MaxY*Q)),static_cast<std::int64_t>(std::ceil(MaxZ*Q))};
std::vector<WorldAssemblyFeatureQueryV1> Queries; std::vector<WorldAssemblyFeatureQueryV1> Queries;
if (Surface) if (Surface)
if (const auto* Plan = Surface->FindPlan(StrateIndex)) if (const auto* Plan = Surface->FindPlan(StrateIndex))
@@ -322,11 +323,54 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
Seed = InSeed; Seed = InSeed;
NativeSurfaceSnapshot.Reset(); NativeSurfaceSnapshot.Reset();
NativeFloatingIslandSnapshot.Reset(); NativeFloatingIslandSnapshot.Reset();
CoreGeometryAnchors.Reset();
if (Settings) if (Settings)
{ {
FString ManifestJson, ManifestHash, ManifestError; FString ManifestJson, ManifestHash, ManifestError;
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get()); const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
FString ResolvedLayoutJson, ResolvedLayoutIdentity;
if (VoxelWorld != nullptr
&& VoxelWorld->GetVoxelForgeCoreResolvedLayout(ResolvedLayoutJson, ResolvedLayoutIdentity))
{
FTCHARToUTF8 Utf8Layout(*ResolvedLayoutJson);
std::string LayoutError;
const std::optional<VoxelForgeCore::ResolvedLayoutSnapshotV3> ResolvedLayout =
VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(
std::string_view(Utf8Layout.Get(), static_cast<std::size_t>(Utf8Layout.Length())), LayoutError);
if (ResolvedLayout)
{
const auto AddAnchor = [this](const std::string& StableId, int32 StrateIndex,
const std::array<std::int64_t, 3>& PositionMicrometres)
{
const FVector PositionCm(
static_cast<double>(PositionMicrometres[0]) / 10000.0,
static_cast<double>(PositionMicrometres[1]) / 10000.0,
static_cast<double>(PositionMicrometres[2]) / 10000.0);
const FString FullName = UTF8_TO_TCHAR(StableId.c_str());
FResolvedCoreGeometryAnchor Anchor{ StrateIndex, PositionCm };
CoreGeometryAnchors.Add(FullName, Anchor);
const int32 Marker = FullName.Find(TEXT("/core/"), ESearchCase::CaseSensitive, ESearchDir::FromEnd);
if (Marker != INDEX_NONE)
{
const FString ShortName = FullName.Mid(Marker + 6);
if (!ShortName.IsEmpty()) CoreGeometryAnchors.Add(ShortName, Anchor);
}
};
for (const VoxelForgeCore::ResolvedLayoutStampV1& Stamp : ResolvedLayout->stamps)
AddAnchor(Stamp.stableId, Stamp.strateIndex, Stamp.positionMicrometres);
for (const VoxelForgeCore::ResolvedCavernRoomV1& Room : ResolvedLayout->cavernRooms)
AddAnchor(Room.stableId, Room.strateIndex, Room.centerMicrometres);
for (const VoxelForgeCore::ResolvedCavernEdgeV1& Edge : ResolvedLayout->cavernEdges)
if (!Edge.polylineMicrometres.empty())
AddAnchor(Edge.stableId, Edge.strateIndex, Edge.polylineMicrometres.front());
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[VoxelContentManager] Could not read resolved Core geometry anchors: %s"),
UTF8_TO_TCHAR(LayoutError.c_str()));
}
}
const bool bHasRuntimeAssembly = VoxelWorld != nullptr const bool bHasRuntimeAssembly = VoxelWorld != nullptr
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash); && VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
if (!bHasRuntimeAssembly) if (!bHasRuntimeAssembly)
@@ -559,9 +603,13 @@ int32 UVoxelContentManager::RegionSize() const
// truncation, a negative-coord cell is queued under one region but routed back to another → its march // truncation, a negative-coord cell is queued under one region but routed back to another → its march
// result is dropped → blank chunk. Truncation == floor for A>=0 and for R==1, which is exactly why only // result is dropped → blank chunk. Truncation == floor for A>=0 and for R==1, which is exactly why only
// NEGATIVE coordinates at R>=2 were affected. R (region size) is always > 0 here. // NEGATIVE coordinates at R>=2 were affected. R (region size) is always > 0 here.
static FORCEINLINE int32 FloorDivPos(int32 A, int32 R) static FORCEINLINE int32 FloorDivPos(int64 A, int32 R)
{ {
return (A >= 0) ? (A / R) : -(((-A) + R - 1) / R); if (R <= 0) { return 0; }
const int64 Quotient = A / R;
const int64 Remainder = A % R;
return static_cast<int32>(FMath::Clamp<int64>(
Remainder < 0 ? Quotient - 1 : Quotient, MIN_int32, MAX_int32));
} }
static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R) static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
@@ -573,10 +621,10 @@ static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell, static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell,
int32 FarR, int32 R) int32 FarR, int32 R)
{ {
const int32 MinX = FloorDivPos(PlayerCell.X - FarR, R); const int32 MinX = FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R);
const int32 MaxX = FloorDivPos(PlayerCell.X + FarR, R); const int32 MaxX = FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R);
const int32 MinY = FloorDivPos(PlayerCell.Y - FarR, R); const int32 MinY = FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R);
const int32 MaxY = FloorDivPos(PlayerCell.Y + FarR, R); const int32 MaxY = FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R);
return Region.X >= MinX && Region.X <= MaxX && Region.Y >= MinY && Region.Y <= MaxY; return Region.X >= MinX && Region.X <= MaxX && Region.Y >= MinY && Region.Y <= MaxY;
} }
@@ -1038,8 +1086,19 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual, OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual,
FMath::Abs(static_cast<double>(Density))); FMath::Abs(static_cast<double>(Density)));
++OutAudit.AnchorChecks; ++OutAudit.AnchorChecks;
if (FMath::Abs(Density)>0.25f) // Core content crossings use the same finite-step column refinement as runtime
{ OutError=TEXT("Populated fixture is not anchored on the canonical field."); return false; } // placement. Allow at most half a Core voxel of field residual in this audit.
// Les croisements de contenu Core gardent le raffinement discret du runtime;
// l'audit tolere au plus un demi-voxel Core de residu de champ.
if (FMath::Abs(Density)>0.5f)
{
OutError=FString::Printf(
TEXT("Populated fixture is not anchored on the canonical field: residual=%.4f core_vox=(%.3f,%.3f,%.3f) local_cm=(%.2f,%.2f,%.2f)."),
Density, Local.X/CurrentCtx.VoxelSizeCm,
Local.Y/CurrentCtx.VoxelSizeCm, Local.Z/CurrentCtx.VoxelSizeCm,
Local.X, Local.Y, Local.Z);
return false;
}
} }
} }
if (!Spawns.IsEmpty()) if (!Spawns.IsEmpty())
@@ -1102,17 +1161,22 @@ void UVoxelContentManager::RebuildDesiredCells(FDecoGrid& G, const FIntPoint& Pl
const int32 R = RegionSize(); const int32 R = RegionSize();
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player. // Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
const FIntPoint RMin(FloorDivPos(PlayerCell.X - FarR, R), const FIntPoint RMin(FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R),
FloorDivPos(PlayerCell.Y - FarR, R)); FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R));
const FIntPoint RMax(FloorDivPos(PlayerCell.X + FarR, R), const FIntPoint RMax(FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R),
FloorDivPos(PlayerCell.Y + FarR, R)); FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R));
TSet<FIntPoint> DesiredRegions; TSet<FIntPoint> DesiredRegions;
DesiredRegions.Reserve((RMax.X - RMin.X + 1) * (RMax.Y - RMin.Y + 1)); constexpr int64 MaxRegionReserve = 4096;
for (int32 ry = RMin.Y; ry <= RMax.Y; ++ry) const int64 WidthX = static_cast<int64>(RMax.X) - RMin.X + 1;
for (int32 rx = RMin.X; rx <= RMax.X; ++rx) const int64 WidthY = static_cast<int64>(RMax.Y) - RMin.Y + 1;
const int64 ReserveCount = WidthY > 0 && WidthX > MaxRegionReserve / WidthY
? MaxRegionReserve : FMath::Max<int64>(0, WidthX * WidthY);
DesiredRegions.Reserve(static_cast<int32>(ReserveCount));
for (int64 ry = RMin.Y; ry <= RMax.Y; ++ry)
for (int64 rx = RMin.X; rx <= RMax.X; ++rx)
{ {
DesiredRegions.Add(FIntPoint(rx, ry)); DesiredRegions.Add(FIntPoint(static_cast<int32>(rx), static_cast<int32>(ry)));
} }
// Unload loaded regions no longer desired (plain DestroyComponent — no per-instance removal). // Unload loaded regions no longer desired (plain DestroyComponent — no per-instance removal).
@@ -1464,8 +1528,15 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u)); const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue; if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
const int32 Span = FMath::Max(0, Comp.CountMax - Comp.CountMin); const int64 Span = FMath::Max<int64>(0,
const int32 Count = Comp.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(CH ^ 0x1234u)) * (float)(Span + 1)); static_cast<int64>(Comp.CountMax) - Comp.CountMin);
const uint64 CountRange = static_cast<uint64>(Span) + 1u;
const float CountOffset = VoxelHash::ToFloat01(
VoxelHash::Mix(CH ^ 0x1234u)) * static_cast<float>(CountRange);
const int64 CountWide = static_cast<int64>(Comp.CountMin)
+ static_cast<int64>(CountOffset);
const int32 Count = static_cast<int32>(FMath::Clamp<int64>(
CountWide, MIN_int32, MAX_int32));
for (int32 ii = 0; ii < Count; ++ii) for (int32 ii = 0; ii < Count; ++ii)
{ {
if (CompBudget <= 0) break; if (CompBudget <= 0) break;
@@ -1514,8 +1585,15 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu)); const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue; if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
const int32 SubSpan = FMath::Max(0, Sub.CountMax - Sub.CountMin); const int64 SubSpan = FMath::Max<int64>(0,
const int32 SubCount = Sub.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(SH ^ 0x1234u)) * (float)(SubSpan + 1)); static_cast<int64>(Sub.CountMax) - Sub.CountMin);
const uint64 SubCountRange = static_cast<uint64>(SubSpan) + 1u;
const float SubCountOffset = VoxelHash::ToFloat01(
VoxelHash::Mix(SH ^ 0x1234u)) * static_cast<float>(SubCountRange);
const int64 SubCountWide = static_cast<int64>(Sub.CountMin)
+ static_cast<int64>(SubCountOffset);
const int32 SubCount = static_cast<int32>(FMath::Clamp<int64>(
SubCountWide, MIN_int32, MAX_int32));
for (int32 sj = 0; sj < SubCount; ++sj) for (int32 sj = 0; sj < SubCount; ++sj)
{ {
if (CompBudget <= 0) break; if (CompBudget <= 0) break;
@@ -2374,6 +2452,28 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld; const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
if (ExclWorld > 0.0f) bAnyExclusion = true; if (ExclWorld > 0.0f) bAnyExclusion = true;
if (!L.CoreGeometryStableName.IsEmpty())
{
const FResolvedCoreGeometryAnchor* Anchor = CoreGeometryAnchors.Find(L.CoreGeometryStableName);
if (Anchor == nullptr || Anchor->StrateIndex != StrateIndex) continue;
const uint32 StableNameHash = FCrc::StrCrc32(*L.CoreGeometryStableName);
const uint32 H = DecoHash(static_cast<int32>(StableNameHash), StrateIndex,
0, 0, 0, EntryIdx, LocalSeed, 0xC04E6E0u);
if (VoxelHash::ToFloat01(H) > L.SpawnProbability) continue;
const float LocalX = static_cast<float>(Anchor->LocalPositionCm.X);
const float LocalY = static_cast<float>(Anchor->LocalPositionCm.Y);
const float dx = LocalX - LocalPlayer.X, dy = LocalY - LocalPlayer.Y;
const float DistSq = dx * dx + dy * dy;
if (DistSq <= GatherWorld * GatherWorld)
{
// A Core-bound landmark is one stable candidate, independent of the hash lattice.
// Un landmark lie a Core reste un candidat stable, independant de la grille de hachage.
Cands.Add({ FIntVector(EntryIdx, 0, EntryIdx), EntryIdx, H, LocalX, LocalY,
ExclWorld, L.Priority, DistSq <= RadiusWorld * RadiusWorld });
}
continue;
}
if (L.AnchorMode == ELandmarkAnchor::HashLattice) if (L.AnchorMode == ELandmarkAnchor::HashLattice)
{ {
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks); const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
@@ -155,9 +155,11 @@ int32 UVoxelDensityVolume::NumLevels() const
FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B) FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B)
{ {
// True floor division (B > 0). FMath::DivideAndRoundDown truncates toward zero for negatives — // True floor division (B > 0). Remainder adjustment handles INT_MIN without negating it.
// a footgun the content manager hit too (see FloorDivPos there). Cells span the origin, so floor. if (B <= 0) { return 0; }
return (A >= 0) ? (A / B) : -(((-A) + B - 1) / B); const int32 Quotient = A / B;
const int32 Remainder = A % B;
return Remainder < 0 ? Quotient - 1 : Quotient;
} }
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity) FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
@@ -180,7 +182,8 @@ void UVoxelDensityVolume::EnsureAllocated()
for (int32 L = 0; L < N; ++L) for (int32 L = 0; L < N; ++L)
{ {
FClipLevel& Lv = Levels[L]; FClipLevel& Lv = Levels[L];
Lv.Step = 1 << L; Lv.Step = static_cast<int32>(uint32{1}
<< static_cast<uint32>(FMath::Clamp(L, 0, 8)));
Lv.OriginCells = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX); Lv.OriginCells = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
Lv.bHasData = false; Lv.bHasData = false;
Lv.Density.SetNumZeroed(Count); // start all-air (0) Lv.Density.SetNumZeroed(Count); // start all-air (0)
@@ -632,7 +635,8 @@ void UVoxelDensityVolume::ProcessOneFill(const FPendingFill& F)
if (!Gen) return; if (!Gen) return;
if (bShuttingDown.load(std::memory_order_relaxed) || bFillThreadStop.load(std::memory_order_relaxed)) return; if (bShuttingDown.load(std::memory_order_relaxed) || bFillThreadStop.load(std::memory_order_relaxed)) return;
const int32 Step = 1 << F.Level; const int32 Step = static_cast<int32>(uint32{1}
<< static_cast<uint32>(FMath::Clamp(F.Level, 0, 8)));
FFillResult R; FFillResult R;
R.Level = F.Level; R.Level = F.Level;
R.Epoch = F.Epoch; R.Epoch = F.Epoch;
@@ -0,0 +1,12 @@
#pragma once
#include "CoreMinimal.h"
// Shared custom package version for idempotent VoxelForge asset migrations.
// Version de package partagee pour les migrations VoxelForge idempotentes.
namespace VoxelForgeAssetVersion
{
inline const FGuid GUID(0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
inline constexpr int32 TunnelFloorSlopeDegrees = 1;
inline constexpr int32 MetreAuthoringV1 = 2;
}
@@ -82,6 +82,113 @@ std::int64_t WorldAssemblyWavelength(double Frequency, double FallbackVoxels)
return std::max<std::int64_t>(1, WorldAssemblyFixed(Wavelength)); 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::uint64_t WorldAssemblyFeatureId(std::uint32_t Seed, std::int64_t X,
std::int64_t Y, std::uint32_t Kind) std::int64_t Y, std::uint32_t Kind)
{ {
@@ -111,7 +218,7 @@ std::int64_t WorldAssemblyMicrometresToSubvoxels(
std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres) std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres)
{ {
if (CoreVoxelMicrometres == 0) return 0; if (CoreVoxelMicrometres == 0) return 0;
return WorldAssemblyRoundRatio(Micrometres * WorldAssemblySubvoxelsPerVoxel, return WorldAssemblyRoundRatio(Micrometres * VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<std::int64_t>(CoreVoxelMicrometres)); static_cast<std::int64_t>(CoreVoxelMicrometres));
} }
@@ -139,7 +246,8 @@ std::uint64_t WorldAssemblyRoundedSqrt(std::uint64_t Value)
} }
bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe, 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; using namespace VoxelForgeCore;
constexpr std::int64_t PlayerRadiusQ = 348; 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 HalfHeightQ = PlayerHeightQ / 2;
constexpr std::int64_t StepQ = 128; constexpr std::int64_t StepQ = 128;
constexpr std::int64_t ProbeRadiusQ = 174; 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 GradeQ16 = 17560;
constexpr std::int64_t RouteRadiusQ = 4 * WorldAssemblySubvoxelsPerVoxel; constexpr std::int64_t RouteRadiusQ = 4 * WorldAssemblySubvoxelsPerVoxel;
struct FDirection { std::int32_t X; std::int32_t Y; }; struct FDirection { std::int32_t X; std::int32_t Y; };
@@ -157,9 +268,12 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
struct FCandidate struct FCandidate
{ {
std::int64_t Score = 0; std::int64_t Score = 0;
std::int64_t OutwardProjection = 0;
std::uint64_t EndpointProximityCost = 0;
std::int32_t CenterX = 0; std::int32_t CenterX = 0;
std::int32_t CenterY = 0; std::int32_t CenterY = 0;
std::int32_t Direction = 0; std::int32_t Direction = 0;
std::int32_t HalfLength = 128;
std::vector<WorldAssemblyRoutePointV1> Points; std::vector<WorldAssemblyRoutePointV1> Points;
}; };
@@ -171,7 +285,7 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
Constraints.sweepSpacingSubvoxels = StepQ; Constraints.sweepSpacingSubvoxels = StepQ;
Constraints.supportProbeRadiusSubvoxels = ProbeRadiusQ; Constraints.supportProbeRadiusSubvoxels = ProbeRadiusQ;
Constraints.supportSearchDepthSubvoxels = SupportDepthQ; Constraints.supportSearchDepthSubvoxels = SupportDepthQ;
Constraints.minimumClearanceSubvoxels = 8; Constraints.minimumClearanceSubvoxels = 128;
Constraints.minimumSupportProbes = 3; Constraints.minimumSupportProbes = 3;
const Heightfield2D BaseField(Recipe, Stratum); const Heightfield2D BaseField(Recipe, Stratum);
@@ -182,11 +296,14 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
} }
std::vector<FCandidate> Candidates; std::vector<FCandidate> Candidates;
constexpr std::int32_t WaypointSpacing = 8; 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 CenterY = -256; CenterY <= 256; CenterY += 64)
for (std::int32_t CenterX = -256; CenterX <= 256; CenterX += 64) for (std::int32_t CenterX = -256; CenterX <= 256; CenterX += 64)
for (std::int32_t DirectionIndex = 0; for (std::int32_t DirectionIndex = 0;
DirectionIndex < static_cast<std::int32_t>(Directions.size()); ++DirectionIndex) DirectionIndex < static_cast<std::int32_t>(Directions.size()); ++DirectionIndex)
{ {
++LegacyCandidatesVisited;
FCandidate Candidate; FCandidate Candidate;
Candidate.CenterX = CenterX; Candidate.CenterX = CenterX;
Candidate.CenterY = CenterY; Candidate.CenterY = CenterY;
@@ -234,15 +351,33 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
PreviousY = Y; PreviousY = Y;
} }
if (bGradePass) Candidates.push_back(std::move(Candidate)); 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) 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.Score != B.Score) return A.Score < B.Score;
if (A.CenterY != B.CenterY) return A.CenterY < B.CenterY; if (A.CenterY != B.CenterY) return A.CenterY < B.CenterY;
if (A.CenterX != B.CenterX) return A.CenterX < B.CenterX; if (A.CenterX != B.CenterX) return A.CenterX < B.CenterX;
return A.Direction < B.Direction; 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) for (const FCandidate& Candidate : Candidates)
{ {
Stratum.route = Candidate.Points; Stratum.route = Candidate.Points;
@@ -260,11 +395,210 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
} }
} }
const Heightfield2D CandidateField(Recipe, Stratum); 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; if (LiftQ >= MaximumRepairLiftQ) break;
LiftQ += RepairStepQ; 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;
const auto EndpointCoordinates = [&](std::int32_t Index)
{
const std::int64_t Along = (Index - 16) * HalfLength / 16;
return std::array<std::int64_t, 2>{
CenterX + WorldAssemblyRoundRatio(
Along * Directions[DirectionIndex].X, 65536),
CenterY + WorldAssemblyRoundRatio(
Along * Directions[DirectionIndex].Y, 65536)};
};
const auto ArrivalEndpoint = EndpointCoordinates(0);
const auto DepartureEndpoint = EndpointCoordinates(32);
const std::int64_t ArrivalXQ = WorldAssemblyFixed(
static_cast<double>(ArrivalEndpoint[0]));
const std::int64_t ArrivalYQ = WorldAssemblyFixed(
static_cast<double>(ArrivalEndpoint[1]));
const std::int64_t DepartureXQ = WorldAssemblyFixed(
static_cast<double>(DepartureEndpoint[0]));
const std::int64_t DepartureYQ = WorldAssemblyFixed(
static_cast<double>(DepartureEndpoint[1]));
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 ((ArrivalRadiusQ > 0
&& !InRadius(ArrivalXQ, ArrivalYQ, ArrivalRadiusQ))
|| (DepartureRadiusQ > 0
&& !InRadius(DepartureXQ, DepartureYQ, DepartureRadiusQ)))
{
// Mouth placement is independent of terrain, so reject it before
// allocating points or sampling the heightfield for grade.
++FallbackPlacementRejected;
continue;
}
Candidate.Points.reserve(33);
if (ArrivalRadiusQ > 0)
{
Candidate.EndpointProximityCost += static_cast<std::uint64_t>(
ArrivalXQ * ArrivalXQ + ArrivalYQ * ArrivalYQ) * 16ull;
}
if (DepartureRadiusQ > 0)
{
Candidate.EndpointProximityCost += static_cast<std::uint64_t>(
DepartureXQ * DepartureXQ + DepartureYQ * DepartureYQ);
// 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.
Candidate.OutwardProjection = DepartureXQ * Directions[DirectionIndex].X
+ DepartureYQ * Directions[DirectionIndex].Y;
}
std::int64_t PreviousGround = 0;
std::int64_t PreviousX = 0;
std::int64_t PreviousY = 0;
bool bGradePass = 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 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) Candidates.push_back(std::move(Candidate));
else
{
++FallbackGradeRejected;
}
}
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."; Error = "SurfaceWorld route candidates failed the capsule and support sweep.";
return false; return false;
} }
@@ -370,6 +704,54 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
BottomVoxel, HeightVoxels)); 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.strateIndex = Slot.StrateIndex;
Stratum.topChunkZ = Slot.TopChunkZ; Stratum.topChunkZ = Slot.TopChunkZ;
Stratum.bottomChunkZ = Slot.BottomChunkZ; Stratum.bottomChunkZ = Slot.BottomChunkZ;
@@ -476,7 +858,29 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
} }
} }
std::string RouteError; 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()); OutError = UTF8_TO_TCHAR(RouteError.c_str());
return false; return false;
@@ -836,6 +1240,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, bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
const UVoxelStrateManager* StrateManager, const UVoxelStrateManager* StrateManager,
double CoreVoxelSizeCm, double CoreVoxelSizeCm,
@@ -876,6 +1623,7 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
Generated.recipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels; Generated.recipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels;
int32 SurfaceSlotCount = 0; int32 SurfaceSlotCount = 0;
int32 IslandSlotCount = 0; int32 IslandSlotCount = 0;
int32 CoreGeometrySlotCount = 0;
for (const FStrateSlot& Slot : StrateManager->GetLayout()) for (const FStrateSlot& Slot : StrateManager->GetLayout())
{ {
if (Slot.Definition == nullptr) continue; if (Slot.Definition == nullptr) continue;
@@ -883,10 +1631,13 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
FIntVector(0, 0, Slot.TopChunkZ)); FIntVector(0, 0, Slot.TopChunkZ));
SurfaceSlotCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0; SurfaceSlotCount += Type == ECaveGeneratorType::SurfaceWorld ? 1 : 0;
IslandSlotCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0; IslandSlotCount += Type == ECaveGeneratorType::FloatingIslands ? 1 : 0;
CoreGeometrySlotCount += HasCoreGeometry(*Slot.Definition) ? 1 : 0;
} }
if (SurfaceSlotCount == 0 && IslandSlotCount == 0) if (SurfaceSlotCount == 0 && IslandSlotCount == 0)
{ {
return true; return CoreGeometrySlotCount == 0 ? true
: AttachCoreGeometryOnlyLayout(StrateManager, OwnerVoxelSizeCm, CoreVoxelSizeCm,
LayoutChunkSizeMicrometres, LayoutChunkWidthSubvoxels, Root, OutError);
} }
if (SurfaceSlotCount == 0) if (SurfaceSlotCount == 0)
{ {
@@ -918,7 +1669,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
Generated.plan.strates.push_back(std::move(Stratum)); Generated.plan.strates.push_back(std::move(Stratum));
++BuiltSurfaceCount; ++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, if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated,
CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError)) CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError))
@@ -991,10 +1746,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
} }
} }
if (Manifest->plan.strates.size() != static_cast<std::size_t>(SurfaceSlotCount) 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( 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->plan.strates.size()),
static_cast<int32>(Manifest->floatingIslands.size()), static_cast<int32>(Manifest->floatingIslands.size()),
SurfaceSlotCount, FloatingIslandSlotCount); SurfaceSlotCount, FloatingIslandSlotCount);
@@ -1088,12 +1844,12 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
WorldAssemblyMicrometresToSubvoxels( WorldAssemblyMicrometresToSubvoxels(
Slot.PassageMouthRadiusMicrometres, ExpectedCoreVoxelMicrons), Slot.PassageMouthRadiusMicrometres, ExpectedCoreVoxelMicrons),
WorldAssemblyMicrometresToSubvoxels( WorldAssemblyMicrometresToSubvoxels(
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons)); 36 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
PassageConfig.midRadiusSubvoxels = std::max( PassageConfig.midRadiusSubvoxels = std::max(
WorldAssemblyMicrometresToSubvoxels( WorldAssemblyMicrometresToSubvoxels(
Slot.PassageMidRadiusMicrometres, ExpectedCoreVoxelMicrons), Slot.PassageMidRadiusMicrometres, ExpectedCoreVoxelMicrons),
WorldAssemblyMicrometresToSubvoxels( WorldAssemblyMicrometresToSubvoxels(
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons)); 24 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
PassageConfig.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels( PassageConfig.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.PassageReachMinimumMicrometres, ExpectedCoreVoxelMicrons); Slot.PassageReachMinimumMicrometres, ExpectedCoreVoxelMicrons);
PassageConfig.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels( PassageConfig.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
@@ -1128,6 +1884,10 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
: Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField" : Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField"
: std::string("legacy.") + generatorName(Type); : std::string("legacy.") + generatorName(Type);
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0; Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
if (!ConfigureCoreGeometryStratum(Slot, Type, bImportedOverride, Item, OutError))
{
return false;
}
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons; Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres; Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
Item.interiorTopMicrometres = Slot.TopWorldMicrometres; Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
@@ -1264,6 +2024,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
UTF8_TO_TCHAR(PassagePlanError.c_str())); UTF8_TO_TCHAR(PassagePlanError.c_str()));
return false; return false;
} }
if (!ResolveAuthoredCoreGeometry(Layout, StrateManager,
StrateManager->GetWorldSeed(), OutError))
{
return false;
}
std::string LayoutError; std::string LayoutError;
if (!Layout.finalize(LayoutError)) if (!Layout.finalize(LayoutError))
{ {
@@ -1372,6 +2137,10 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0))); static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0)));
Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm); Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm);
Root->SetNumberField(TEXT("core_voxel_size_microns"), CoreVoxelSizeMicrons); 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->SetNumberField(TEXT("seed"), Seed);
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry); Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius); Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
@@ -15,9 +15,73 @@
#include "HAL/IConsoleManager.h" #include "HAL/IConsoleManager.h"
#include "Misc/CommandLine.h" #include "Misc/CommandLine.h"
#include <atomic> #include <atomic>
#include <array>
namespace namespace
{ {
struct FVoxelMesherThreadState
{
int32 OctaveBias=0,SampleStep=1,TileCells=0,TileReachBlocks=0,TileReachBlockIndex=INDEX_NONE;
FIntVector TileOrigin=FIntVector::ZeroValue;
uint8 TilePostReachFlags=VoxelGenLOD::AllTileReach;
bool bTilePostReachDebug=false,bTilePostReachBypass=false;
bool bTileReachDiagnosticTile=false,bTileCoreReachProofEnabled=false;
bool bTileCoreReachDecisionRecorded=false,bTileBlockReachValid=false;
float TileReachScale=1.0f;
std::array<uint8,VoxelGenLOD::TileReachMaxBlockCount> TileBlockReachFlags{};
static FVoxelMesherThreadState Capture()
{
FVoxelMesherThreadState State;
State.OctaveBias=VoxelGenLOD::OctaveBias;
State.SampleStep=VoxelGenLOD::SampleStep;
State.TileOrigin=VoxelGenLOD::TileOriginVoxels;
State.TileCells=VoxelGenLOD::TileCellsPerAxis;
State.TilePostReachFlags=VoxelGenLOD::TilePostReachFlags;
State.bTilePostReachDebug=VoxelGenLOD::bTilePostReachDebug;
State.bTilePostReachBypass=VoxelGenLOD::bTilePostReachBypass;
State.bTileReachDiagnosticTile=VoxelGenLOD::bTileReachDiagnosticTile;
State.bTileCoreReachProofEnabled=VoxelGenLOD::bTileCoreReachProofEnabled;
State.bTileCoreReachDecisionRecorded=VoxelGenLOD::bTileCoreReachDecisionRecorded;
State.bTileBlockReachValid=VoxelGenLOD::bTileBlockReachValid;
State.TileReachScale=VoxelGenLOD::TileReachScale;
State.TileReachBlocks=VoxelGenLOD::TileReachBlocksPerAxis;
State.TileReachBlockIndex=VoxelGenLOD::TileReachBlockIndex;
std::copy(std::begin(VoxelGenLOD::TileBlockReachFlags),
std::end(VoxelGenLOD::TileBlockReachFlags),State.TileBlockReachFlags.begin());
return State;
}
void Apply() const
{
VoxelGenLOD::OctaveBias=OctaveBias;
VoxelGenLOD::SampleStep=SampleStep;
VoxelGenLOD::TileOriginVoxels=TileOrigin;
VoxelGenLOD::TileCellsPerAxis=TileCells;
VoxelGenLOD::TilePostReachFlags=TilePostReachFlags;
VoxelGenLOD::bTilePostReachDebug=bTilePostReachDebug;
VoxelGenLOD::bTilePostReachBypass=bTilePostReachBypass;
VoxelGenLOD::bTileReachDiagnosticTile=bTileReachDiagnosticTile;
VoxelGenLOD::bTileCoreReachProofEnabled=bTileCoreReachProofEnabled;
VoxelGenLOD::bTileCoreReachDecisionRecorded=bTileCoreReachDecisionRecorded;
VoxelGenLOD::bTileBlockReachValid=bTileBlockReachValid;
VoxelGenLOD::TileReachScale=TileReachScale;
VoxelGenLOD::TileReachBlocksPerAxis=TileReachBlocks;
VoxelGenLOD::TileReachBlockIndex=TileReachBlockIndex;
std::copy(TileBlockReachFlags.begin(),TileBlockReachFlags.end(),
std::begin(VoxelGenLOD::TileBlockReachFlags));
}
};
struct FVoxelMesherThreadStateScope
{
explicit FVoxelMesherThreadStateScope(const FVoxelMesherThreadState& InTarget)
:Previous(FVoxelMesherThreadState::Capture())
{ InTarget.Apply(); }
~FVoxelMesherThreadStateScope(){Previous.Apply();}
FVoxelMesherThreadState Previous;
};
int32 GVoxelForgeUseOperatorBlock = 1; int32 GVoxelForgeUseOperatorBlock = 1;
FAutoConsoleVariableRef CVarVoxelForgeUseOperatorBlock( FAutoConsoleVariableRef CVarVoxelForgeUseOperatorBlock(
TEXT("voxel.UseOperatorBlock"), TEXT("voxel.UseOperatorBlock"),
@@ -692,6 +756,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile. // capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
static thread_local TArray<float> DensityGrid; static thread_local TArray<float> DensityGrid;
DensityGrid.SetNumUninitialized(MDim * MDim * MDim); DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
float* const DensityGridData=DensityGrid.GetData();
const FVoxelSharedDensityGrid* ReuseGrid = InSharedDensityGrid != nullptr const FVoxelSharedDensityGrid* ReuseGrid = InSharedDensityGrid != nullptr
? InSharedDensityGrid : SharedDensityGrid; ? InSharedDensityGrid : SharedDensityGrid;
const int32 SharedDeltaX = ReuseGrid const int32 SharedDeltaX = ReuseGrid
@@ -739,7 +804,15 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const bool bUseOperatorBlock = !bUseSharedDensityGrid const bool bUseOperatorBlock = !bUseSharedDensityGrid
&& !bUseParameterizedBrickEvaluator && !bUseParameterizedBrickEvaluator
&& GVoxelForgeUseOperatorBlock != 0; && GVoxelForgeUseOperatorBlock != 0;
if (bUseOperatorBlock) const int32 GridZSampleCount=GzHi-GzLo+1;
const std::size_t GridFillLaneCount=std::min<std::size_t>({
GridFillExecution.WorkerLimit,16,
static_cast<std::size_t>(FMath::Max(0,GridZSampleCount))});
const bool bUseParallelDensityGrid=GridFillExecution.ParallelFor!=nullptr
&&GridFillLaneCount>1&&GridFillExecution.BeginWorkerContext!=nullptr
&&GridFillExecution.EndWorkerContext!=nullptr&&!bUseSharedDensityGrid
&&!bUseParameterizedBrickEvaluator&&OutCaptureGrid==nullptr;
if (bUseOperatorBlock&&!bUseParallelDensityGrid)
{ {
// The grid includes one sample of halo on every side. The generator groups these exact // The grid includes one sample of halo on every side. The generator groups these exact
// lattice points by chunk key and lets the prepared op stack fill each group once. // lattice points by chunk key and lets the prepared op stack fill each group once.
@@ -889,6 +962,80 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
} }
#endif #endif
} }
else if(bUseParallelDensityGrid)
{
const FVoxelMesherThreadState TileThreadState=FVoxelMesherThreadState::Capture();
std::array<int64,16> LaneSampleCounts{};
std::atomic<bool> bAborted{false};
auto RunLane=[&](std::size_t Lane,std::size_t Worker)
{
void* WorkerState=GridFillExecution.BeginWorkerContext(
GridFillExecution.WorkerContext,Worker);
int64 LaneSamples=0;
{
FVoxelMesherThreadStateScope ThreadStateScope(TileThreadState);
const int32 FirstGz=GzLo+static_cast<int32>(
(static_cast<std::size_t>(GridZSampleCount)*Lane)/GridFillLaneCount);
const int32 EndGz=GzLo+static_cast<int32>(
(static_cast<std::size_t>(GridZSampleCount)*(Lane+1))/GridFillLaneCount);
if(bUseOperatorBlock)
{
Generator->BeginDensityBlock(
OriginVoxels+FIntVector(-Step,-Step,FirstGz*Step),Step,
MDim,MDim,EndGz-FirstGz);
}
for(int32 gz=FirstGz;gz<EndGz;++gz)
{
if(ShouldAbortWork())
{ bAborted.store(true,std::memory_order_relaxed);break; }
for(int32 gy=-1;gy<=GridDim;++gy)
{
if(ShouldAbortWork())
{ bAborted.store(true,std::memory_order_relaxed);break; }
for(int32 gx=-1;gx<=GridDim;++gx)
{
const int32 LocalIndex=((gz+1)*MDim+(gy+1))*MDim+(gx+1);
const int32 ReachBlockX=FMath::Clamp(gx,0,CellsPerAxis-1)
/VoxelGenLOD::TileReachBlockCells;
const int32 ReachBlockY=FMath::Clamp(gy,0,CellsPerAxis-1)
/VoxelGenLOD::TileReachBlockCells;
const int32 ReachBlockZ=FMath::Clamp(gz,0,CellsPerAxis-1)
/VoxelGenLOD::TileReachBlockCells;
VoxelGenLOD::TileReachBlockIndex=
((ReachBlockZ*ReachBlocksPerAxis)+ReachBlockY)
*ReachBlocksPerAxis+ReachBlockX;
float PreEditDensity=0.0f;
DensityGridData[LocalIndex]=Generator->GetDensityAt(
OriginVoxels.X+gx*Step,OriginVoxels.Y+gy*Step,
OriginVoxels.Z+gz*Step,
bCapturePreEditGrid?&PreEditDensity:nullptr);
if(bCapturePreEditGrid)
OutPreEditDensityGrid->Samples[LocalIndex]=PreEditDensity;
++LaneSamples;
}
}
if(bAborted.load(std::memory_order_relaxed))break;
}
if(bUseOperatorBlock)Generator->EndDensityBlock();
}
LaneSampleCounts[Lane]=LaneSamples;
GridFillExecution.EndWorkerContext(
GridFillExecution.WorkerContext,WorkerState,Worker);
};
struct FDispatchContext{decltype(RunLane)* Run=nullptr;};
FDispatchContext DispatchContext{&RunLane};
const auto DispatchLane=[](void* RawContext,std::size_t Lane,std::size_t Worker)
{
auto& Context=*static_cast<FDispatchContext*>(RawContext);
(*Context.Run)(Lane,Worker);
};
GridFillExecution.ParallelFor(GridFillExecution.SchedulerContext,
GridFillLaneCount,GridFillLaneCount,&DispatchContext,DispatchLane);
if(OutDensitySampleCount)
for(std::size_t Lane=0;Lane<GridFillLaneCount;++Lane)
*OutDensitySampleCount+=LaneSampleCounts[Lane];
if(bAborted.load(std::memory_order_relaxed))return FVoxelMeshData();
}
else for (int32 gz = GzLo; gz <= GzHi; gz++) else for (int32 gz = GzLo; gz <= GzHi; gz++)
{ {
if (ShouldAbortWork()) if (ShouldAbortWork())
@@ -959,7 +1106,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
GridSampleCount * sizeof(float)); GridSampleCount * sizeof(float));
} }
} }
if (bUseOperatorBlock) if (bUseOperatorBlock&&!bUseParallelDensityGrid)
{ {
Generator->EndDensityBlock(); Generator->EndDensityBlock();
} }
@@ -2590,8 +2590,8 @@ static float EvaluateInternal(const UVoxelGenerator& Generator,
{ {
EnsurePlan(Generator); EnsurePlan(Generator);
} }
const bool bChunkResolved = Context != nullptr && Context->bChunkResolved const bool bChunkResolved = (Context != nullptr && Context->bChunkResolved)
|| ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved; || (ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved);
const FIntVector Chunk = Context != nullptr && Context->bChunkResolved const FIntVector Chunk = Context != nullptr && Context->bChunkResolved
? Context->Chunk ? Context->Chunk
: ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved : ParameterizedContext != nullptr && ParameterizedContext->bChunkResolved
@@ -1958,10 +1958,11 @@ namespace VoxelSeasonCompositionPrivate
} }
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition, void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
FVoxelSeasonFixedStrate& OutFixed) double LayoutChunkMeters, FVoxelSeasonFixedStrate& OutFixed)
{ {
OutFixed = FVoxelSeasonFixedStrate(); OutFixed = FVoxelSeasonFixedStrate();
OutFixed.HeightInChunks = Definition.StrateHeightInChunks; OutFixed.HeightInChunks = FMath::Max(1, FMath::RoundToInt(
Definition.StrateHeightMeters / FMath::Max(0.001, LayoutChunkMeters)));
OutFixed.Archetype = Definition.GeneratorType; OutFixed.Archetype = Definition.GeneratorType;
OutFixed.SourceDefinitionPath = Definition.GetPathName(); OutFixed.SourceDefinitionPath = Definition.GetPathName();
OutFixed.bUsesRecipe = false; OutFixed.bUsesRecipe = false;
@@ -1999,7 +2000,9 @@ namespace VoxelSeasonCompositionPrivate
return false; return false;
} }
FVoxelSeasonFixedStrate Fixed; FVoxelSeasonFixedStrate Fixed;
CopyDefinitionParams(*Definition, Fixed); const double LayoutChunkMeters = Settings.WorldSettings != nullptr
? Settings.WorldSettings->StrateLayoutChunkSizeMeters : 8.0;
CopyDefinitionParams(*Definition, LayoutChunkMeters, Fixed);
Fixed.Seed = SeasonSeed; Fixed.Seed = SeasonSeed;
if (Fixed.SourceDefinitionPath.IsEmpty()) if (Fixed.SourceDefinitionPath.IsEmpty())
{ {
@@ -2974,7 +2977,9 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* Wo
if (WorldSettings != nullptr) if (WorldSettings != nullptr)
{ {
Settings.SelectedStrateCount = WorldSettings->TotalStrates; Settings.SelectedStrateCount = WorldSettings->TotalStrates;
Settings.InterStrateGapChunks = WorldSettings->InterStrateGapChunks; Settings.InterStrateGapChunks = FMath::Max(0, FMath::RoundToInt(
WorldSettings->InterStrateGapMeters * 1000000.0
/ FMath::Max(1.0, static_cast<double>(WorldSettings->StrateLayoutChunkSizeMeters) * 1000000.0)));
Settings.OriginSpineRadius = WorldSettings->OriginSpineRadius; Settings.OriginSpineRadius = WorldSettings->OriginSpineRadius;
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels; Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
Settings.SeasonNumber = WorldSettings->CurrentSeason; Settings.SeasonNumber = WorldSettings->CurrentSeason;
@@ -1,5 +1,7 @@
#include "VoxelSettings.h" #include "VoxelSettings.h"
#include "VoxelForgeAssetVersion.h"
#include "VoxelStrateDefinition.h"
#include "VoxelSeasonAsset.h" #include "VoxelSeasonAsset.h"
#if WITH_EDITOR #if WITH_EDITOR
@@ -9,6 +11,74 @@
#include <VoxelForgeCore/VoxelForgeCore.h> #include <VoxelForgeCore/VoxelForgeCore.h>
#include <string> #include <string>
void UVoxelSettings::Serialize(FArchive& Ar)
{
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
Super::Serialize(Ar);
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
< VoxelForgeAssetVersion::MetreAuthoringV1)
{
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
* LayoutChunkMeters;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeAssetUpgrade] type=Settings asset=%s gap_chunks=%d gap_m=%.6f layout_chunk_m=%.6f"),
*GetPathName(), InterStrateGapChunks, InterStrateGapMeters, LayoutChunkMeters);
}
}
void UVoxelSettings::PostLoad()
{
Super::PostLoad();
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
>= VoxelForgeAssetVersion::MetreAuthoringV1)
{
return;
}
// Resolve the legacy count against this settings asset's saved physical layout width.
// Convertit les anciens comptes avec la largeur physique sauvegardee dans ces reglages.
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
* LayoutChunkMeters;
TSet<UVoxelStrateDefinition*> Migrated;
const auto MigrateStrate = [this, LayoutChunkMeters, &Migrated](
const TSoftObjectPtr<UVoxelStrateDefinition>& SoftDefinition)
{
UVoxelStrateDefinition* Definition = SoftDefinition.LoadSynchronous();
if (Definition == nullptr || Migrated.Contains(Definition)
|| Definition->GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
>= VoxelForgeAssetVersion::MetreAuthoringV1)
{
return;
}
Migrated.Add(Definition);
const int32 OldHeightChunks = Definition->StrateHeightInChunks;
const int32 OldTransitionChunks = Definition->TransitionBlendChunks;
Definition->StrateHeightMeters = static_cast<double>(FMath::Max(1, OldHeightChunks))
* LayoutChunkMeters;
Definition->TransitionWidthMeters = static_cast<double>(FMath::Max(0, OldTransitionChunks))
* LayoutChunkMeters;
Definition->MarkPackageDirty();
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f effective_layout_chunk_m=%.6f"),
*Definition->GetPathName(), OldHeightChunks, Definition->StrateHeightMeters,
OldTransitionChunks, Definition->TransitionWidthMeters, LayoutChunkMeters);
};
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Definition : StratePool)
{
MigrateStrate(Definition);
}
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Definition : FixedStrates)
{
MigrateStrate(Definition.Value);
}
MarkPackageDirty();
}
int32 UVoxelSettings::GetEffectiveWorldSeed() const int32 UVoxelSettings::GetEffectiveWorldSeed() const
{ {
if (!Season.IsNull()) if (!Season.IsNull())
@@ -1,9 +1,12 @@
#include "VoxelStartupTrace.h" #include "VoxelStartupTrace.h"
#include "HAL/FileManager.h" #include "HAL/FileManager.h"
#include "HAL/PlatformTLS.h"
#include "HAL/PlatformTime.h" #include "HAL/PlatformTime.h"
#include "CoreGlobals.h"
#include "Misc/CommandLine.h" #include "Misc/CommandLine.h"
#include "Misc/Parse.h" #include "Misc/Parse.h"
#include "UObject/UObjectGlobals.h"
#include <atomic> #include <atomic>
@@ -15,7 +18,13 @@ namespace
{ {
FArchive* Archive = nullptr; FArchive* Archive = nullptr;
FString Path; FString Path;
uint32 GameThreadId = 0;
double StartSeconds = 0.0; double StartSeconds = 0.0;
double GameThreadJsonWriteSeconds = 0.0;
double GameThreadArchiveSerializeSeconds = 0.0;
double GarbageCollectionStartSeconds = 0.0;
FDelegateHandle PreGarbageCollectHandle;
FDelegateHandle PostGarbageCollectHandle;
int32 TileRecordCap = 4096; int32 TileRecordCap = 4096;
int32 TileRecordsSeen = 0; int32 TileRecordsSeen = 0;
int32 TileRecordsWritten = 0; int32 TileRecordsWritten = 0;
@@ -50,15 +59,22 @@ namespace
void WriteLineLocked(FState& State, const FString& Line) void WriteLineLocked(FState& State, const FString& Line)
{ {
if (State.Archive == nullptr) return; if (State.Archive == nullptr) return;
const uint64 StartCycles = FPlatformTime::Cycles64();
const FString WithNewline = Line + TEXT("\n"); const FString WithNewline = Line + TEXT("\n");
FTCHARToUTF8 Utf8(*WithNewline); FTCHARToUTF8 Utf8(*WithNewline);
State.Archive->Serialize(const_cast<ANSICHAR*>(Utf8.Get()), Utf8.Length()); State.Archive->Serialize(const_cast<ANSICHAR*>(Utf8.Get()), Utf8.Length());
if (FPlatformTLS::GetCurrentThreadId() == State.GameThreadId)
{
State.GameThreadArchiveSerializeSeconds += FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - StartCycles);
}
// The file archive buffers records; flushing every tile/request can stall the game thread. // The file archive buffers records; flushing every tile/request can stall the game thread.
// L'archive tamponne les lignes ; vider apres chaque tuile/requete peut bloquer le thread de jeu. // L'archive tamponne les lignes ; vider apres chaque tuile/requete peut bloquer le thread de jeu.
} }
void WriteEventLocked(FState& State, const TCHAR* Type, const FString& FieldsJson) void WriteEventLocked(FState& State, const TCHAR* Type, const FString& FieldsJson)
{ {
const uint64 StartCycles = FPlatformTime::Cycles64();
FString Line = FString::Printf( FString Line = FString::Printf(
TEXT("{\"type\":\"%s\",\"t_s\":%s"), TEXT("{\"type\":\"%s\",\"t_s\":%s"),
Type, Type,
@@ -70,6 +86,11 @@ namespace
} }
Line += TEXT("}"); Line += TEXT("}");
WriteLineLocked(State, Line); WriteLineLocked(State, Line);
if (FPlatformTLS::GetCurrentThreadId() == State.GameThreadId)
{
State.GameThreadJsonWriteSeconds += FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - StartCycles);
}
} }
double Percentile(TArray<double> Values, double Fraction) double Percentile(TArray<double> Values, double Fraction)
@@ -198,6 +219,7 @@ void BeginFromCommandLine()
TUniquePtr<FState> NewState = MakeUnique<FState>(); TUniquePtr<FState> NewState = MakeUnique<FState>();
NewState->Path = Path; NewState->Path = Path;
NewState->GameThreadId = FPlatformTLS::GetCurrentThreadId();
FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceMaxTiles="), NewState->TileRecordCap); FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceMaxTiles="), NewState->TileRecordCap);
NewState->TileRecordCap = FMath::Clamp(NewState->TileRecordCap, 64, 16384); NewState->TileRecordCap = FMath::Clamp(NewState->TileRecordCap, 64, 16384);
NewState->Samples.Reserve(NewState->TileRecordCap); NewState->Samples.Reserve(NewState->TileRecordCap);
@@ -211,6 +233,32 @@ void BeginFromCommandLine()
NewState->bActive = true; NewState->bActive = true;
GState = MoveTemp(NewState); GState = MoveTemp(NewState);
GTraceActive.store(true, std::memory_order_release); GTraceActive.store(true, std::memory_order_release);
GState->PreGarbageCollectHandle = FCoreUObjectDelegates::GetPreGarbageCollectDelegate()
.AddLambda([]()
{
FScopeLock CallbackLock(&GTraceLock);
if (GState.IsValid() && GState->bActive)
{
GState->GarbageCollectionStartSeconds = FPlatformTime::Seconds();
}
});
GState->PostGarbageCollectHandle = FCoreUObjectDelegates::GetPostGarbageCollect()
.AddLambda([]()
{
FScopeLock CallbackLock(&GTraceLock);
if (!GState.IsValid() || !GState->bActive
|| GState->GarbageCollectionStartSeconds <= 0.0)
{
return;
}
const double DurationMilliseconds = FMath::Max(
0.0, FPlatformTime::Seconds() - GState->GarbageCollectionStartSeconds) * 1000.0;
GState->GarbageCollectionStartSeconds = 0.0;
WriteEventLocked(*GState, TEXT("garbage_collect"), FString::Printf(
TEXT("\"frame\":%llu,\"duration_ms\":%s"),
static_cast<unsigned long long>(GFrameCounter),
*Number(DurationMilliseconds)));
});
WriteEventLocked(*GState, TEXT("header"), FString::Printf( WriteEventLocked(*GState, TEXT("header"), FString::Printf(
TEXT("\"schema\":1,\"path\":\"%s\",\"tile_record_cap\":%d"), TEXT("\"schema\":1,\"path\":\"%s\",\"tile_record_cap\":%d"),
*JsonEscape(GState->Path), GState->TileRecordCap)); *JsonEscape(GState->Path), GState->TileRecordCap));
@@ -221,6 +269,21 @@ bool IsActive()
return GTraceActive.load(std::memory_order_acquire); return GTraceActive.load(std::memory_order_acquire);
} }
FWriteMetrics ConsumeCurrentThreadWriteMetrics()
{
FWriteMetrics Metrics;
FScopeLock Lock(&GTraceLock);
if (!GState.IsValid() || FPlatformTLS::GetCurrentThreadId() != GState->GameThreadId)
{
return Metrics;
}
Metrics.JsonWriteMilliseconds = GState->GameThreadJsonWriteSeconds * 1000.0;
Metrics.ArchiveSerializeMilliseconds = GState->GameThreadArchiveSerializeSeconds * 1000.0;
GState->GameThreadJsonWriteSeconds = 0.0;
GState->GameThreadArchiveSerializeSeconds = 0.0;
return Metrics;
}
void RecordEvent(const TCHAR* Type, const FString& FieldsJson) void RecordEvent(const TCHAR* Type, const FString& FieldsJson)
{ {
FScopeLock Lock(&GTraceLock); FScopeLock Lock(&GTraceLock);
@@ -297,7 +360,11 @@ void RecordTile(const FTileSample& Sample)
"\"submitted_t_s\":%s,\"worker_start_t_s\":%s,\"worker_end_t_s\":%s,\"worker_thread_id\":%u," "\"submitted_t_s\":%s,\"worker_start_t_s\":%s,\"worker_end_t_s\":%s,\"worker_thread_id\":%u,"
"\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu," "\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu,"
"\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,\"worker_task_s\":%s," "\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,\"worker_task_s\":%s,"
"\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"), "\"generation_s\":%s,"
#if PLATFORM_LINUX
"\"core_generate_s\":%s,\"mesh_copy_s\":%s,\"rmc_stream_build_s\":%s,"
#endif
"\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
Sample.TileX, Sample.TileY, Sample.TileZ, Sample.TileX, Sample.TileY, Sample.TileZ,
Sample.Level, Sample.WorkPriority, Sample.Level, Sample.WorkPriority,
static_cast<unsigned long long>(Sample.RequestOrdinal), static_cast<unsigned long long>(Sample.RequestOrdinal),
@@ -312,8 +379,12 @@ void RecordTile(const FTileSample& Sample)
static_cast<unsigned long long>(Sample.GeometryBytes), static_cast<unsigned long long>(Sample.GeometryBytes),
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds), *Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds), *Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
*Number(Sample.WorkerTaskSeconds), *Number(Sample.WorkerTaskSeconds), *Number(Sample.GenerationSeconds),
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds), #if PLATFORM_LINUX
*Number(Sample.CoreGenerateSeconds), *Number(Sample.MeshCopySeconds),
*Number(Sample.StreamBuildSeconds),
#endif
*Number(Sample.ClassifySeconds),
*Number(Sample.MeshSeconds), *Number(Sample.StreamSeconds), *Number(Sample.ApplySeconds))); *Number(Sample.MeshSeconds), *Number(Sample.StreamSeconds), *Number(Sample.ApplySeconds)));
} }
@@ -370,6 +441,18 @@ void Finish(const TCHAR* Reason)
FScopeLock Lock(&GTraceLock); FScopeLock Lock(&GTraceLock);
if (!GState.IsValid() || !GState->bActive) return; if (!GState.IsValid() || !GState->bActive) return;
GTraceActive.store(false, std::memory_order_release); GTraceActive.store(false, std::memory_order_release);
if (GState->PreGarbageCollectHandle.IsValid())
{
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().Remove(
GState->PreGarbageCollectHandle);
GState->PreGarbageCollectHandle.Reset();
}
if (GState->PostGarbageCollectHandle.IsValid())
{
FCoreUObjectDelegates::GetPostGarbageCollect().Remove(
GState->PostGarbageCollectHandle);
GState->PostGarbageCollectHandle.Reset();
}
// Close any stage left open by an abnormal early exit so the summary remains useful. // Close any stage left open by an abnormal early exit so the summary remains useful.
const double Now = FPlatformTime::Seconds(); const double Now = FPlatformTime::Seconds();
@@ -379,7 +462,19 @@ void Finish(const TCHAR* Reason)
} }
GState->StageStarts.Reset(); GState->StageStarts.Reset();
WriteEventLocked(*GState, TEXT("summary"), BuildSummaryFieldsLocked(*GState, Reason)); WriteEventLocked(*GState, TEXT("summary"), BuildSummaryFieldsLocked(*GState, Reason));
const uint64 FlushStartCycles = FPlatformTime::Cycles64();
GState->Archive->Flush(); GState->Archive->Flush();
const double FlushMilliseconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - FlushStartCycles) * 1000.0;
WriteEventLocked(*GState, TEXT("trace_finish_flush"), FString::Printf(
TEXT("\"first_flush_ms\":%s"), *Number(FlushMilliseconds)));
const uint64 FinalFlushStartCycles = FPlatformTime::Cycles64();
GState->Archive->Flush();
const double FinalFlushMilliseconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - FinalFlushStartCycles) * 1000.0;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStartupTrace] finish_flush_ms=%.3f final_flush_ms=%.3f"),
FlushMilliseconds, FinalFlushMilliseconds);
delete GState->Archive; delete GState->Archive;
GState->Archive = nullptr; GState->Archive = nullptr;
GState->bActive = false; GState->bActive = false;
@@ -1,20 +1,17 @@
// Serialized strate-definition migrations. // Serialized strate-definition migrations.
#include "VoxelStrateDefinition.h" #include "VoxelStrateDefinition.h"
#include "VoxelForgeAssetVersion.h"
#include "Serialization/CustomVersion.h" #include "Serialization/CustomVersion.h"
#include "VoxelPassageGeometry.h" #include "VoxelPassageGeometry.h"
namespace namespace
{ {
// The field name is retained for package compatibility, but version 1 changes its meaning constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = VoxelForgeAssetVersion::TunnelFloorSlopeDegrees;
// from a raw tangent/gradient to authored degrees. This GUID is local to the plugin.
const FGuid GVoxelForgeStrateSerializationVersion(
0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = 1;
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration( FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
GVoxelForgeStrateSerializationVersion, VoxelForgeAssetVersion::GUID,
GVoxelForgeStrateGentleSlopeDegreesVersion, VoxelForgeAssetVersion::MetreAuthoringV1,
TEXT("VoxelForgeStrateSerialization")); TEXT("VoxelForgeStrateSerialization"));
float LegacyGradientToDegrees(float LegacyGradient) float LegacyGradientToDegrees(float LegacyGradient)
@@ -58,6 +55,31 @@ namespace
} }
} }
void UVoxelStrateDefinition::Serialize(FArchive& Ar)
{
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
Super::Serialize(Ar);
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
< VoxelForgeAssetVersion::MetreAuthoringV1)
{
// A stand-alone strate has no owning settings object during Serialize. Convert using the
// current 8 m layout default here; UVoxelSettings corrects referenced old strates using
// their saved effective width during its own load upgrade.
// Une strate seule n'a pas ses reglages proprietaires ici; UVoxelSettings corrige ensuite
// les references avec leur largeur physique sauvegardee.
constexpr double LegacyDefaultLayoutChunkMeters = 8.0;
StrateHeightMeters = static_cast<double>(FMath::Max(1, StrateHeightInChunks))
* LegacyDefaultLayoutChunkMeters;
TransitionWidthMeters = static_cast<double>(FMath::Max(0, TransitionBlendChunks))
* LegacyDefaultLayoutChunkMeters;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f fallback_layout_chunk_m=%.6f"),
*GetPathName(), StrateHeightInChunks, StrateHeightMeters,
TransitionBlendChunks, TransitionWidthMeters, LegacyDefaultLayoutChunkMeters);
}
}
void UVoxelStrateDefinition::PostLoad() void UVoxelStrateDefinition::PostLoad()
{ {
Super::PostLoad(); Super::PostLoad();
@@ -67,7 +89,7 @@ void UVoxelStrateDefinition::PostLoad()
// is normalized to the ignored compatibility default before any runtime copy is made. // is normalized to the ignored compatibility default before any runtime copy is made.
GenerationParams.bTunnelFloorEnabled = true; GenerationParams.bTunnelFloorEnabled = true;
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion) if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
>= GVoxelForgeStrateGentleSlopeDegreesVersion) >= GVoxelForgeStrateGentleSlopeDegreesVersion)
{ {
return; return;
@@ -1255,7 +1255,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
? SeasonManifest.Strates.Num() : Settings->TotalStrates; ? SeasonManifest.Strates.Num() : Settings->TotalStrates;
const int32 LayoutGapChunks = bUseSeason const int32 LayoutGapChunks = bUseSeason
? SeasonManifest.InterStrateGapChunks ? SeasonManifest.InterStrateGapChunks
: FMath::Max(0, Settings->InterStrateGapChunks); : FMath::Max(0, FMath::RoundToInt(static_cast<double>(Settings->InterStrateGapMeters)
* 1000000.0 / std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
//========================================================================= //=========================================================================
// STEP 1: Build shuffled pool (seed-based randomization) // STEP 1: Build shuffled pool (seed-based randomization)
@@ -1369,7 +1370,8 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
} }
Slot.Definition->GeneratorType = SeasonStrate.Archetype; Slot.Definition->GeneratorType = SeasonStrate.Archetype;
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks; Slot.Definition->StrateHeightMeters = static_cast<double>(SeasonStrate.HeightInChunks)
* StrateLayoutChunkSizeMicrometres / 1000000.0;
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams; Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams; Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams; Slot.Definition->MazeParams = SeasonStrate.Params.MazeParams;
@@ -1426,7 +1428,9 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
} }
// Resolve the authored chunk height into an exact physical interval. // 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.TopWorldMicrometres = CurrentTopWorldMicrometres;
Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks) Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks)
* StrateLayoutChunkSizeMicrometres; * StrateLayoutChunkSizeMicrometres;
@@ -1439,9 +1443,11 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
} }
Slot.ResolvedTransitionType = Slot.Definition->TransitionType; Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
Slot.ResolvedTransitionBlendChunks = Slot.Definition->TransitionBlendChunks; Slot.ResolvedTransitionBlendChunks = FMath::Max(0, FMath::RoundToInt(
Slot.TransitionWidthMicrometres = static_cast<int64>( Slot.Definition->TransitionWidthMeters * 1000000.0
Slot.ResolvedTransitionBlendChunks) * StrateLayoutChunkSizeMicrometres; / std::max<uint32>(1, StrateLayoutChunkSizeMicrometres)));
Slot.TransitionWidthMicrometres = static_cast<int64>(Slot.ResolvedTransitionBlendChunks)
* StrateLayoutChunkSizeMicrometres;
Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig; Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig;
Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres( Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.MouthRadius); Slot.ResolvedPassageConfig.MouthRadius);
File diff suppressed because it is too large. Load diff
+20 -9
View File
@@ -261,8 +261,12 @@ namespace VoxelHash
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed) FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
{ {
uint32 H = Seed; uint32 H = Seed;
H ^= Mix((uint32)(CellX + 0x7FFFFFFF)); // Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for
H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u); // positive cells and makes the world hash compiler-dependent.
const uint32 BiasedX = static_cast<uint32>(CellX) + 0x7FFFFFFFu;
const uint32 BiasedY = static_cast<uint32>(CellY) + 0x7FFFFFFFu;
H ^= Mix(BiasedX);
H ^= Mix(BiasedY * 2654435761u);
return Mix(H); return Mix(H);
} }
@@ -274,10 +278,14 @@ namespace VoxelHash
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY); int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY); int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
uint32 H = Seed ^ 0xDEADBEEF; uint32 H = Seed ^ 0xDEADBEEF;
H ^= Mix((uint32)(MinX + 0x7FFFFFFF)); const uint32 BiasedMinX = static_cast<uint32>(MinX) + 0x7FFFFFFFu;
H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u); const uint32 BiasedMinY = static_cast<uint32>(MinY) + 0x7FFFFFFFu;
H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u); const uint32 BiasedMaxX = static_cast<uint32>(MaxX) + 0x7FFFFFFFu;
H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u); const uint32 BiasedMaxY = static_cast<uint32>(MaxY) + 0x7FFFFFFFu;
H ^= Mix(BiasedMinX);
H ^= Mix(BiasedMinY * 2654435761u);
H ^= Mix(BiasedMaxX * 374761393u);
H ^= Mix(BiasedMaxY * 668265263u);
return Mix(H); return Mix(H);
} }
@@ -477,10 +485,13 @@ namespace VoxelNoise
// Hash the neighbor cell to get a feature point position [0,1) // Hash the neighbor cell to get a feature point position [0,1)
// Using three different hash mixes for X, Y, Z offsets // Using three different hash mixes for X, Y, Z offsets
const uint32 BiasedX = static_cast<uint32>(NX) + 0x7FFFFFFFu;
const uint32 BiasedY = static_cast<uint32>(NY) + 0x7FFFFFFFu;
const uint32 BiasedZ = static_cast<uint32>(NZ) + 0x7FFFFFFFu;
uint32 H = VoxelHash::Mix( uint32 H = VoxelHash::Mix(
(uint32)(NX + 0x7FFFFFFF) BiasedX
^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u) ^ VoxelHash::Mix(BiasedY * 2654435761u)
^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u) ^ VoxelHash::Mix(BiasedZ * 374761393u)
); );
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX; float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
@@ -340,6 +340,14 @@ private:
float SuppressRadiusWorld = 0.0f; float SuppressRadiusWorld = 0.0f;
}; };
// Game-thread lookup copied from the immutable resolved Core layout.
// Recherche game-thread copiee depuis la disposition Core resolue et immuable.
struct FResolvedCoreGeometryAnchor
{
int32 StrateIndex = INDEX_NONE;
FVector LocalPositionCm = FVector::ZeroVector;
};
/** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle /** WORKER-THREAD surface find → fills OutSpawns for one cell. SurfaceWorld uses the height oracle
* (cheap, O(1)/column); other archetypes ray-march the density column. No UObject access except * (cheap, O(1)/column); other archetypes ray-march the density column. No UObject access except
* Generator (thread-safe). Determinism-critical. Resolves the dominant biome PER COLUMN * Generator (thread-safe). Determinism-critical. Resolves the dominant biome PER COLUMN
@@ -436,6 +444,7 @@ private:
// (kept until the cell leaves range so the surface-find isn't repeated). Strate-bounded. // (kept until the cell leaves range so the surface-find isn't repeated). Strate-bounded.
// Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint. // Keyed by FIntVector(cellX|passageIdx, cellY|side, entryIndex) — lattice cell OR passage endpoint.
TMap<FIntVector, FLandmarkInstance> LandmarkInstances; TMap<FIntVector, FLandmarkInstance> LandmarkInstances;
TMap<FString, FResolvedCoreGeometryAnchor> CoreGeometryAnchors;
int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks int32 LastLandmarkStrate = INT32_MIN; // strate change → wipe + rebuild landmarks
// Set in BeginDestroy; worker tasks check it before touching us. // Set in BeginDestroy; worker tasks check it before touching us.
@@ -13,6 +13,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include <atomic> #include <atomic>
#include <cstddef>
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc. #include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
#include "VoxelGenerator.h" #include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.generated.h" #include "VoxelMarchingCubesMesher.generated.h"
@@ -38,6 +39,24 @@ struct FVoxelSharedDensityGrid
} }
}; };
// Host-owned scheduling hooks for a predicted-heavy density-grid fill. Each task owns one
// deterministic Z slab and gets the source tile's worker-local sampling context.
struct FVoxelGridFillExecution
{
using FTask = void(*)(void* taskContext, std::size_t index, std::size_t workerIndex);
using FParallelFor = double(*)(void* schedulerContext, std::size_t count,
std::size_t workerLimit, void* taskContext, FTask task);
using FBeginWorkerContext = void*(*)(void* context, std::size_t workerIndex);
using FEndWorkerContext = void(*)(void* context, void* workerState,
std::size_t workerIndex);
void* SchedulerContext = nullptr;
FParallelFor ParallelFor = nullptr;
std::size_t WorkerLimit = 1;
void* WorkerContext = nullptr;
FBeginWorkerContext BeginWorkerContext = nullptr;
FEndWorkerContext EndWorkerContext = nullptr;
};
// Stable plan data copied onto one worker-owned mesher for hybrid LOD0 generation. // Stable plan data copied onto one worker-owned mesher for hybrid LOD0 generation.
// Donnees stables du plan copiees dans un mailleur de worker pour le mode hybride LOD0. // Donnees stables du plan copiees dans un mailleur de worker pour le mode hybride LOD0.
struct FVoxelHybridSharpFeatureBox struct FVoxelHybridSharpFeatureBox
@@ -111,6 +130,8 @@ public:
// GenerateMesh calls finish. A malformed/incomplete lattice is ignored and falls back to // GenerateMesh calls finish. A malformed/incomplete lattice is ignored and falls back to
// the canonical generator path rather than changing geometry. // the canonical generator path rather than changing geometry.
void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; } void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; }
void SetGridFillExecution(const FVoxelGridFillExecution& InExecution)
{ GridFillExecution = InExecution; }
// The caller owns this mesher instance for one tile task; no shared mutable feature state. // The caller owns this mesher instance for one tile task; no shared mutable feature state.
// L'appelant possede cette instance pour une tuile; aucune donnee partagee modifiable. // L'appelant possede cette instance pour une tuile; aucune donnee partagee modifiable.
@@ -137,6 +158,7 @@ private:
const std::atomic<bool>* ShutdownFlag = nullptr; const std::atomic<bool>* ShutdownFlag = nullptr;
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr; const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
FVoxelGridFillExecution GridFillExecution;
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures; TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
bool bUseHybridSharpMesher = false; bool bUseHybridSharpMesher = false;
bool bUseHybridSharpFeatureSnap = false; bool bUseHybridSharpFeatureSnap = false;
+10 -6
View File
@@ -21,6 +21,8 @@ class UVoxelSettings : public UPrimaryDataAsset
GENERATED_BODY() GENERATED_BODY()
public: public:
virtual void Serialize(FArchive& Ar) override;
virtual void PostLoad() override;
/** Season seed/radius replace the mutable authored values only when Season is assigned. */ /** Season seed/radius replace the mutable authored values only when Season is assigned. */
int32 GetEffectiveWorldSeed() const; int32 GetEffectiveWorldSeed() const;
@@ -411,14 +413,16 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "1"))
int32 TotalStrates = 10; int32 TotalStrates = 10;
// Vertical SEPARATION between consecutive strates, in chunks of solid bedrock. // Legacy count retained so pre-metre assets still deserialize during the upgrade.
// 0 = strates touch (just the seals between them). Higher = a thick rock layer // Ancien compte conserve pour lire les assets precedant les metres.
// between each biome that the player must dig through at (0,0), and that the UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (DeprecatedProperty, DeprecationMessage = "Use InterStrateGapMeters."))
// auto-carved passages tunnel across.
// 0 → adjacent layers (default) · 1-2 → a real bedrock band · 4+ → deep separation
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (ClampMin = "0", ClampMax = "16"))
int32 InterStrateGapChunks = 0; int32 InterStrateGapChunks = 0;
// Physical separation between strata; the layout remains aligned to whole layout chunks.
// Separation physique; la disposition reste alignee sur des chunks entiers.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates", meta = (Units = "m", ClampMin = "0", ClampMax = "1024"))
double InterStrateGapMeters = 0.0;
// Physical width of one vertical strate-layout chunk. This is independent of the // Physical width of one vertical strate-layout chunk. This is independent of the
// Core tile width (32^3 voxels) and of the source generator's voxel resolution. // Core tile width (32^3 voxels) and of the source generator's voxel resolution.
// 8 m preserves the owner's current SurfaceWorld extent at 25 cm Core voxels. // 8 m preserves the owner's current SurfaceWorld extent at 25 cm Core voxels.
@@ -35,6 +35,9 @@ namespace VoxelForgeStartupTrace
double ResultQueueSeconds = 0.0; double ResultQueueSeconds = 0.0;
double WorkerTaskSeconds = 0.0; double WorkerTaskSeconds = 0.0;
double GenerationSeconds = 0.0; double GenerationSeconds = 0.0;
double CoreGenerateSeconds = 0.0;
double MeshCopySeconds = 0.0;
double StreamBuildSeconds = 0.0;
double ClassifySeconds = 0.0; double ClassifySeconds = 0.0;
double MeshSeconds = 0.0; double MeshSeconds = 0.0;
double StreamSeconds = 0.0; double StreamSeconds = 0.0;
@@ -66,11 +69,20 @@ namespace VoxelForgeStartupTrace
TStaticArray<double, 9> ApplySecondsByLevel{}; TStaticArray<double, 9> ApplySecondsByLevel{};
}; };
struct VOXELFORGE_API FWriteMetrics
{
double JsonWriteMilliseconds = 0.0;
double ArchiveSerializeMilliseconds = 0.0;
};
/** Starts when -voxel.StartupTraceFile=<absolute path> is present. */ /** Starts when -voxel.StartupTraceFile=<absolute path> is present. */
VOXELFORGE_API void BeginFromCommandLine(); VOXELFORGE_API void BeginFromCommandLine();
VOXELFORGE_API bool IsActive(); VOXELFORGE_API bool IsActive();
/** Returns and resets startup-trace writes performed by the current game-thread frame. */
VOXELFORGE_API FWriteMetrics ConsumeCurrentThreadWriteMetrics();
/** FieldsJson is the contents after the type/timestamp object prefix, without braces. */ /** FieldsJson is the contents after the type/timestamp object prefix, without braces. */
VOXELFORGE_API void RecordEvent(const TCHAR* Type, const FString& FieldsJson = FString()); VOXELFORGE_API void RecordEvent(const TCHAR* Type, const FString& FieldsJson = FString());
@@ -48,6 +48,7 @@ class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
GENERATED_BODY() GENERATED_BODY()
public: public:
virtual void Serialize(FArchive& Ar) override;
virtual void PostLoad() override; virtual void PostLoad() override;
//========================================================================= //=========================================================================
@@ -66,14 +67,14 @@ public:
// DIMENSIONS // DIMENSIONS
//========================================================================= //=========================================================================
// How many chunks tall this strate is. // Legacy serialized chunk count kept for Blueprint/package compatibility during the unit migration.
// Each strate can have a different height! // Ancien nombre de chunks conserve pour la compatibilite Blueprint/package pendant la migration.
// Human-scale defaults use 16 chunks = 64 m. This gives a 20 m+ vertical budget for UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (DeprecatedProperty, DeprecationMessage = "Use StrateHeightMeters."))
// cathedral rooms, tall shafts, and the surface sky cap without changing the lateral gate.
// 1 chunk = 32 voxels x 12.5cm = 4 m; 16 chunks preserves the 64 m authored default.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (ClampMin = "1", ClampMax = "256"))
int32 StrateHeightInChunks = 16; int32 StrateHeightInChunks = 16;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Dimensions", meta = (Units = "m", ClampMin = "1", ClampMax = "2048"))
double StrateHeightMeters = 128.0;
//========================================================================= //=========================================================================
// BOUNDARY TRANSITION // BOUNDARY TRANSITION
//========================================================================= //=========================================================================
@@ -99,9 +100,13 @@ public:
// 2 → moderate (good default) // 2 → moderate (good default)
// 4 → wide, gentle blend across many chunks // 4 → wide, gentle blend across many chunks
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary", UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
meta = (ClampMin = "1", ClampMax = "8", EditCondition = "TransitionType != EVoxelStrateTransition::Hard")) meta = (DeprecatedProperty, DeprecationMessage = "Use TransitionWidthMeters."))
int32 TransitionBlendChunks = 2; int32 TransitionBlendChunks = 2;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Boundary",
meta = (Units = "m", ClampMin = "0", ClampMax = "512", EditCondition = "TransitionType != EVoxelStrateTransition::Hard"))
double TransitionWidthMeters = 16.0;
//========================================================================= //=========================================================================
// GENERATOR TYPE // GENERATOR TYPE
//========================================================================= //=========================================================================
@@ -115,6 +120,11 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork; ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
// Optional native Core authoring; leaving it empty keeps the established generator path.
// Edition Core native facultative; laissez vide pour garder le generateur existant.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Core Geometry")
FVoxelForgeCoreGeometry CoreGeometry;
/** /**
* Authoritative per-strate worm switch. Existing assets default to enabled. The manager * Authoritative per-strate worm switch. Existing assets default to enabled. The manager
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or * resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
+207
View File
@@ -200,6 +200,208 @@ enum class ECaveGeneratorType : uint8
Underwater UMETA(DisplayName = "Underwater (flooded cavern system)"), Underwater UMETA(DisplayName = "Underwater (flooded cavern system)"),
}; };
// Optional native-Core recipe selected independently for one strate.
// Recette Core native facultative, choisie independamment pour chaque strate.
UENUM(BlueprintType)
enum class EVoxelForgeCoreGeneratorRecipe : uint8
{
None UMETA(DisplayName = "Off (legacy recipe)"),
NativeCaves UMETA(DisplayName = "Native caves"),
NativeLattice UMETA(DisplayName = "Native lattice"),
ProfiledHeightfield UMETA(DisplayName = "Profiled heightfield")
};
UENUM(BlueprintType)
enum class EVoxelForgeCoreHeightfieldProfile : uint8
{
PeriodicHills,
MirroredRelief,
FlatField
};
UENUM(BlueprintType)
enum class EVoxelForgeCoreStampKind : uint8
{
CarveEllipsoid,
CarveArch,
FillPillar,
FillDais,
FillMonolith,
FillPlatform
};
// Stable authored volume stamp. All distances and positions are metres at the asset boundary.
// Volume stable, defini par son auteur. Toutes les positions et distances sont en metres.
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelForgeCoreStampDefinition
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
FString StableName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
EVoxelForgeCoreStampKind Kind = EVoxelForgeCoreStampKind::CarveEllipsoid;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector AnchorPositionMeters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double PlacementRadiusMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
FVector HalfExtentsMeters = FVector(2.0, 2.0, 2.0);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double SmoothWidthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityAmplitudeMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityWavelengthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double RoundingRadiusMeters = 0.5;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double MinimumSpacingMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (ClampMin = "0", ClampMax = "3"))
uint8 YawQuarterTurns = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
bool bAttachToFloor = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
double FloorMeters = 0.0;
};
// Authored cavern chamber, in local world metres.
// Salle cavernicole authored, en metres locaux du monde.
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelForgeCoreRoomDefinition
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
FString StableName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector CenterMeters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
FVector HalfExtentsMeters = FVector(8.0, 8.0, 4.0);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
double FloorMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double SmoothWidthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double UnionWidthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityAmplitudeMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityWavelengthMeters = 0.0;
};
// Authored cubic tunnel between a stable start anchor and a stable room name.
// Tunnel cubique authored entre une ancre stable et une salle nommee.
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelForgeCoreTunnelDefinition
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
FString StableName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
FString StartAnchorStableName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
FString EndRoomStableName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector ControlPoint0Meters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector ControlPoint1Meters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector ControlPoint2Meters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m"))
FVector ControlPoint3Meters = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0.5"))
double BoreRadiusMeters = 2.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "1"))
double BoreHeightMeters = 4.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double FloorSupportDepthMeters = 0.5;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double SmoothWidthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double UnionWidthMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityAmplitudeMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry", meta = (Units = "m", ClampMin = "0"))
double IrregularityWavelengthMeters = 0.0;
// Lattice tunnels require an explicit bounded polyline; cave tunnels use the cubic curve.
// Les tunnels en treillis exigent une polyligne bornee; les tunnels caves utilisent la courbe cubique.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
TArray<FVector> PolylineMeters;
};
// Optional Core authored recipe. Empty arrays and None preserve the prior asset output.
// Recette Core authored facultative. None et les tableaux vides conservent la sortie historique.
USTRUCT(BlueprintType)
struct VOXELFORGE_API FVoxelForgeCoreGeometry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
EVoxelForgeCoreGeneratorRecipe GeneratorRecipe = EVoxelForgeCoreGeneratorRecipe::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
EVoxelForgeCoreHeightfieldProfile HeightfieldProfile = EVoxelForgeCoreHeightfieldProfile::PeriodicHills;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
double HeightfieldBaseMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m"))
FVector2D HeightfieldAmplitudeMeters = FVector2D(20.0, 20.0);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
FVector2D HeightfieldWavelengthMeters = FVector2D(256.0, 256.0);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (ClampMin = "0.01", ClampMax = "4.0"))
double MirrorLambda = 1.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry|Heightfield", meta = (Units = "m", ClampMin = "0"))
double MinimumArenaClearanceMeters = 0.0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
TArray<FVoxelForgeCoreStampDefinition> Stamps;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
TArray<FVoxelForgeCoreRoomDefinition> Rooms;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Core Geometry")
TArray<FVoxelForgeCoreTunnelDefinition> Tunnels;
};
/** /**
* EVoxelStrateTransition — How two adjacent strates meet at their shared boundary. * EVoxelStrateTransition — How two adjacent strates meet at their shared boundary.
* *
@@ -2289,6 +2491,11 @@ struct VOXELFORGE_API FStrateLandmark
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark")
ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice; ELandmarkAnchor AnchorMode = ELandmarkAnchor::HashLattice;
// Optional stable Core feature binding; an empty value keeps lattice/mouth placement unchanged.
// Liaison facultative vers une ancre Core stable; vide, le placement existant reste identique.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Landmark|Core Geometry")
FString CoreGeometryStableName;
// ----- Hash lattice (AnchorMode == HashLattice — scattered placement; cheap at any radius) ----- // ----- Hash lattice (AnchorMode == HashLattice — scattered placement; cheap at any radius) -----
// Average spacing between instances, IN CHUNKS = the lattice cell size (one candidate per cell, so cost // Average spacing between instances, IN CHUNKS = the lattice cell size (one candidate per cell, so cost
+21 -4
View File
@@ -332,16 +332,33 @@ struct FVoxelTileKey
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {} FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
// Cell size (sampling step) in voxels, and tile extent in voxels per axis. // Cell size (sampling step) in voxels, and tile extent in voxels per axis.
int32 StepVoxels() const { return 1 << Level; } int32 StepVoxels() const
int32 ExtentVoxels() const { return CHUNK_SIZE << Level; } {
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
return static_cast<int32>(uint32{1} << SafeLevel);
}
int32 ExtentVoxels() const
{
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
return static_cast<int32>(static_cast<uint32>(CHUNK_SIZE) * (uint32{1} << SafeLevel));
}
// Min-corner of the tile in VOXEL coords (what the mesher takes). // Min-corner of the tile in VOXEL coords (what the mesher takes).
FIntVector OriginVoxels() const { return Coord * (CHUNK_SIZE << Level); } FIntVector OriginVoxels() const
{
const int64 Extent = ExtentVoxels();
const auto ScaleAxis = [Extent](int32 Axis)
{
return static_cast<int32>(FMath::Clamp<int64>(
static_cast<int64>(Axis) * Extent, MIN_int32, MAX_int32));
};
return FIntVector(ScaleAxis(Coord.X), ScaleAxis(Coord.Y), ScaleAxis(Coord.Z));
}
// Tile centre in world cm (for distance sorting / LOD selection). // Tile centre in world cm (for distance sorting / LOD selection).
FVector CenterCm() const FVector CenterCm() const
{ {
const double Ext = (double)(CHUNK_SIZE << Level) * (double)VOXEL_SIZE; const double Ext = static_cast<double>(ExtentVoxels()) * static_cast<double>(VOXEL_SIZE);
return FVector((Coord.X + 0.5) * Ext, (Coord.Y + 0.5) * Ext, (Coord.Z + 0.5) * Ext); return FVector((Coord.X + 0.5) * Ext, (Coord.Y + 0.5) * Ext, (Coord.Z + 0.5) * Ext);
} }
+19
View File
@@ -145,6 +145,10 @@ struct FChunkResult
// per-tile profiling remains opt-in. // per-tile profiling remains opt-in.
uint64 GenerationStartCycles = 0; uint64 GenerationStartCycles = 0;
uint64 GenerationEndCycles = 0; uint64 GenerationEndCycles = 0;
// Optional phase timings recorded only while the bounded startup trace is active.
double CoreGenerateSeconds = 0.0;
double MeshCopySeconds = 0.0;
double StreamBuildSeconds = 0.0;
uint64 ApplyStartCycles = 0; uint64 ApplyStartCycles = 0;
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon; EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
// Ordinals make admitted order and actual worker start order auditable across MPSC results. // Ordinals make admitted order and actual worker start order auditable across MPSC results.
@@ -352,6 +356,10 @@ public:
* the first resolved collidable support tile. */ * the first resolved collidable support tile. */
TSet<FVoxelTileKey> CollisionNotRequiredTiles; TSet<FVoxelTileKey> CollisionNotRequiredTiles;
/** Known-empty collision classifications survive unload for the tile's current edit epoch. */
TMap<FVoxelTileKey, uint32> KnownEmptyCollisionTileEpoch;
TSet<FVoxelTileKey> KnownEmptyCollisionSolidTiles;
/** Subset of CollisionNotRequiredTiles proved all-solid. A support probe inside one of these /** Subset of CollisionNotRequiredTiles proved all-solid. A support probe inside one of these
* tiles searches upward for the boundary tile; all-air/empty tiles search downward. */ * tiles searches upward for the boundary tile; all-air/empty tiles search downward. */
TSet<FVoxelTileKey> CollisionSolidTiles; TSet<FVoxelTileKey> CollisionSolidTiles;
@@ -614,6 +622,7 @@ public:
FVector GetVoxelForgeCoreDefaultCenterWorld() const; FVector GetVoxelForgeCoreDefaultCenterWorld() const;
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const; bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const; bool GetVoxelForgeCoreWorldAssembly(FString& OutCanonicalJson, FString& OutContentHash) const;
bool GetVoxelForgeCoreResolvedLayout(FString& OutCanonicalJson, FString& OutStableIdentity) const;
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate") UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
int32 GetStrateAtPosition(FVector WorldPosition) const; int32 GetStrateAtPosition(FVector WorldPosition) const;
@@ -778,7 +787,10 @@ private:
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState; TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
FString VoxelForgeCoreWorldAssemblyJson; FString VoxelForgeCoreWorldAssemblyJson;
FString VoxelForgeCoreWorldAssemblyHash; FString VoxelForgeCoreWorldAssemblyHash;
FString VoxelForgeCoreResolvedLayoutJson;
FString VoxelForgeCoreResolvedLayoutIdentity;
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector; FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
int32 CoreRenderDistanceChunks = 0;
bool bCoreRegionCompilePending = false; bool bCoreRegionCompilePending = false;
TArray<FVoxelTileKey> PendingCoreRegionTiles; TArray<FVoxelTileKey> PendingCoreRegionTiles;
TSet<FVoxelTileKey> PendingCoreRegionTileSet; TSet<FVoxelTileKey> PendingCoreRegionTileSet;
@@ -1194,6 +1206,9 @@ public:
// and records the ones ONLY a CollisionOnly anchor wants in CollisionOnlyTiles. // and records the ones ONLY a CollisionOnly anchor wants in CollisionOnlyTiles.
void AddAnchorDesiredTiles(); void AddAnchorDesiredTiles();
void ReconcileCollisionDesiredTiles(); void ReconcileCollisionDesiredTiles();
void RestoreKnownEmptyCollisionTiles();
void RebaseKnownEmptyCollisionEpoch();
void InvalidateKnownEmptyCollisionTile(const FVoxelTileKey& Tile, const TCHAR* Reason);
// Forces a desired-set rebuild next Tick even if neither the player nor an anchor crossed a tile // Forces a desired-set rebuild next Tick even if neither the player nor an anchor crossed a tile
// boundary (set by UnregisterStreamingAnchor so a removed anchor's tiles get culled). // boundary (set by UnregisterStreamingAnchor so a removed anchor's tiles get culled).
bool bForceDesiredRebuild = false; bool bForceDesiredRebuild = false;
@@ -1229,10 +1244,13 @@ public:
bool bSampleStacksStarted = false; bool bSampleStacksStarted = false;
bool bStartupApplyBoostStarted = false; bool bStartupApplyBoostStarted = false;
bool bStartupApplyBoostEnded = false; bool bStartupApplyBoostEnded = false;
uint64 StartupApplyBoostStartCycles = 0;
// Trace the resolved startup budget once per world. / Tracer une fois le budget de demarrage resolu par monde. // Trace the resolved startup budget once per world. / Tracer une fois le budget de demarrage resolu par monde.
bool bStartupApplyConfigTraced = false; bool bStartupApplyConfigTraced = false;
uint64 LastApplyFrameCycles = 0; uint64 LastApplyFrameCycles = 0;
float StartupTraceSpeedCmPerSecond = 0.0f; float StartupTraceSpeedCmPerSecond = 0.0f;
bool bSpeedAwareTravelMode = false;
bool bSpeedAwareFastMode = false;
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this // Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
@@ -1340,6 +1358,7 @@ public:
double HeadlessStreamingTestDistanceCm = 0.0; double HeadlessStreamingTestDistanceCm = 0.0;
float HeadlessStreamingTestSpeedCmPerSecond = 0.0f; float HeadlessStreamingTestSpeedCmPerSecond = 0.0f;
float HeadlessStreamingTestStartSeconds = 0.0f; float HeadlessStreamingTestStartSeconds = 0.0f;
float HeadlessStreamingTestMoveDurationSeconds = 0.0f;
float HeadlessStreamingTestExitSeconds = 0.0f; float HeadlessStreamingTestExitSeconds = 0.0f;
float HeadlessStreamingTestTeleportAtSeconds = 0.0f; float HeadlessStreamingTestTeleportAtSeconds = 0.0f;
float HeadlessStreamingTestTeleportDistanceCm = 0.0f; float HeadlessStreamingTestTeleportDistanceCm = 0.0f;
+7
View File
@@ -7,6 +7,13 @@ public class VoxelForge : ModuleRules
{ {
public VoxelForge(ReadOnlyTargetRules Target) : base(Target) public VoxelForge(ReadOnlyTargetRules Target) : base(Target)
{ {
if (Target.Platform == UnrealTargetPlatform.Linux)
{
// Test translation units have independent anonymous-namespace helpers that collide in
// Linux Unity batches. Keep Linux compilation one source per TU; Windows remains unchanged.
bUseUnity = false;
}
// PCH = Precompiled Headers - speeds up compilation // PCH = Precompiled Headers - speeds up compilation
// UseExplicitOrSharedPCHs is the modern recommended setting // UseExplicitOrSharedPCHs is the modern recommended setting
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
@@ -39,6 +39,13 @@ public class VoxelForgeCore : ModuleRules
PublicDependencyModuleNames.Add("Core"); PublicDependencyModuleNames.Add("Core");
CppStandard = CppStandardVersion.Cpp20; CppStandard = CppStandardVersion.Cpp20;
if (Target.Platform == UnrealTargetPlatform.Linux)
{
// The compatibility include directory contains Unreal-shaped placeholders. On Linux,
// let generated module TUs pass those names through to UE while Core shims opt in.
PrivateDefinitions.Add("VFCORE_COMPAT_FORWARD_HEADERS=1");
MinCpuArchX64 = MinimumCpuArchitectureX64.AVX2;
}
// Canonical Standalone code predates UE's stricter warning defaults; keep this narrow to legal local shadowing. // Canonical Standalone code predates UE's stricter warning defaults; keep this narrow to legal local shadowing.
CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off; CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off;
FPSemantics = FPSemanticsMode.Precise; FPSemantics = FPSemanticsMode.Precise;
@@ -878,6 +878,8 @@ struct FExploreArguments
bool bPassageFloorWritesOverride = false; bool bPassageFloorWritesOverride = false;
int32 PassageFloorWrites = 1; int32 PassageFloorWrites = 1;
bool bStandaloneWorldExport = false; bool bStandaloneWorldExport = false;
bool bOwnerFourStratePlan = false;
bool bSkipStandaloneCoreAdapterParity = false;
bool bCoreWorldTest = false; bool bCoreWorldTest = false;
bool bCoreWorldStreamTraceOnly = false; bool bCoreWorldStreamTraceOnly = false;
bool bRenderCameraOverride = false; bool bRenderCameraOverride = false;
@@ -922,6 +924,12 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
int32 StandaloneWorldExport = 0; int32 StandaloneWorldExport = 0;
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport); FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0; OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
int32 OwnerFourStratePlan = 0;
FParse::Value(*Params, TEXT("ownerfourstrateplan="), OwnerFourStratePlan);
OutArguments.bOwnerFourStratePlan = OwnerFourStratePlan != 0;
int32 SkipStandaloneCoreAdapterParity = 0;
FParse::Value(*Params, TEXT("skipstandalonecoreadapterparity="), SkipStandaloneCoreAdapterParity);
OutArguments.bSkipStandaloneCoreAdapterParity = SkipStandaloneCoreAdapterParity != 0;
int32 CoreWorldTest = 0; int32 CoreWorldTest = 0;
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest); FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
OutArguments.bCoreWorldTest = CoreWorldTest != 0; OutArguments.bCoreWorldTest = CoreWorldTest != 0;
@@ -1266,12 +1274,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
return false; return false;
} }
// The historical explorer contract needs two adjacent mouths around an interior slot. A // Passage walks need both mouths; a render/export-only generation can inspect an end slot.
// parity export can instead target the game's authored single-strate layout, which has no // Parity exports also accept the game's authored single-strate layout.
// such mouths and is valid for export-only mode. Build() still validates the slot against const bool bRequiresInteriorSlot = OutArguments.bWalk;
// the loaded settings layout. const int32 MinimumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot ? 0 : 1;
const int32 MinimumSlot = OutArguments.bGameRuntimeConfig ? 0 : 1; const int32 MaximumSlot = OutArguments.bGameRuntimeConfig || !bRequiresInteriorSlot
const int32 MaximumSlot = OutArguments.bGameRuntimeConfig
? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2; ? MaxSyntheticStrates - 1 : MaxSyntheticStrates - 2;
if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot) if (OutArguments.Slot < MinimumSlot || OutArguments.Slot > MaximumSlot)
{ {
@@ -1284,8 +1291,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
else else
{ {
OutError = FString::Printf( OutError = FString::Printf(
TEXT("slot must be an interior slot in [1,%d] so arrival and departure mouths exist."), TEXT("slot must be in [0,%d]; walk mode requires an interior slot in [1,%d] for both passage mouths."),
MaxSyntheticStrates - 2); MaxSyntheticStrates - 1, MaxSyntheticStrates - 2);
} }
return false; return false;
} }
@@ -1498,6 +1505,17 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref."); OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
return false; return false;
} }
if (OutArguments.bOwnerFourStratePlan
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
{
OutError = TEXT("ownerfourstrateplan requires settingsref and must omit strateref.");
return false;
}
if (OutArguments.bSkipStandaloneCoreAdapterParity && !OutArguments.bStandaloneWorldExport)
{
OutError = TEXT("skipstandalonecoreadapterparity requires standaloneworldexport.");
return false;
}
if (OutArguments.bCoreWorldTest if (OutArguments.bCoreWorldTest
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty())) && (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
{ {
@@ -1702,6 +1720,66 @@ struct FExploreMeshBvhNode
} }
}; };
bool ApplyOwnerFourStratePlan(UVoxelSettings* Settings,
TArray<TStrongObjectPtr<UVoxelStrateDefinition>>& OutDefinitions,
bool bDuplicateDefinitions, FString& OutError)
{
if (Settings == nullptr)
{
OutError = TEXT("The transient owner plan has no settings object.");
return false;
}
if (!Settings->Season.IsNull())
{
OutError = TEXT("The transient four-strate owner plan cannot replace an assigned season manifest.");
return false;
}
static const TCHAR* AssetPaths[] = {
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
};
Settings->TotalStrates = UE_ARRAY_COUNT(AssetPaths);
Settings->StratePool.Reset();
Settings->FixedStrates.Reset();
OutDefinitions.Reset();
OutDefinitions.Reserve(UE_ARRAY_COUNT(AssetPaths));
for (int32 Index = 0; Index < UE_ARRAY_COUNT(AssetPaths); ++Index)
{
UVoxelStrateDefinition* AuthoredDefinition = LoadObject<UVoxelStrateDefinition>(
nullptr, AssetPaths[Index]);
if (AuthoredDefinition == nullptr)
{
OutError = FString::Printf(
TEXT("Could not load staged owner strate asset '%s'."), AssetPaths[Index]);
return false;
}
UVoxelStrateDefinition* Definition = AuthoredDefinition;
if (bDuplicateDefinitions)
{
Definition = DuplicateObject<UVoxelStrateDefinition>(
AuthoredDefinition, GetTransientPackage(),
FName(*FString::Printf(TEXT("VoxelForgeOwnerPlan_Strate_%d"), Index)));
if (Definition == nullptr)
{
OutError = FString::Printf(
TEXT("Could not make a transient copy of owner strate asset '%s'."), AssetPaths[Index]);
return false;
}
}
Settings->FixedStrates.Add(Index,
TSoftObjectPtr<UVoxelStrateDefinition>(Definition));
OutDefinitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeOwnerPlan] transient fixed slots=4 assets=DA_Strate1,DA_Strate2,DA_Strate3,DA_Strate4 settings_asset_untouched=1"));
return true;
}
struct FExploreWorld struct FExploreWorld
{ {
TStrongObjectPtr<UVoxelSettings> Settings; TStrongObjectPtr<UVoxelSettings> Settings;
@@ -1834,7 +1912,7 @@ struct FExploreWorld
Settings->InterStrateGapChunks, Settings->SkirtCells); Settings->InterStrateGapChunks, Settings->SkirtCells);
} }
const int32 NumStrates = bUseAuthoredSettings const int32 NumStrates = Arguments.bOwnerFourStratePlan ? 4 : bUseAuthoredSettings
? (Arguments.bGameRuntimeConfig ? (Arguments.bGameRuntimeConfig
? FMath::Max(Settings->TotalStrates, 1) ? FMath::Max(Settings->TotalStrates, 1)
: FMath::Max(Settings->TotalStrates, Arguments.Slot + 2)) : FMath::Max(Settings->TotalStrates, Arguments.Slot + 2))
@@ -1864,10 +1942,18 @@ struct FExploreWorld
// loaded package remains untouched. // loaded package remains untouched.
Settings->TotalStrates = NumStrates; Settings->TotalStrates = NumStrates;
if (Arguments.bOwnerFourStratePlan)
{
if (!ApplyOwnerFourStratePlan(Settings.Get(), Definitions, true, OutError))
{
return false;
}
}
else
{
if (bUseAuthoredStrate) if (bUseAuthoredStrate)
{ {
// The owner’s snapshot states that every slot uses DA_Strate3. Clone it per slot so // Keep the historical single-asset interior probe isolated from loaded packages.
// commandlet overrides never mutate a loaded package or leak between slots.
Settings->StratePool.Reset(); Settings->StratePool.Reset();
Settings->FixedStrates.Reset(); Settings->FixedStrates.Reset();
} }
@@ -1923,6 +2009,7 @@ struct FExploreWorld
} }
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition)); Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
} }
}
auto ApplyTargetOverrides = [&Arguments](UVoxelStrateDefinition* Definition) -> bool auto ApplyTargetOverrides = [&Arguments](UVoxelStrateDefinition* Definition) -> bool
{ {
@@ -13472,14 +13559,16 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
const std::array<std::uint64_t, 4> Actual = StandaloneCoreAggregateHashes(Rows); const std::array<std::uint64_t, 4> Actual = StandaloneCoreAggregateHashes(Rows);
const std::array<std::uint64_t, 4> Expected{ const std::array<std::uint64_t, 4> Expected{
0xdb2991c73215af4cull, 0x8236c5edd7fc8e4full, 0xde345030348f2c5dull, 0xcbf0888004618b87ull,
0x4c48fa75a543d58eull, 0xbea22e76b34a4dfcull}; 0x7a11e28e052d69a8ull, 0x52b88607e8dbcbfdull};
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats(); const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
const bool bPassageWalkabilityPassed = Stats.walkSamples == 5642 && Stats.walkFailures == 0; constexpr std::uint64_t ExpectedWalkSamples = 21425;
if (Rows.size() != 2226 || Actual != Expected || !bPassageWalkabilityPassed) const bool bPassageWalkabilityPassed = Stats.walkSamples == ExpectedWalkSamples
&& Stats.walkFailures == 0;
if (Rows.size() != 2578 || Actual != Expected || !bPassageWalkabilityPassed)
{ {
OutError = FString::Printf( OutError = FString::Printf(
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected=5642/5642"), TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected_walk=%llu"),
static_cast<unsigned long long>(WorkerLimit), static_cast<unsigned long long>(WorkerLimit),
static_cast<unsigned long long>(Rows.size()), static_cast<unsigned long long>(Rows.size()),
static_cast<unsigned long long>(Actual[0]), static_cast<unsigned long long>(Actual[0]),
@@ -13487,12 +13576,13 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
static_cast<unsigned long long>(Actual[2]), static_cast<unsigned long long>(Actual[2]),
static_cast<unsigned long long>(Actual[3]), static_cast<unsigned long long>(Actual[3]),
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures), static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
static_cast<unsigned long long>(Stats.walkSamples)); static_cast<unsigned long long>(Stats.walkSamples),
static_cast<unsigned long long>(ExpectedWalkSamples));
return false; return false;
} }
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreAdapter] pass schema=1 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=5642/5642"), TEXT("[VoxelForgeCoreAdapter] pass schema=2 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu"),
static_cast<unsigned long long>(WorkerLimit), static_cast<unsigned long long>(WorkerLimit),
OwnerClipRadius, OwnerClipRadius,
OwnerMaxLod, OwnerMaxLod,
@@ -13500,7 +13590,9 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
static_cast<unsigned long long>(Actual[0]), static_cast<unsigned long long>(Actual[0]),
static_cast<unsigned long long>(Actual[1]), static_cast<unsigned long long>(Actual[1]),
static_cast<unsigned long long>(Actual[2]), static_cast<unsigned long long>(Actual[2]),
static_cast<unsigned long long>(Actual[3])); static_cast<unsigned long long>(Actual[3]),
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
static_cast<unsigned long long>(ExpectedWalkSamples));
} }
return true; return true;
} }
@@ -13515,6 +13607,24 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
*Arguments.SettingsReference); *Arguments.SettingsReference);
return false; return false;
} }
TStrongObjectPtr<UVoxelSettings> OwnerPlanSettings;
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
UVoxelSettings* ExportSettings = AuthoredSettings;
if (Arguments.bOwnerFourStratePlan)
{
OwnerPlanSettings = TStrongObjectPtr<UVoxelSettings>(
DuplicateObject<UVoxelSettings>(AuthoredSettings, GetTransientPackage()));
if (!OwnerPlanSettings.IsValid())
{
OutError = TEXT("Could not make a transient copy of owner settings for the four-strate plan.");
return false;
}
ExportSettings = OwnerPlanSettings.Get();
if (!ApplyOwnerFourStratePlan(ExportSettings, OwnerPlanDefinitions, false, OutError))
{
return false;
}
}
UVoxelStrateManager* Manager = NewObject<UVoxelStrateManager>( UVoxelStrateManager* Manager = NewObject<UVoxelStrateManager>(
GetTransientPackage(), NAME_None, RF_Transient); GetTransientPackage(), NAME_None, RF_Transient);
int32 Seed = AuthoredSettings->Seed; int32 Seed = AuthoredSettings->Seed;
@@ -13526,7 +13636,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
OriginSpineRadius = Season->GetOriginSpineRadius(); OriginSpineRadius = Season->GetOriginSpineRadius();
WorldRadiusVoxels = Season->GetWorldRadiusVoxels(); WorldRadiusVoxels = Season->GetWorldRadiusVoxels();
} }
if (!Manager->Initialize(AuthoredSettings, Seed)) if (!Manager->Initialize(ExportSettings, Seed))
{ {
OutError = TEXT("Owner settings did not produce a valid resolved strate layout."); OutError = TEXT("Owner settings did not produce a valid resolved strate layout.");
return false; return false;
@@ -13534,7 +13644,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
Seed = Manager->GetWorldSeed(); Seed = Manager->GetWorldSeed();
UVoxelGenerator* Generator = NewObject<UVoxelGenerator>( UVoxelGenerator* Generator = NewObject<UVoxelGenerator>(
GetTransientPackage(), NAME_None, RF_Transient); GetTransientPackage(), NAME_None, RF_Transient);
Generator->InitializeSettings(AuthoredSettings); Generator->InitializeSettings(ExportSettings);
Generator->SetStrateManager(Manager); Generator->SetStrateManager(Manager);
TArray<FStandaloneExportSample> Samples; TArray<FStandaloneExportSample> Samples;
@@ -13668,25 +13778,25 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export")); Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export"));
Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm); Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm);
Writer->WriteValue(TEXT("seed"), Seed); Writer->WriteValue(TEXT("seed"), Seed);
Writer->WriteValue(TEXT("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry); Writer->WriteValue(TEXT("open_surface_entry"), ExportSettings->bOpenSurfaceEntry);
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius); Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
Writer->WriteValue(TEXT("world_radius_voxels"), WorldRadiusVoxels); Writer->WriteValue(TEXT("world_radius_voxels"), WorldRadiusVoxels);
Writer->WriteValue(TEXT("edge_seal_thickness"), AuthoredSettings->EdgeSealThickness); Writer->WriteValue(TEXT("edge_seal_thickness"), ExportSettings->EdgeSealThickness);
Writer->WriteValue(TEXT("total_strates"), Manager->GetNumStrates()); Writer->WriteValue(TEXT("total_strates"), Manager->GetNumStrates());
Writer->WriteValue(TEXT("inter_strate_gap_chunks"), AuthoredSettings->InterStrateGapChunks); Writer->WriteValue(TEXT("inter_strate_gap_chunks"), ExportSettings->InterStrateGapChunks);
Writer->WriteValue(TEXT("clip_radius"), AuthoredSettings->ClipRadius); Writer->WriteValue(TEXT("clip_radius"), ExportSettings->ClipRadius);
Writer->WriteValue(TEXT("max_clip_level"), AuthoredSettings->MaxClipLevel); Writer->WriteValue(TEXT("max_clip_level"), ExportSettings->MaxClipLevel);
Writer->WriteValue(TEXT("full_res_clip_levels"), AuthoredSettings->FullResClipLevels); Writer->WriteValue(TEXT("full_res_clip_levels"), ExportSettings->FullResClipLevels);
Writer->WriteValue(TEXT("coarse_tile_cells"), AuthoredSettings->CoarseTileCells); Writer->WriteValue(TEXT("coarse_tile_cells"), ExportSettings->CoarseTileCells);
Writer->WriteValue(TEXT("render_distance_chunks"), AuthoredSettings->RenderDistanceChunks); Writer->WriteValue(TEXT("render_distance_chunks"), ExportSettings->RenderDistanceChunks);
Writer->WriteValue(TEXT("generate_skirts"), AuthoredSettings->bGenerateSkirts); Writer->WriteValue(TEXT("generate_skirts"), ExportSettings->bGenerateSkirts);
Writer->WriteValue(TEXT("skirt_cells"), AuthoredSettings->SkirtCells); Writer->WriteValue(TEXT("skirt_cells"), ExportSettings->SkirtCells);
Writer->WriteValue(TEXT("lod_octave_drop"), AuthoredSettings->LODOctaveDrop); Writer->WriteValue(TEXT("lod_octave_drop"), ExportSettings->LODOctaveDrop);
Writer->WriteValue(TEXT("strate_content_cut_min_level"), AuthoredSettings->GetEffectiveStrateContentCutMinLevel()); Writer->WriteValue(TEXT("strate_content_cut_min_level"), ExportSettings->GetEffectiveStrateContentCutMinLevel());
TSet<const UObject*> ActiveObjects; TSet<const UObject*> ActiveObjects;
StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"), StandaloneWriteJsonValue(*Writer, TEXT("settings_asset"),
StandaloneSerializeStruct(AuthoredSettings->GetClass(), AuthoredSettings, StandaloneSerializeStruct(ExportSettings->GetClass(), ExportSettings,
ActiveObjects, AuthoredSettings)); ActiveObjects, ExportSettings));
Writer->WriteArrayStart(TEXT("strates")); Writer->WriteArrayStart(TEXT("strates"));
for (const FStrateSlot& Slot : Manager->GetLayout()) for (const FStrateSlot& Slot : Manager->GetLayout())
@@ -13756,6 +13866,68 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
Writer->Close(); Writer->Close();
// Export the exact Core manifest and resolved layout that the Unreal adapter just
// generated from these live settings and slots. The standalone research tools consume
// these snapshots instead of maintaining a second copy of the planning rules.
FString RuntimeSnapshotJson;
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
ExportSettings, Manager, 3, 8, StandaloneCoreVoxelSizeCm,
RuntimeSnapshotJson, OutError))
{
return false;
}
FTCHARToUTF8 RuntimeSnapshotUtf8(*RuntimeSnapshotJson);
std::string CoreSnapshotError;
const std::optional<VoxelForgeCore::ResolvedWorldSnapshot> CoreSnapshot =
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
std::string_view(RuntimeSnapshotUtf8.Get(),
static_cast<std::size_t>(RuntimeSnapshotUtf8.Length())), CoreSnapshotError);
if (!CoreSnapshot || CoreSnapshot->worldAssembly() == nullptr
|| CoreSnapshot->resolvedLayout() == nullptr)
{
OutError = FString::Printf(
TEXT("Core adapter export did not produce a complete manifest and resolved layout: %s"),
UTF8_TO_TCHAR(CoreSnapshotError.c_str()));
return false;
}
const std::string CoreAssemblyText = CoreSnapshot->worldAssembly()->ToJson();
const std::string CoreLayoutText = CoreSnapshot->resolvedLayout()->ToJson();
if (CoreAssemblyText.empty() || CoreLayoutText.empty())
{
OutError = TEXT("Core adapter export produced an empty canonical manifest or layout.");
return false;
}
const FString CanonicalAssemblyJson = UTF8_TO_TCHAR(CoreAssemblyText.c_str());
const FString CanonicalLayoutJson = UTF8_TO_TCHAR(CoreLayoutText.c_str());
TSharedPtr<FJsonObject> ExportRoot;
const TSharedRef<TJsonReader<>> ExportReader = TJsonReaderFactory<>::Create(Json);
if (!FJsonSerializer::Deserialize(ExportReader, ExportRoot) || !ExportRoot.IsValid())
{
OutError = TEXT("Could not reopen the owner export to attach the Core plan snapshots.");
return false;
}
TSharedPtr<FJsonObject> AssemblyObject;
const TSharedRef<TJsonReader<>> AssemblyReader =
TJsonReaderFactory<>::Create(CanonicalAssemblyJson);
if (!FJsonSerializer::Deserialize(AssemblyReader, AssemblyObject) || !AssemblyObject.IsValid())
{
OutError = TEXT("Could not parse the Core adapter's canonical manifest for export.");
return false;
}
ExportRoot->SetObjectField(TEXT("world_assembly"), AssemblyObject.ToSharedRef());
ExportRoot->SetStringField(TEXT("core_world_assembly_json"), CanonicalAssemblyJson);
ExportRoot->SetStringField(TEXT("resolved_layout_json"), CanonicalLayoutJson);
FString ExportedJson;
const TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> ExportWriter =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&ExportedJson);
if (!FJsonSerializer::Serialize(ExportRoot.ToSharedRef(), ExportWriter))
{
OutError = TEXT("Could not serialize the owner export with its canonical Core plan.");
return false;
}
Json = MoveTemp(ExportedJson);
const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json")); const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json"));
if (!FFileHelper::SaveStringToFile( if (!FFileHelper::SaveStringToFile(
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
@@ -13764,11 +13936,16 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
return false; return false;
} }
if (OutJson) *OutJson = Json; if (OutJson) *OutJson = Json;
if (!VerifyStandaloneCoreAdapter(Json, OutError)) if (!Arguments.bSkipStandaloneCoreAdapterParity && !VerifyStandaloneCoreAdapter(Json, OutError))
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
return false; return false;
} }
if (Arguments.bSkipStandaloneCoreAdapterParity)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreAdapter] skipped legacy W1/W8/W16 fixture parity for this export-only update."));
}
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"), TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
Seed, StandaloneCoreVoxelSizeCm, Manager->GetNumStrates(), Manager->GetPassages().Num(), Seed, StandaloneCoreVoxelSizeCm, Manager->GetNumStrates(), Manager->GetPassages().Num(),
@@ -14808,7 +14985,7 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
UVoxelSettings* Source, int32 Seed, UVoxelSettings* Source, int32 Seed,
TStrongObjectPtr<UVoxelSettings>& OutSettings, TStrongObjectPtr<UVoxelSettings>& OutSettings,
TStrongObjectPtr<UVoxelStrateDefinition>& OutSurfaceDefinition, TStrongObjectPtr<UVoxelStrateDefinition>& OutSurfaceDefinition,
FString& OutError) FString& OutError, bool bCompactTracePlan = false)
{ {
if (Source == nullptr) if (Source == nullptr)
{ {
@@ -14824,11 +15001,10 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
nullptr, TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1")); nullptr, TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"));
if (SurfaceDefinition == nullptr if (SurfaceDefinition == nullptr
|| SurfaceDefinition->GeneratorType != ECaveGeneratorType::SurfaceWorld || SurfaceDefinition->GeneratorType != ECaveGeneratorType::SurfaceWorld
|| SurfaceDefinition->StrateHeightInChunks <= 0 || SurfaceDefinition->StrateHeightInChunks <= 0)
|| Source->TotalStrates != 1)
{ {
OutError = FString::Printf( OutError = FString::Printf(
TEXT("The staged owner assets do not describe the logged single SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."), TEXT("The staged owner assets do not contain a usable DA_Strate1 SurfaceWorld (definition_loaded=%d type=%d height=%d settings_strates=%d)."),
SurfaceDefinition != nullptr ? 1 : 0, SurfaceDefinition != nullptr ? 1 : 0,
SurfaceDefinition != nullptr ? static_cast<int32>(SurfaceDefinition->GeneratorType) : INDEX_NONE, SurfaceDefinition != nullptr ? static_cast<int32>(SurfaceDefinition->GeneratorType) : INDEX_NONE,
SurfaceDefinition != nullptr ? SurfaceDefinition->StrateHeightInChunks : INDEX_NONE, SurfaceDefinition != nullptr ? SurfaceDefinition->StrateHeightInChunks : INDEX_NONE,
@@ -14843,9 +15019,24 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
return false; return false;
} }
OutSettings = TStrongObjectPtr<UVoxelSettings>(SettingsCopy); OutSettings = TStrongObjectPtr<UVoxelSettings>(SettingsCopy);
if (bCompactTracePlan)
{
SurfaceDefinition = DuplicateObject<UVoxelStrateDefinition>(SurfaceDefinition,
GetTransientPackage(), TEXT("VoxelForgeCompactSurfaceTraceDefinition"));
if (SurfaceDefinition == nullptr)
{
OutError = TEXT("Could not make a transient DA_Strate1 copy for the compact common-plan trace.");
return false;
}
// L14's historical fixed-length SurfaceWorld route search cannot place the
// new trace fixture even at 16 chunks. Use the same simple, walkable FlatPlain
// plan in both host revisions so the streaming comparison reaches AVoxelWorld.
SurfaceDefinition->GeneratorType = ECaveGeneratorType::FlatPlain;
SurfaceDefinition->StrateHeightInChunks = 16;
}
OutSurfaceDefinition = TStrongObjectPtr<UVoxelStrateDefinition>(SurfaceDefinition); OutSurfaceDefinition = TStrongObjectPtr<UVoxelStrateDefinition>(SurfaceDefinition);
SettingsCopy->Seed = Seed; SettingsCopy->Seed = Seed;
SettingsCopy->TotalStrates = Source->TotalStrates; SettingsCopy->TotalStrates = 1;
SettingsCopy->StratePool.Reset(); SettingsCopy->StratePool.Reset();
SettingsCopy->FixedStrates.Reset(); SettingsCopy->FixedStrates.Reset();
SettingsCopy->FixedStrates.Add(0, SettingsCopy->FixedStrates.Add(0,
@@ -14855,8 +15046,10 @@ bool CreateLoggedOwnerSurfaceSettingsCopy(
SettingsCopy->bUseWorldAssemblyManifestOverride = false; SettingsCopy->bUseWorldAssemblyManifestOverride = false;
const int32 BottomChunkZ = 1 - SurfaceDefinition->StrateHeightInChunks; const int32 BottomChunkZ = 1 - SurfaceDefinition->StrateHeightInChunks;
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 seed=%d strata=1 bounds=[0,%d] height_chunks=%d gap=%d imported_override=cleared"), TEXT("[VoxelForgeCoreTestLayout] source=owner_asset_transient_copy definition=DA_Strate1 generator=%d seed=%d strata=1 bounds=[0,%d] height_chunks=%d compact_trace=%d gap=%d imported_override=cleared"),
Seed, BottomChunkZ, SurfaceDefinition->StrateHeightInChunks, SettingsCopy->InterStrateGapChunks); static_cast<int32>(SurfaceDefinition->GeneratorType), Seed, BottomChunkZ,
SurfaceDefinition->StrateHeightInChunks,
bCompactTracePlan ? 1 : 0, SettingsCopy->InterStrateGapChunks);
return true; return true;
} }
@@ -16432,7 +16625,15 @@ bool RunCoreFloatingIslandActorIntegrationTest(
} }
float StagedWalkSpeedCmPerSecond = 0.0f; float StagedWalkSpeedCmPerSecond = 0.0f;
float StagedSprintSpeedCmPerSecond = 0.0f; float StagedSprintSpeedCmPerSecond = 0.0f;
if (!ReadStagedPlayerSpeeds( if (Arguments.bCoreWorldStreamTraceOnly)
{
StagedWalkSpeedCmPerSecond = 600.0f;
StagedSprintSpeedCmPerSecond = 6000.0f;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgePawnSpeedFixture] source=trace_fixture_defaults walk_cm_s=%.3f sprint_cm_s=%.3f"),
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond);
}
else if (!ReadStagedPlayerSpeeds(
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError)) StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
{ {
return false; return false;
@@ -17065,7 +17266,8 @@ bool RunCoreFloatingIslandActorIntegrationTest(
{ {
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false; if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
for (const FVoxelTileKey& Key : World->DesiredSorted) for (const FVoxelTileKey& Key : World->DesiredSorted)
if (!World->LoadedTiles.Contains(Key)) return false; if (!World->LoadedTiles.Contains(Key)
&& !World->CollisionNotRequiredTiles.Contains(Key)) return false;
return true; return true;
}; };
const auto CollisionSetSettled = [](const AVoxelWorld* World) const auto CollisionSetSettled = [](const AVoxelWorld* World)
@@ -17654,7 +17856,15 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
float StagedWalkSpeedCmPerSecond = 0.0f; float StagedWalkSpeedCmPerSecond = 0.0f;
float StagedSprintSpeedCmPerSecond = 0.0f; float StagedSprintSpeedCmPerSecond = 0.0f;
if (!ReadStagedPlayerSpeeds( if (Arguments.bCoreWorldStreamTraceOnly)
{
StagedWalkSpeedCmPerSecond = 600.0f;
StagedSprintSpeedCmPerSecond = 6000.0f;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgePawnSpeedFixture] source=trace_fixture_defaults walk_cm_s=%.3f sprint_cm_s=%.3f"),
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond);
}
else if (!ReadStagedPlayerSpeeds(
StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError)) StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError))
{ {
return false; return false;
@@ -17663,8 +17873,26 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings); const int32 OwnerSeed = ResolveCoreTestWorldSeed(AuthoredSettings);
TStrongObjectPtr<UVoxelSettings> OwnerSurfaceSettings; TStrongObjectPtr<UVoxelSettings> OwnerSurfaceSettings;
TStrongObjectPtr<UVoxelStrateDefinition> OwnerSurfaceDefinition; TStrongObjectPtr<UVoxelStrateDefinition> OwnerSurfaceDefinition;
if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed, TArray<TStrongObjectPtr<UVoxelStrateDefinition>> OwnerPlanDefinitions;
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError)) if (Arguments.bOwnerFourStratePlan)
{
UVoxelSettings* PlanSettings = DuplicateObject<UVoxelSettings>(
AuthoredSettings, GetTransientPackage(), TEXT("VoxelForgeOwnerFourStrateTraceSettings"));
if (PlanSettings == nullptr)
{
OutError = TEXT("Could not duplicate owner settings for the transient four-strate Core trace.");
return false;
}
PlanSettings->Seed = OwnerSeed;
if (!ApplyOwnerFourStratePlan(PlanSettings, OwnerPlanDefinitions, true, OutError))
{
return false;
}
OwnerSurfaceSettings = TStrongObjectPtr<UVoxelSettings>(PlanSettings);
}
else if (!CreateLoggedOwnerSurfaceSettingsCopy(AuthoredSettings, OwnerSeed,
OwnerSurfaceSettings, OwnerSurfaceDefinition, OutError,
Arguments.bCoreWorldStreamTraceOnly))
{ {
return false; return false;
} }
@@ -17822,6 +18050,25 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
OutError = TEXT("Could not create the BuildHost stream-trace pawn."); OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
return false; return false;
} }
FVector StartupTraceSpawnCm = FVector::ZeroVector;
FString StartupTraceSpawnText;
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceSpawnCm="),
StartupTraceSpawnText, false))
{
TArray<FString> Components;
StartupTraceSpawnText.ParseIntoArray(Components, TEXT(","), true);
if (Components.Num() != 3
|| !LexTryParseString(StartupTraceSpawnCm.X, *Components[0])
|| !LexTryParseString(StartupTraceSpawnCm.Y, *Components[1])
|| !LexTryParseString(StartupTraceSpawnCm.Z, *Components[2])
|| !FMath::IsFinite(StartupTraceSpawnCm.X)
|| !FMath::IsFinite(StartupTraceSpawnCm.Y)
|| !FMath::IsFinite(StartupTraceSpawnCm.Z))
{
OutError = TEXT("voxel.StartupTraceSpawnCm must be finite X,Y,Z centimetres.");
return false;
}
}
USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient); USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient);
if (PawnRoot == nullptr) if (PawnRoot == nullptr)
{ {
@@ -17830,7 +18077,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
} }
PlayerPawn->SetRootComponent(PawnRoot); PlayerPawn->SetRootComponent(PawnRoot);
PawnRoot->RegisterComponent(); PawnRoot->RegisterComponent();
PlayerPawn->SetActorLocation(FVector::ZeroVector, false, nullptr, ETeleportType::TeleportPhysics); PlayerPawn->SetActorLocation(StartupTraceSpawnCm, false, nullptr, ETeleportType::TeleportPhysics);
PlayerController->Possess(PlayerPawn); PlayerController->Possess(PlayerPawn);
if (TestWorld->GetFirstPlayerController() != PlayerController if (TestWorld->GetFirstPlayerController() != PlayerController
|| PlayerController->GetPawn() != PlayerPawn) || PlayerController->GetPawn() != PlayerPawn)
@@ -17859,7 +18106,10 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1); : EVoxelForgeCoreMesherRecipe::MarchingCubesV1);
Actor->CoreClipRadius = 3; Actor->CoreClipRadius = 3;
Actor->CoreMaxClipLevel = 8; Actor->CoreMaxClipLevel = 8;
Actor->bCaptureVoxelForgeCoreHashesForValidation = true; // A timing trace must use the production tile path. Core density hashing disables the
// native heightfield band shortcut and measures a different workload; hash validation
// remains enabled in the dedicated parity tests below.
Actor->bCaptureVoxelForgeCoreHashesForValidation = false;
Actor->FinishSpawning(ActorTransform); Actor->FinishSpawning(ActorTransform);
TestWorld->InitializeActorsForPlay(FURL()); TestWorld->InitializeActorsForPlay(FURL());
TestWorld->BeginPlay(); TestWorld->BeginPlay();
@@ -17869,13 +18119,19 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
return false; return false;
} }
Actor->SetActorTickEnabled(true); Actor->SetActorTickEnabled(true);
VoxelForgeStartupTrace::RecordEvent(TEXT("trace_spawn_location"), FString::Printf(
TEXT("\"location_cm\":[%s,%s,%s]"),
*FString::SanitizeFloat(StartupTraceSpawnCm.X),
*FString::SanitizeFloat(StartupTraceSpawnCm.Y),
*FString::SanitizeFloat(StartupTraceSpawnCm.Z)));
auto DesiredTilesLoaded = [](const AVoxelWorld* World) auto DesiredTilesLoaded = [](const AVoxelWorld* World)
{ {
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false; if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
for (const FVoxelTileKey& Key : World->DesiredSorted) for (const FVoxelTileKey& Key : World->DesiredSorted)
{ {
if (!World->LoadedTiles.Contains(Key)) return false; if (!World->LoadedTiles.Contains(Key)
&& !World->CollisionNotRequiredTiles.Contains(Key)) return false;
} }
return true; return true;
}; };
@@ -17916,11 +18172,60 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
TArray<double> SpawnTickMilliseconds; TArray<double> SpawnTickMilliseconds;
TArray<double> WalkTickMilliseconds; TArray<double> WalkTickMilliseconds;
TArray<TPair<double, double>> TickSamples; TArray<TPair<double, double>> TickSamples;
double WorldTickStartedSeconds = 0.0;
double WorldActorPhaseStartedSeconds = 0.0;
double WorldPostActorStartedSeconds = 0.0;
double WorldPreActorMilliseconds = 0.0;
double WorldActorPhaseMilliseconds = 0.0;
double WorldPostActorMilliseconds = 0.0;
const FDelegateHandle WorldTickStartHandle = FWorldDelegates::OnWorldTickStart.AddLambda(
[&](UWorld* World, ELevelTick, float)
{
if (World == TestWorld)
{
WorldTickStartedSeconds = FPlatformTime::Seconds();
}
});
const FDelegateHandle WorldPreActorTickHandle = FWorldDelegates::OnWorldPreActorTick.AddLambda(
[&](UWorld* World, ELevelTick, float)
{
if (World == TestWorld)
{
const double NowSeconds = FPlatformTime::Seconds();
WorldPreActorMilliseconds =
(NowSeconds - WorldTickStartedSeconds) * 1000.0;
WorldActorPhaseStartedSeconds = NowSeconds;
}
});
const FDelegateHandle WorldPostActorTickHandle = FWorldDelegates::OnWorldPostActorTick.AddLambda(
[&](UWorld* World, ELevelTick, float)
{
if (World == TestWorld)
{
const double NowSeconds = FPlatformTime::Seconds();
WorldActorPhaseMilliseconds =
(NowSeconds - WorldActorPhaseStartedSeconds) * 1000.0;
WorldPostActorStartedSeconds = NowSeconds;
}
});
const FDelegateHandle WorldTickEndHandle = FWorldDelegates::OnWorldTickEnd.AddLambda(
[&](UWorld* World, ELevelTick, float)
{
if (World == TestWorld)
{
WorldPostActorMilliseconds =
(FPlatformTime::Seconds() - WorldPostActorStartedSeconds) * 1000.0;
}
});
bool bInitialSetReady = false; bool bInitialSetReady = false;
double InitialSetReadySeconds = -1.0; double InitialSetReadySeconds = -1.0;
while (FPlatformTime::Seconds() - ActorBeginSeconds < RunDurationSeconds) while (FPlatformTime::Seconds() - ActorBeginSeconds < RunDurationSeconds)
{ {
const double TickStartSeconds = FPlatformTime::Seconds(); const double TickStartSeconds = FPlatformTime::Seconds();
const uint64 TickFrame = GFrameCounter;
WorldPreActorMilliseconds = 0.0;
WorldActorPhaseMilliseconds = 0.0;
WorldPostActorMilliseconds = 0.0;
TestWorld->Tick(LEVELTICK_All, 0.016f); TestWorld->Tick(LEVELTICK_All, 0.016f);
const double TickMilliseconds = const double TickMilliseconds =
(FPlatformTime::Seconds() - TickStartSeconds) * 1000.0; (FPlatformTime::Seconds() - TickStartSeconds) * 1000.0;
@@ -17928,6 +18233,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
const double ElapsedSeconds = FPlatformTime::Seconds() - ActorBeginSeconds; const double ElapsedSeconds = FPlatformTime::Seconds() - ActorBeginSeconds;
TickSamples.Emplace(ElapsedSeconds, TickMilliseconds); TickSamples.Emplace(ElapsedSeconds, TickMilliseconds);
const double ReadinessCheckStartSeconds = FPlatformTime::Seconds();
if (!bInitialSetReady && Actor->IsVoxelForgeCoreReady() if (!bInitialSetReady && Actor->IsVoxelForgeCoreReady()
&& Actor->bAllChunksLoaded && Actor->PendingTiles.IsEmpty() && Actor->bAllChunksLoaded && Actor->PendingTiles.IsEmpty()
&& Actor->PendingCollisionCooks.IsEmpty() && Actor->PendingUnload.IsEmpty() && Actor->PendingCollisionCooks.IsEmpty() && Actor->PendingUnload.IsEmpty()
@@ -17942,8 +18248,30 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
Actor->LoadedTiles.Num(), Actor->CollisionDesiredTiles.Num(), Actor->LoadedTiles.Num(), Actor->CollisionDesiredTiles.Num(),
Actor->CollisionReadyTiles.Num())); Actor->CollisionReadyTiles.Num()));
} }
const double ReadinessCheckMilliseconds =
(FPlatformTime::Seconds() - ReadinessCheckStartSeconds) * 1000.0;
const double SleepStartSeconds = FPlatformTime::Seconds();
FPlatformProcess::Sleep(0.016f); FPlatformProcess::Sleep(0.016f);
const double SleepMilliseconds =
(FPlatformTime::Seconds() - SleepStartSeconds) * 1000.0;
VoxelForgeStartupTrace::RecordEvent(TEXT("commandlet_world_tick_frame"), FString::Printf(
TEXT("\"frame\":%llu,\"world_tick_ms\":%s,"
"\"world_pre_actor_ms\":%s,\"world_actor_phase_ms\":%s,"
"\"world_post_actor_ms\":%s,"
"\"readiness_check_ms\":%s,\"sleep_ms\":%s,\"loop_ms\":%s"),
static_cast<unsigned long long>(TickFrame),
*FString::SanitizeFloat(TickMilliseconds),
*FString::SanitizeFloat(WorldPreActorMilliseconds),
*FString::SanitizeFloat(WorldActorPhaseMilliseconds),
*FString::SanitizeFloat(WorldPostActorMilliseconds),
*FString::SanitizeFloat(ReadinessCheckMilliseconds),
*FString::SanitizeFloat(SleepMilliseconds),
*FString::SanitizeFloat((FPlatformTime::Seconds() - TickStartSeconds) * 1000.0)));
} }
FWorldDelegates::OnWorldTickStart.Remove(WorldTickStartHandle);
FWorldDelegates::OnWorldPreActorTick.Remove(WorldPreActorTickHandle);
FWorldDelegates::OnWorldPostActorTick.Remove(WorldPostActorTickHandle);
FWorldDelegates::OnWorldTickEnd.Remove(WorldTickEndHandle);
for (const TPair<double, double>& Sample : TickSamples) for (const TPair<double, double>& Sample : TickSamples)
{ {
@@ -18023,6 +18351,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
const double CarveBrushRadiusCoreVoxels = const double CarveBrushRadiusCoreVoxels =
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm; CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4); const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
FIntVector ColdStartCollisionCenter = FIntVector::ZeroValue;
uint64 ExpectedEditDigest = 0; uint64 ExpectedEditDigest = 0;
std::array<std::uint64_t, 4> InitialAggregates{}; std::array<std::uint64_t, 4> InitialAggregates{};
std::array<std::uint64_t, 4> TravelAggregates{}; std::array<std::uint64_t, 4> TravelAggregates{};
@@ -18208,10 +18537,17 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
BiomeProbeSpanVoxels); BiomeProbeSpanVoxels);
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot)); VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({}); // Match the actor's cold-start fallback camera when the transient world has no pawn.
// Aligner la reference sur la camera de repli de l'acteur sans pawn au demarrage.
const VoxelForgeCore::WorldConfiguration& ReferenceConfiguration =
ReferenceWorld.configuration();
const VoxelForgeCore::Vec3d InitialCameraVoxels = ReferenceConfiguration.viewerStartFound
? ReferenceConfiguration.viewerStartVoxels : VoxelForgeCore::Vec3d{};
const std::vector<VoxelForgeCore::TileKey> Desired =
ReferenceWorld.desiredTiles(InitialCameraVoxels);
if (Desired.empty()) if (Desired.empty())
{ {
OutError = TEXT("Standalone Core returned an empty reference clipmap at the origin."); OutError = TEXT("Standalone Core returned an empty reference clipmap at the cold-start camera.");
return false; return false;
} }
ExpectedInitialTileCount = Desired.size(); ExpectedInitialTileCount = Desired.size();
@@ -18258,7 +18594,23 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
UTF8_TO_TCHAR(CoreError.c_str())); UTF8_TO_TCHAR(CoreError.c_str()));
return false; return false;
} }
ReferenceWorld.retainTileRegions(Desired); // Match the runtime collision probe around the same cold-start viewer tile.
// Reproduire la sonde de collision autour de la meme tuile de depart a froid.
ColdStartCollisionCenter = FIntVector(
FMath::FloorToInt(InitialCameraVoxels.x / 32.0),
FMath::FloorToInt(InitialCameraVoxels.y / 32.0),
FMath::FloorToInt(InitialCameraVoxels.z / 32.0));
std::vector<VoxelForgeCore::TileKey> ColdStartCollisionTiles;
ColdStartCollisionTiles.reserve(static_cast<std::size_t>(2 * CollisionRadius + 1)
* static_cast<std::size_t>(2 * CollisionRadius + 1)
* static_cast<std::size_t>(2 * CollisionRadius + 1));
for (int32 Z = -CollisionRadius; Z <= CollisionRadius; ++Z)
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
ColdStartCollisionTiles.push_back({
{ColdStartCollisionCenter.X + X,
ColdStartCollisionCenter.Y + Y,
ColdStartCollisionCenter.Z + Z}, 0});
if (Arguments.bCoreHybridCliffOnly) if (Arguments.bCoreHybridCliffOnly)
{ {
@@ -18332,6 +18684,47 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1; Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
else else
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1; Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
// Keep collision hashes outside ExpectedInitialDesiredSet; the collision cube is an
// additional runtime request even where it overlaps the visual clipmap.
// Garder les hachages de collision hors de l'ensemble visuel initial, meme s'ils se
// recoupent avec le clipmap.
std::vector<VoxelForgeCore::TileKey> RetainedReferenceTiles = Desired;
RetainedReferenceTiles.insert(RetainedReferenceTiles.end(),
ColdStartCollisionTiles.begin(), ColdStartCollisionTiles.end());
ReferenceWorld.retainTileRegions(RetainedReferenceTiles);
if (!ReferenceWorld.compileTileRegion(ColdStartCollisionTiles, 16, Scheduler, CoreError))
{
OutError = FString::Printf(TEXT("Standalone collision reference compile failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
std::size_t CollisionBodyTiles = 0;
for (const VoxelForgeCore::TileKey& Key : ColdStartCollisionTiles)
{
const std::optional<VoxelForgeCore::TileMesh> Mesh =
ReferenceWorld.generateTile(Key, Options, CoreError);
if (!Mesh)
{
OutError = FString::Printf(TEXT("Standalone collision tile generation failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
FVoxelForgeCoreTileHashRecord Record;
Record.bEmpty = Mesh->indices.empty();
Record.bHasHashes = Mesh->hasDensityHash && Mesh->hasMeshHash
&& Mesh->hasPayloadHash && Mesh->hasTopologyHash;
if (!Record.bEmpty) ++CollisionBodyTiles;
ExpectedHashes.Add(FVoxelTileKey(
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod), Record);
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] standalone_collision_reference=1 center=(%d,%d,%d) tiles=%d body=%llu no_body=%llu"),
ColdStartCollisionCenter.X, ColdStartCollisionCenter.Y, ColdStartCollisionCenter.Z,
static_cast<int32>(ColdStartCollisionTiles.size()),
static_cast<unsigned long long>(CollisionBodyTiles),
static_cast<unsigned long long>(ColdStartCollisionTiles.size() - CollisionBodyTiles));
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows; std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
AggregateRows.reserve(Desired.size()); AggregateRows.reserve(Desired.size());
bool bFoundCarveVertex = false; bool bFoundCarveVertex = false;
@@ -19264,7 +19657,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false; if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
for (const FVoxelTileKey& Key : World->DesiredSorted) for (const FVoxelTileKey& Key : World->DesiredSorted)
{ {
if (!World->LoadedTiles.Contains(Key)) return false; if (!World->LoadedTiles.Contains(Key)
&& !World->CollisionNotRequiredTiles.Contains(Key)) return false;
} }
return true; return true;
}; };
@@ -19367,7 +19761,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y) for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X) for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
{ {
const FVoxelTileKey Key(FIntVector(X, Y, Z), 0); const FVoxelTileKey Key(ColdStartCollisionCenter + FIntVector(X, Y, Z), 0);
ExpectedCollisionSet.Add(Key); ExpectedCollisionSet.Add(Key);
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key); const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
if (Record == nullptr) if (Record == nullptr)
@@ -21361,6 +21755,44 @@ int32 RunBatch(const FString& Params)
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params) int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
{ {
if (FParse::Param(*Params, TEXT("coreassetupgradeaudit")))
{
// Load the untouched BuildHost copies and emit their in-memory conversions; never save a package.
// Charge les copies BuildHost intactes et affiche les conversions en memoire, sans sauver de package.
UVoxelSettings* SettingsAsset = LoadObject<UVoxelSettings>(nullptr,
TEXT("/Game/VoxelForge/DA_Settings.DA_Settings"));
if (SettingsAsset == nullptr)
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing DA_Settings copy"));
return 2;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_gap_chunks=%d new_gap_m=%.6f layout_chunk_m=%.6f"),
*SettingsAsset->GetPathName(), SettingsAsset->InterStrateGapChunks,
SettingsAsset->InterStrateGapMeters, SettingsAsset->StrateLayoutChunkSizeMeters);
const TCHAR* StratePaths[] = {
TEXT("/Game/VoxelForge/DA_Strate1.DA_Strate1"),
TEXT("/Game/VoxelForge/DA_Strate2.DA_Strate2"),
TEXT("/Game/VoxelForge/DA_Strate3.DA_Strate3"),
TEXT("/Game/VoxelForge/DA_Strate4.DA_Strate4")
};
for (const TCHAR* Path : StratePaths)
{
UVoxelStrateDefinition* Definition = LoadObject<UVoxelStrateDefinition>(nullptr, Path);
if (Definition == nullptr)
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeAssetMigrationAudit] missing strate copy %s"), Path);
return 2;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeAssetMigrationAudit] asset=%s old_height_chunks=%d new_height_m=%.6f old_transition_chunks=%d new_transition_m=%.6f"),
*Definition->GetPathName(), Definition->StrateHeightInChunks,
Definition->StrateHeightMeters, Definition->TransitionBlendChunks,
Definition->TransitionWidthMeters);
}
return 0;
}
if (FParse::Param(*Params, TEXT("step2f"))) if (FParse::Param(*Params, TEXT("step2f")))
{ {
return RunStep2f(Params); return RunStep2f(Params);
+31 -3
View File
@@ -13,6 +13,8 @@ COMPILER="clang++-19"
VARIANT="clang19" VARIANT="clang19"
FP_CONTRACT="off" FP_CONTRACT="off"
JOBS=12 JOBS=12
SANITIZERS=""
EXCLUDE_UNSIGNED_OVERFLOW=0
while (($#)); do while (($#)); do
case "$1" in case "$1" in
@@ -20,6 +22,8 @@ while (($#)); do
--variant=*) VARIANT="${1#*=}" ;; --variant=*) VARIANT="${1#*=}" ;;
--fp-contract=*) FP_CONTRACT="${1#*=}" ;; --fp-contract=*) FP_CONTRACT="${1#*=}" ;;
--jobs=*) JOBS="${1#*=}" ;; --jobs=*) JOBS="${1#*=}" ;;
--sanitize=*) SANITIZERS="${1#*=}" ;;
--exclude-unsigned-integer-overflow) EXCLUDE_UNSIGNED_OVERFLOW=1 ;;
*) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;; *) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;;
esac esac
shift shift
@@ -60,14 +64,32 @@ INCLUDES=(
"-I$ROOT/Source/VoxelForge/Public" "-I$ROOT/Source/VoxelForge/Public"
"-I$ROOT/Source/VoxelForge/Private" "-I$ROOT/Source/VoxelForge/Private"
) )
OPT_LEVEL=-O2
LINK_FLAGS=()
if [[ -n "$SANITIZERS" ]]; then
OPT_LEVEL=-O1
LINK_FLAGS+=("-fsanitize=$SANITIZERS" -fno-sanitize-recover=undefined)
if ((EXCLUDE_UNSIGNED_OVERFLOW)); then
LINK_FLAGS+=(-fno-sanitize=unsigned-integer-overflow)
fi
fi
FLAGS=( FLAGS=(
-std=c++20 -O2 -mavx2 -mfma -fno-fast-math -std=c++20 "$OPT_LEVEL" -mavx2 -mfma -fno-fast-math
-fexceptions -pthread "${INCLUDES[@]}" -fexceptions -pthread "${INCLUDES[@]}"
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" "-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\""
) )
FLAGS+=("${LINK_FLAGS[@]}")
if [[ "$FP_CONTRACT" != "default" ]]; then if [[ "$FP_CONTRACT" != "default" ]]; then
FLAGS+=("-ffp-contract=$FP_CONTRACT") FLAGS+=("-ffp-contract=$FP_CONTRACT")
fi fi
if [[ "${VOXELFORGE_PROFILE_LEGACY_TERMS:-0}" == "1" ]]; then
FLAGS+=(-DVF_L76_PROFILE_LEGACY_TERMS=1)
fi
if [[ "${VOXELFORGE_PROFILE_L77:-0}" == "1" ]]; then
FLAGS+=(-DVF_L77_PROFILE=1 -g)
LINK_FLAGS+=(-no-pie)
fi
if [[ "$($COMPILER --version | head -n 1)" == *clang* ]]; then if [[ "$($COMPILER --version | head -n 1)" == *clang* ]]; then
FLAGS+=(-Wno-c++11-narrowing) FLAGS+=(-Wno-c++11-narrowing)
else else
@@ -124,6 +146,11 @@ done < "$ROOT/Core/core_sources.txt"
headless_object="$BUILD_DIR/objects/StreamBenchmarkHeadless.o" headless_object="$BUILD_DIR/objects/StreamBenchmarkHeadless.o"
compile_background "$ROOT/Source/StreamBenchmarkHeadless.cpp" "$headless_object" compile_background "$ROOT/Source/StreamBenchmarkHeadless.cpp" "$headless_object"
LEGACY_OBJECTS+=("$headless_object") LEGACY_OBJECTS+=("$headless_object")
if [[ "${VOXELFORGE_PROFILE_L77:-0}" == "1" ]]; then
l77_profiler_object="$BUILD_DIR/objects/Linux77Profiler.o"
compile_background "$ROOT/Source/Linux77Profiler.cpp" "$l77_profiler_object"
LEGACY_OBJECTS+=("$l77_profiler_object")
fi
for pid in "${PIDS[@]}"; do wait "$pid"; done for pid in "${PIDS[@]}"; do wait "$pid"; done
PIDS=() PIDS=()
@@ -131,7 +158,7 @@ PIDS=()
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a" CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}" "$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT" OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT"
"$COMPILER" -pthread "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT" "$COMPILER" -pthread "${LINK_FLAGS[@]}" "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT" CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT"
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \ "$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" -I"$ROOT/Source" \ "-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" -I"$ROOT/Source" \
@@ -145,4 +172,5 @@ if [[ "$POST_BUILD_STAMP" != "$SOURCE_STAMP" ]]; then
fi fi
printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT" printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT"
printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_OUTPUT" printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_OUTPUT"
printf 'Compiler: %s | variant: %s | fp-contract: %s | jobs: %s\n' "$COMPILER" "$VARIANT" "$FP_CONTRACT" "$JOBS" printf 'Compiler: %s | variant: %s | fp-contract: %s | sanitizers: %s | jobs: %s\n' \
"$COMPILER" "$VARIANT" "$FP_CONTRACT" "${SANITIZERS:-off}" "$JOBS"
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <AssetRegistry/AssetData.h>
#endif
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <AssetRegistry/AssetRegistryModule.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Async/ParallelFor.h>
#endif
+9
View File
@@ -1,5 +1,10 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <CoreMinimal.h>
#else
// Deliberately small UE-style surface used by the copied VoxelForge CPU sources. // Deliberately small UE-style surface used by the copied VoxelForge CPU sources.
// This is a value/container compatibility layer; it does not start or link Unreal. // This is a value/container compatibility layer; it does not start or link Unreal.
#include <algorithm> #include <algorithm>
@@ -45,6 +50,9 @@
#define GENERATED_UCLASS_BODY() #define GENERATED_UCLASS_BODY()
#define VOXELFORGE_API #define VOXELFORGE_API
#define FORCEINLINE inline #define FORCEINLINE inline
#ifdef WITH_EDITOR
#undef WITH_EDITOR
#endif
#define WITH_EDITOR 0 #define WITH_EDITOR 0
#define TRACE_CPUPROFILER_EVENT_SCOPE(...) #define TRACE_CPUPROFILER_EVENT_SCOPE(...)
#define PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS #define PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS
@@ -705,3 +713,4 @@ struct FParse
struct FCrc { static uint32 MemCrc32(const void*Data,int32 Length,uint32 Crc=0){const auto*P=static_cast<const uint8*>(Data);uint32 V=~Crc;for(int32 I=0;I<Length;++I){V^=P[I];for(int J=0;J<8;++J)V=(V>>1)^(0xEDB88320u&static_cast<uint32>(-static_cast<int32>(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast<int32>(std::strlen(S)),Crc);} }; struct FCrc { static uint32 MemCrc32(const void*Data,int32 Length,uint32 Crc=0){const auto*P=static_cast<const uint8*>(Data);uint32 V=~Crc;for(int32 I=0;I<Length;++I){V^=P[I];for(int J=0;J<8;++J)V=(V>>1)^(0xEDB88320u&static_cast<uint32>(-static_cast<int32>(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast<int32>(std::strlen(S)),Crc);} };
template<class T> using TAtomic = std::atomic<T>; template<class T> using TAtomic = std::atomic<T>;
class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomic<int32>Value{0};}; class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomic<int32>Value{0};};
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Dom/JsonObject.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Dom/JsonValue.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Engine/DataAsset.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <GameplayTagContainer.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/CriticalSection.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/Event.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/FileManager.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/IConsoleManager.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/PlatformProcess.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <HAL/PlatformTime.h>
#endif
+6
View File
@@ -1,5 +1,10 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Json.h>
#else
#include <charconv> #include <charconv>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
@@ -162,3 +167,4 @@ namespace VFJson
return Parser(std::move(Text)).Parse(Out, Error); return Parser(std::move(Text)).Parse(Out, Error);
} }
} }
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Math/RandomStream.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/CommandLine.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/Crc.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/FileHelper.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/Parse.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/Paths.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/ScopeLock.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/ScopeRWLock.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Misc/SecureHash.h>
#endif
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Modules/ModuleManager.h>
#endif
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <ProfilingDebugging/CpuProfilerTrace.h>
#endif
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Serialization/CustomVersion.h>
#endif
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Serialization/JsonSerializer.h>
#endif
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Serialization/JsonWriter.h>
#endif
+6
View File
@@ -1,4 +1,9 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Stats/Stats.h>
#else
#define STATCAT_Advanced 0 #define STATCAT_Advanced 0
#define DECLARE_STATS_GROUP(Name,Group,Category) constexpr int32 Group=0 #define DECLARE_STATS_GROUP(Name,Group,Category) constexpr int32 Group=0
#define DECLARE_DWORD_COUNTER_STAT_EXTERN(Name,Stat,Group,Api) constexpr int32 Stat=0 #define DECLARE_DWORD_COUNTER_STAT_EXTERN(Name,Stat,Group,Api) constexpr int32 Stat=0
@@ -8,3 +13,4 @@
#define SET_DWORD_STAT(Stat,Value) ((void)0) #define SET_DWORD_STAT(Stat,Value) ((void)0)
#define SCOPE_CYCLE_COUNTER(Stat) ((void)0) #define SCOPE_CYCLE_COUNTER(Stat) ((void)0)
#define QUICK_SCOPE_CYCLE_COUNTER(Stat) ((void)0) #define QUICK_SCOPE_CYCLE_COUNTER(Stat) ((void)0)
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Templates/SubclassOf.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <Templates/UniquePtr.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <UObject/EnumProperty.h>
#endif
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <UObject/FieldIterator.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <UObject/Object.h>
#endif
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <UObject/UObjectGlobals.h>
#endif
+5
View File
@@ -1 +1,6 @@
#pragma once #pragma once
#if defined(VFCORE_COMPAT_FORWARD_HEADERS) && !defined(VFCORE_USE_COMPAT) \
&& (defined(__clang__) || defined(__GNUC__))
#include_next <UObject/UnrealType.h>
#endif
@@ -3,6 +3,7 @@
#include "VoxelForgeCore/CpuCycleCounter.h" #include "VoxelForgeCore/CpuCycleCounter.h"
#include <array> #include <array>
#include <chrono>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
@@ -26,9 +27,12 @@ namespace VoxelForgeCore::floating_island_detail
static constexpr std::uint64_t SampleMask=255; 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)> 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)> 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)> 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)> 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)> 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::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> scopeSamples{};
std::uint64_t traceSampledMisses=0,traceSeenBefore=0,traceEvictions=0, std::uint64_t traceSampledMisses=0,traceSeenBefore=0,traceEvictions=0,
traceReusedEvictions=0; traceReusedEvictions=0;
@@ -118,7 +122,11 @@ namespace VoxelForgeCore::floating_island_detail
&FIslandFieldAttributionInstrumentation::SampleMask; &FIslandFieldAttributionInstrumentation::SampleMask;
sampled=(((calls+phase)&FIslandFieldAttributionInstrumentation::SampleMask)==0); sampled=(((calls+phase)&FIslandFieldAttributionInstrumentation::SampleMask)==0);
} }
if(sampled)start=VoxelForgeCoreShared::ReadCpuCycles(); if(sampled)
{
start=VoxelForgeCoreShared::ReadCpuCycles();
wallStart=std::chrono::steady_clock::now();
}
} }
~FScopedIslandAttributionTimer(){Stop();} ~FScopedIslandAttributionTimer(){Stop();}
std::uint64_t Stop() noexcept 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 raw=VoxelForgeCoreShared::ReadCpuCycles()-start;
const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead(); const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead();
const std::uint64_t cycles=raw>overhead?raw-overhead:0; 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->exclusiveCycles[bucketIndex]+=cycles;
metrics->exclusiveNanoseconds[bucketIndex]+=elapsed>0
?static_cast<std::uint64_t>(elapsed):0;
++metrics->scopeSamples[bucketIndex]; ++metrics->scopeSamples[bucketIndex];
if(bucketIndex==static_cast<std::size_t>(EIslandAttributionBucket::Field)) if(bucketIndex==static_cast<std::size_t>(EIslandAttributionBucket::Field))
{ {
const std::size_t callerIndex=static_cast<std::size_t>(ActiveIslandFieldCaller()); const std::size_t callerIndex=static_cast<std::size_t>(ActiveIslandFieldCaller());
metrics->fieldCycles[callerIndex]+=cycles; metrics->fieldCycles[callerIndex]+=cycles;
metrics->fieldNanoseconds[callerIndex]+=elapsed>0
?static_cast<std::uint64_t>(elapsed):0;
++metrics->sampledFieldCalls[callerIndex]; ++metrics->sampledFieldCalls[callerIndex];
} }
return cycles; return cycles;
@@ -141,6 +155,7 @@ namespace VoxelForgeCore::floating_island_detail
FIslandFieldAttributionInstrumentation* metrics=nullptr; FIslandFieldAttributionInstrumentation* metrics=nullptr;
std::size_t bucketIndex=0; std::size_t bucketIndex=0;
std::uint64_t start=0; std::uint64_t start=0;
std::chrono::steady_clock::time_point wallStart{};
bool sampled=false; bool sampled=false;
}; };
@@ -160,6 +175,7 @@ namespace VoxelForgeCore::floating_island_detail
sampledCaller=caller; sampledCaller=caller;
sampled=true; sampled=true;
start=VoxelForgeCoreShared::ReadCpuCycles(); start=VoxelForgeCoreShared::ReadCpuCycles();
wallStart=std::chrono::steady_clock::now();
++attribution->sampledNativeCalls[callerIndex]; ++attribution->sampledNativeCalls[callerIndex];
} }
~FScopedIslandAttributionSample() ~FScopedIslandAttributionSample()
@@ -168,12 +184,17 @@ namespace VoxelForgeCore::floating_island_detail
const std::uint64_t raw=VoxelForgeCoreShared::ReadCpuCycles()-start; const std::uint64_t raw=VoxelForgeCoreShared::ReadCpuCycles()-start;
const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead(); const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead();
const std::uint64_t duration=raw>overhead?raw-overhead:0; 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); const std::size_t callerIndex=static_cast<std::size_t>(sampledCaller);
sampledAttribution->nativeCycles[callerIndex]+=duration; sampledAttribution->nativeCycles[callerIndex]+=duration;
sampledAttribution->nativeNanoseconds[callerIndex]+=elapsed>0
?static_cast<std::uint64_t>(elapsed):0;
} }
FIslandFieldAttributionInstrumentation* sampledAttribution=nullptr; FIslandFieldAttributionInstrumentation* sampledAttribution=nullptr;
EIslandFieldCaller sampledCaller=EIslandFieldCaller::Other; EIslandFieldCaller sampledCaller=EIslandFieldCaller::Other;
std::uint64_t start=0; std::uint64_t start=0;
std::chrono::steady_clock::time_point wallStart{};
bool sampled=false; 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 <utility>
#include <vector> #include <vector>
#include "VoxelForgeCore/Linux77Profiler.h"
// UBT supplies this export/import macro for the runtime module. The standalone target has no // UBT supplies this export/import macro for the runtime module. The standalone target has no
// module boundary, so leave it empty there. UBT fournit la macro; le viewer autonome reste neutre. // module boundary, so leave it empty there. UBT fournit la macro; le viewer autonome reste neutre.
#ifndef VOXELFORGECORE_API #ifndef VOXELFORGECORE_API
@@ -131,9 +133,86 @@ namespace VoxelForgeCore
std::uint64_t mixedBoxes = 0; std::uint64_t mixedBoxes = 0;
std::uint64_t unknownBoxes = 0; std::uint64_t unknownBoxes = 0;
std::uint64_t fieldCalls = 0; std::uint64_t fieldCalls = 0;
// Opt-in composer diagnostics. These stay zero during ordinary generation.
bool passageComposerProfiled = false;
std::uint64_t passageTileFeatureCount = 0;
std::uint64_t passageTileRouteSegmentCandidates = 0;
std::uint64_t passageTileMouthSegmentCandidates = 0;
std::uint64_t resolvedBoundaryBandQueries = 0;
std::uint64_t passageComposerQueries = 0;
std::uint64_t passageIterations = 0;
std::uint64_t passageTileBoundChecks = 0;
std::uint64_t passageTileBoundRejects = 0;
std::uint64_t passageTileBoundSkippedIterations = 0;
std::uint64_t passageUpperMouthFilterChecks = 0;
std::uint64_t passageColumnCandidates = 0;
std::uint64_t passageColumnDistanceCalls = 0;
std::uint64_t passageNegativeBoundChecks = 0;
std::uint64_t passageNegativeBoundRejects = 0;
std::uint64_t passagePositiveBoundChecks = 0;
std::uint64_t passagePositiveBoundRejects = 0;
std::uint64_t passageCarveSegmentVisits = 0;
std::uint64_t passageSmoothCarveJoins = 0;
std::uint64_t passageSupportSegmentVisits = 0;
std::uint64_t passageSmoothSupportJoins = 0;
std::uint64_t passageSupportCapsuleChecks = 0;
std::uint64_t passageLaterBoreChecks = 0;
std::uint64_t passageOtherCapsuleChecks = 0;
std::uint64_t passageMouthSupportCalls = 0;
std::uint64_t passageMouthRouteSegmentChecks = 0;
std::uint64_t passageReliefFieldCalls = 0;
std::uint64_t passageSmoothstepEvaluations = 0;
// L99 tile-local swept-tube base and detail passes. These are populated only
// by the opt-in passage composer profile, except the mesh composition itself.
std::uint64_t passageSweepRouteSegmentCandidates = 0;
std::uint64_t passageSweepSegmentSamplePairs = 0;
std::uint64_t passageSweepOutsideInfluencePairs = 0;
std::uint64_t passageSweepMinimumUpdates = 0;
std::uint64_t passageSweepBaseInfluenceSamples = 0;
std::uint64_t passageSweepDetailShellSamples = 0;
std::uint64_t passageSweepGridSamples = 0;
std::uint64_t passageSweepNoInfluenceGridSamples = 0;
std::uint64_t passageSweepLookups = 0;
std::uint64_t passageSweepCacheHits = 0;
std::uint64_t passageSweepBaseInfluenceHits = 0;
std::uint64_t passageSweepOutOfInfluenceHits = 0;
std::uint64_t passageSweepMissingSamples = 0;
std::array<std::uint64_t,12> passageSweepMissingLookupsByCaller{};
std::array<std::uint64_t,12> passageSweepExactRouteFallbacksByCaller{};
std::array<std::uint64_t,12> passageSweepFeatureBoundSkipsByCaller{};
std::array<std::uint64_t,12> passageSweepTileFreeSkipsByCaller{};
std::array<std::uint64_t,12> passageSweepVerticalBoundSkipsByCaller{};
std::array<std::uint64_t,12> passageSweepNegativeReachSkipsByCaller{};
std::array<std::uint64_t,12> passageSweepPositiveReachSkipsByCaller{};
std::uint64_t floatFieldCalls = 0; std::uint64_t floatFieldCalls = 0;
std::uint64_t latticeFieldCalls = 0; std::uint64_t latticeFieldCalls = 0;
std::uint64_t unclampedFieldCalls = 0; std::uint64_t unclampedFieldCalls = 0;
std::uint64_t densityPointCalls = 0;
std::uint64_t densityFloatPointCalls = 0;
std::uint64_t densityLatticePointCalls = 0;
std::uint64_t densityUnclampedPointCalls = 0;
std::array<std::uint64_t,12> densityOffGridFloatByCaller{};
std::array<std::uint64_t,12> densityOffGridUnclampedByCaller{};
std::array<std::uint64_t,12> densityOffGridDetailByCaller{};
std::uint64_t densityX8PointCalls = 0;
std::uint64_t densityFieldEvalNanoseconds = 0;
std::uint64_t densityFieldEvalSamplePoints = 0;
std::uint64_t legacyFieldEvalCalls = 0;
std::array<std::uint64_t,9> legacyFieldTermCalls{};
std::array<std::uint64_t,9> legacyFieldTermNanoseconds{};
std::array<std::uint64_t,9> legacyFieldTermTimedCalls{};
// Linux77-only mutually-exclusive route counts, flattened as
// (stage * route_count + route) * 2 + {calls,points}.
std::array<std::uint64_t,
VoxelForgeCore::Linux77Profiler::StageCount
*VoxelForgeCore::Linux77Profiler::RouteCount*2> linux77DispatchCounters{};
std::uint64_t linux77GridInteriorSamples = 0;
std::uint64_t linux77GridHaloSamples = 0;
std::uint64_t legacyPrimitiveCandidateVisits = 0;
std::uint64_t legacyPrimitiveDistanceEvaluations = 0;
std::uint64_t legacySmoothMinCalls = 0;
std::uint64_t legacySmoothMinNanoseconds = 0;
std::uint64_t legacySmoothMinTimedCalls = 0;
std::uint64_t columnCacheHits = 0; std::uint64_t columnCacheHits = 0;
std::uint64_t columnCacheMisses = 0; std::uint64_t columnCacheMisses = 0;
std::uint64_t columnCacheConflictMisses = 0; std::uint64_t columnCacheConflictMisses = 0;
@@ -161,6 +240,7 @@ namespace VoxelForgeCore
double classificationExclusiveMilliseconds = 0.0; double classificationExclusiveMilliseconds = 0.0;
double marchingCubesExclusiveMilliseconds = 0.0; double marchingCubesExclusiveMilliseconds = 0.0;
double marchingCubesDensityGridMilliseconds = 0.0; double marchingCubesDensityGridMilliseconds = 0.0;
std::uint32_t marchingCubesGridFillWorkers = 1;
double marchingCubesCellClassificationMilliseconds = 0.0; double marchingCubesCellClassificationMilliseconds = 0.0;
double marchingCubesGradientNormalsMilliseconds = 0.0; double marchingCubesGradientNormalsMilliseconds = 0.0;
double marchingCubesEdgeInterpolationMilliseconds = 0.0; double marchingCubesEdgeInterpolationMilliseconds = 0.0;
@@ -184,6 +264,8 @@ namespace VoxelForgeCore
std::uint64_t marchingCubesSkirtEdgeCandidates = 0; std::uint64_t marchingCubesSkirtEdgeCandidates = 0;
std::uint64_t marchingCubesSkirtBoundaryCandidates = 0; std::uint64_t marchingCubesSkirtBoundaryCandidates = 0;
std::uint64_t marchingCubesSkirtDensityProbes = 0; std::uint64_t marchingCubesSkirtDensityProbes = 0;
std::array<std::uint64_t,5> marchingCubesSkirtDensityProbesByCaller{};
std::array<std::uint64_t,5> marchingCubesSkirtDensityOffGridProbesByCaller{};
double groundRefinementExclusiveMilliseconds = 0.0; double groundRefinementExclusiveMilliseconds = 0.0;
double capRefinementExclusiveMilliseconds = 0.0; double capRefinementExclusiveMilliseconds = 0.0;
double meshAssemblyMilliseconds = 0.0; double meshAssemblyMilliseconds = 0.0;
@@ -220,6 +302,9 @@ namespace VoxelForgeCore
std::uint32_t farHorizonVersion = 0; std::uint32_t farHorizonVersion = 0;
bool farShellRepairSkipped = false; bool farShellRepairSkipped = false;
bool tileWasClassified = false; bool tileWasClassified = false;
std::uint8_t tileProofVetoReason = 0;
bool tileProofSafe = false;
bool tileLegacyBlockProofRelevant = false;
bool meshingSkipped = false; bool meshingSkipped = false;
bool featureRefinedResolution = false; bool featureRefinedResolution = false;
bool heightfieldBandLimited = false; bool heightfieldBandLimited = false;
@@ -400,7 +485,10 @@ namespace VoxelForgeCore
// version 169 sank every connector 2 m; it left four FI route support probes low. // version 169 sank every connector 2 m; it left four FI route support probes low.
// version 170 sinks only connectors touching the anchored root by 1 m. // version 170 sinks only connectors touching the anchored root by 1 m.
// Version 208 keeps the crater tread scoped to style-zero FI mouths. // Version 208 keeps the crater tread scoped to style-zero FI mouths.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 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 = 216;
enum class FieldChannel : std::uint32_t enum class FieldChannel : std::uint32_t
{ {
@@ -1004,13 +1092,11 @@ namespace VoxelForgeCore
&&stratum.recipeVersion==1; &&stratum.recipeVersion==1;
} }
inline constexpr std::uint32_t NativeLatticeFieldRecipeVersion=2;
inline bool IsNativeLatticeFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept inline bool IsNativeLatticeFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
{ {
return (stratum.generatorKind=="Maze"||stratum.generatorKind=="VerticalShafts") return (stratum.generatorKind=="Maze"||stratum.generatorKind=="VerticalShafts")
&&stratum.recipeId=="Core.Lattice.PrimitiveField" &&stratum.recipeId=="Core.Lattice.PrimitiveField"
&&stratum.recipeVersion==NativeLatticeFieldRecipeVersion; &&stratum.recipeVersion==1;
} }
inline bool HasNativeGraphField(const ResolvedLayoutStratumV2& stratum) noexcept inline bool HasNativeGraphField(const ResolvedLayoutStratumV2& stratum) noexcept
@@ -1405,6 +1491,7 @@ namespace VoxelForgeCore
static void setThreadColumnInterpolationStrideSubvoxels(std::int64_t stride) noexcept; static void setThreadColumnInterpolationStrideSubvoxels(std::int64_t stride) noexcept;
static std::int64_t threadColumnInterpolationStrideSubvoxels() noexcept; static std::int64_t threadColumnInterpolationStrideSubvoxels() noexcept;
static void setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept; static void setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept;
static bool threadColumnCacheLegacyDirectMapped() noexcept;
static const HeightfieldColumnCacheStats& threadColumnCacheStats() noexcept; static const HeightfieldColumnCacheStats& threadColumnCacheStats() noexcept;
FieldInterval interval(std::int64_t minimumXSubvoxels, FieldInterval interval(std::int64_t minimumXSubvoxels,
std::int64_t minimumYSubvoxels, std::int64_t minimumYSubvoxels,
@@ -1726,6 +1813,9 @@ namespace VoxelForgeCore
std::int32_t bandMinZVoxel = -2147483647 - 1; std::int32_t bandMinZVoxel = -2147483647 - 1;
std::int32_t bandMaxZVoxel = 2147483647; std::int32_t bandMaxZVoxel = 2147483647;
bool hashDensity = false; bool hashDensity = false;
// Falsifier mode: retain mesh/topology hashes while permitting proven empty tiles
// to skip their density lattice. The density hash of such a skipped tile is empty.
bool allowUniformSkipsWhenHashing = false;
bool captureDensitySamples = false; bool captureDensitySamples = false;
// Diagnostic only; materialize the full density lattice even when block proofs // Diagnostic only; materialize the full density lattice even when block proofs
// establish that the tile has no surface. // establish that the tile has no surface.
@@ -1744,10 +1834,22 @@ namespace VoxelForgeCore
// Separate cold pass only; samples the MC inner stages without changing output. // Separate cold pass only; samples the MC inner stages without changing output.
// Passe froide separee seulement; mesure les etapes internes MC sans changer la sortie. // Passe froide separee seulement; mesure les etapes internes MC sans changer la sortie.
bool profileMesherStages = false; bool profileMesherStages = false;
// Research-only: count passage-composer work per generated tile.
bool profilePassageComposer = false;
// Research-only field comparison. A nonnegative value clamps the source density
// entering passage composition to at least -N voxel units for this tile.
// A negative value leaves the process-level diagnostic override in control.
double diagnosticPassageSourceFloorVoxels = -1.0;
// Research-only field comparison. Zero keeps the unbounded passage carve;
// a positive value bounds its signed-distance reach in metres. A negative
// value leaves the process-level L93 setting in control.
double diagnosticPassageCarveReachMetres = -1.0;
// Research-only: collect tile-drain kernel counters and floating-island cache metrics. // 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. // Inactive on ordinary generation so the hot path only pays the option check.
bool instrumentTileDrain = false; bool instrumentTileDrain = false;
bool exactMidpointDensityMemo = false; // Linux headless measurement only. Compiled out of ordinary builds.
bool linux77Profile = false;
bool exactMidpointDensityMemo = true;
bool activeLaneBatching = false; bool activeLaneBatching = false;
std::uint32_t proofBlockCells = 8; std::uint32_t proofBlockCells = 8;
// Diagnostic only; fills and checks every lattice sample in blocks certified uniform. // Diagnostic only; fills and checks every lattice sample in blocks certified uniform.
@@ -1759,6 +1861,10 @@ namespace VoxelForgeCore
// Diagnostic/performance control; memoizes exact MC lattice gradients at LOD0. // Diagnostic/performance control; memoizes exact MC lattice gradients at LOD0.
// Controle de mesure/performance; memorise les gradients MC exacts au LOD0. // Controle de mesure/performance; memorise les gradients MC exacts au LOD0.
bool memoizeMcGradients = true; bool memoizeMcGradients = true;
// Optional host-owned executor for predicted-heavy density-grid fills.
// Hosts leave this null for ordinary tiles and set a small limit for heavy tiles.
const JobScheduler* gridFillScheduler = nullptr;
std::uint32_t gridFillWorkerLimit = 1;
// LOD8-only planar horizon interior grid; zero follows the MC step, 128 restores S9. // LOD8-only planar horizon interior grid; zero follows the MC step, 128 restores S9.
// Grille interieure LOD8 seulement; zero suit la maille MC, 128 restaure S9. // Grille interieure LOD8 seulement; zero suit la maille MC, 128 restaure S9.
std::int32_t lod8HorizonInteriorStepVoxels = 0; std::int32_t lod8HorizonInteriorStepVoxels = 0;
@@ -1814,7 +1920,7 @@ namespace VoxelForgeCore
bool analyticOnly = true, bool analyticOnly = true,
bool diagnosticIncludeBaseCoveredTiles = false); bool diagnosticIncludeBaseCoveredTiles = false);
void retainTileRegions(std::span<const TileKey> desiredTiles); void retainTileRegions(std::span<const TileKey> desiredTiles);
void buildFarWorld(); bool buildFarWorld();
bool isCompiled() const noexcept; bool isCompiled() const noexcept;
bool hasFarWorld() const noexcept; bool hasFarWorld() const noexcept;
ShapeBuildStats buildStats() const; ShapeBuildStats buildStats() const;
@@ -1836,6 +1942,8 @@ namespace VoxelForgeCore
std::string densityDispatchPathAt(Vec3d voxelPosition) const; std::string densityDispatchPathAt(Vec3d voxelPosition) const;
// Analytic source distance without the occupancy-preserving near-zero clamp or packed proxy. // Analytic source distance without the occupancy-preserving near-zero clamp or packed proxy.
float unclampedDensityAt(Vec3d voxelPosition) const; float unclampedDensityAt(Vec3d voxelPosition) const;
// Research-only source field entering passage composition, with passage carving omitted.
float passageSourceDensityAt(Vec3d voxelPosition) const;
DetailFieldSample detailSampleAt(Vec3d voxelPosition) const; DetailFieldSample detailSampleAt(Vec3d voxelPosition) const;
float densityAtLattice(Vec3i voxelPosition) const; float densityAtLattice(Vec3i voxelPosition) const;
bool isSolidAt(Vec3d voxelPosition) const; bool isSolidAt(Vec3d voxelPosition) const;
@@ -1845,6 +1953,9 @@ namespace VoxelForgeCore
std::vector<TileKey> desiredTiles(Vec3d cameraPositionVoxels) const; std::vector<TileKey> desiredTiles(Vec3d cameraPositionVoxels) const;
static std::vector<TileKey> prioritizeTiles(std::span<const TileKey> tiles, static std::vector<TileKey> prioritizeTiles(std::span<const TileKey> tiles,
Vec3i centerChunk); Vec3i centerChunk);
// Count expanded resolved-passage route bounds intersecting the full tile bounds.
// Hosts use this stable estimate to prioritize and split expensive near tiles.
std::uint32_t passageOverlapScore(const TileKey& key) const noexcept;
std::optional<TileMesh> generateTile(const TileKey& key, std::optional<TileMesh> generateTile(const TileKey& key,
const TileGenerationOptions& options, const TileGenerationOptions& options,
std::string& error) const; std::string& error) const;
@@ -1,5 +1,9 @@
#pragma once #pragma once
// Core implementation TUs opt into the standalone Unreal-shaped compatibility headers. UE's
// Linux-generated module TUs leave this unset so those same header names forward to the engine.
#define VFCORE_USE_COMPAT 1
// Every standard or platform header used by the copied implementation must be included before // Every standard or platform header used by the copied implementation must be included before
// its wrapper opens vfcore::detail. This prevents system declarations from being nested in the // its wrapper opens vfcore::detail. This prevents system declarations from being nested in the
// private namespace when the implementation includes its Unreal-shaped compatibility headers. // private namespace when the implementation includes its Unreal-shaped compatibility headers.
+16
View File
@@ -154,6 +154,8 @@ inline bool ContainsFarTilePoint(const FVoxelTileKey& Key,const FVector& P)
inline bool FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center, inline bool FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center,
const FVector& Direction,double PreferredRadius,double& OutRadius) const FVector& Direction,double PreferredRadius,double& OutRadius)
{ {
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
EPassageDensityQueryCaller::FarShellSurfaceQuery);
const double SearchLength=PreferredRadius*2.0+32.0; const double SearchLength=PreferredRadius*2.0+32.0;
const double AcceptBand=std::max(16.0,PreferredRadius*0.75); const double AcceptBand=std::max(16.0,PreferredRadius*0.75);
constexpr double Stride=8.0; constexpr double Stride=8.0;
@@ -227,6 +229,8 @@ inline bool FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,con
inline bool FindFarConnectorRadiusNearNominal(const FStandaloneShapeWorld& Shape,const FVector& Center, inline bool FindFarConnectorRadiusNearNominal(const FStandaloneShapeWorld& Shape,const FVector& Center,
const FVector& Direction,double NominalRadius,double& OutRadius,double& OutUncertainty) const FVector& Direction,double NominalRadius,double& OutRadius,double& OutUncertainty)
{ {
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
EPassageDensityQueryCaller::FarShellSurfaceQuery);
const double MatchTolerance=std::max(8.0,NominalRadius*0.35); const double MatchTolerance=std::max(8.0,NominalRadius*0.35);
const double HalfWidth=std::min(16.0,MatchTolerance*0.75); const double HalfWidth=std::min(16.0,MatchTolerance*0.75);
const double Lo=std::max(0.0,NominalRadius-HalfWidth),Hi=NominalRadius+HalfWidth; const double Lo=std::max(0.0,NominalRadius-HalfWidth),Hi=NominalRadius+HalfWidth;
@@ -245,6 +249,8 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
int32 Step,int32 Cells,FVoxelMeshData& Mesh) int32 Step,int32 Cells,FVoxelMeshData& Mesh)
{ {
if(Key.Level<6||Step<=0)return; if(Key.Level<6||Step<=0)return;
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
EPassageDensityQueryCaller::FarShellProjectionNormal);
// Four axial panels are the coarsest cross-section the survival audit samples. // Four axial panels are the coarsest cross-section the survival audit samples.
constexpr int32 SideCount=4; constexpr int32 SideCount=4;
const FIntVector TileOrigin=Key.OriginVoxels(); const FIntVector TileOrigin=Key.OriginVoxels();
@@ -963,6 +969,8 @@ private:
inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld& Shape, inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld& Shape,
double X,double Y,double ZMin,double ZMax) double X,double Y,double ZMin,double ZMax)
{ {
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
EPassageDensityQueryCaller::FarShellSurfaceQuery);
std::vector<double> Crossings; std::vector<double> Crossings;
if(ZMax<=ZMin)return Crossings; if(ZMax<=ZMin)return Crossings;
double PreviousZ=ZMin; double PreviousZ=ZMin;
@@ -990,6 +998,8 @@ inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld&
inline bool FindNearestFarStratumCrossing(const FStandaloneShapeWorld& Shape, inline bool FindNearestFarStratumCrossing(const FStandaloneShapeWorld& Shape,
double X,double Y,double ZMin,double ZMax,double Prediction,double& OutCrossing) double X,double Y,double ZMin,double ZMax,double Prediction,double& OutCrossing)
{ {
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
EPassageDensityQueryCaller::FarShellSurfaceQuery);
if(ZMax<=ZMin||!std::isfinite(Prediction))return false; if(ZMax<=ZMin||!std::isfinite(Prediction))return false;
const int32 Center=std::clamp(static_cast<int32>(std::floor(Prediction)), const int32 Center=std::clamp(static_cast<int32>(std::floor(Prediction)),
static_cast<int32>(std::ceil(ZMin)),static_cast<int32>(std::floor(ZMax))); static_cast<int32>(std::ceil(ZMin)),static_cast<int32>(std::floor(ZMax)));
@@ -1052,6 +1062,8 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh) const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
{ {
if(Key.Level<7||Step<4||!World.Manager)return; if(Key.Level<7||Step<4||!World.Manager)return;
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
EPassageDensityQueryCaller::FarShellStratumRefinement);
struct FBoundary { double Z=0.0,HalfWidth=8.0; }; struct FBoundary { double Z=0.0,HalfWidth=8.0; };
const auto& Layout=World.Manager->GetLayout(); const auto& Layout=World.Manager->GetLayout();
std::vector<FBoundary> Boundaries; std::vector<FBoundary> Boundaries;
@@ -1296,6 +1308,8 @@ inline int32 RemoveCoarseTransitionFaceTriangles(FVoxelMeshData& Mesh,
inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World, inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh) const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
{ {
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
EPassageDensityQueryCaller::FarShellStratumRefinement);
if(Key.Level<0||Step<=0||!World.Manager)return; if(Key.Level<0||Step<=0||!World.Manager)return;
const FIntVector Origin=Key.OriginVoxels(); const FIntVector Origin=Key.OriginVoxels();
const int32 Extent=Key.ExtentVoxels(); const int32 Extent=Key.ExtentVoxels();
@@ -1496,6 +1510,8 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World, inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh) const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
{ {
FStandaloneShapeWorld::FScopedPassageDensityQueryCaller queryCaller(
EPassageDensityQueryCaller::FarShellStratumRefinement);
if(Key.Level<0||Step<=0||!World.Manager||!Shape.ResolvedLayout)return; if(Key.Level<0||Step<=0||!World.Manager||!Shape.ResolvedLayout)return;
const FIntVector Origin=Key.OriginVoxels(); const FIntVector Origin=Key.OriginVoxels();
const int32 Extent=Key.ExtentVoxels(); const int32 Extent=Key.ExtentVoxels();
+380 -32
View File
@@ -5,6 +5,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdio>
#include <iomanip> #include <iomanip>
#include <limits> #include <limits>
#include <sstream> #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 // 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. // serialized seed in the exact integer range, as the stamp vocabulary does.
room.irregularitySeed=(isNativeLatticeStrate(room.strateIndex) room.irregularitySeed=(isNativeLatticeStrate(room.strateIndex)
?LayoutLatticeFeatureSeed(seed,room.strateIndex, ?LayoutLatticeFeatureSeed(seed,room.strateIndex,1,room.stableId)
NativeLatticeFieldRecipeVersion,room.stableId)
:LayoutStampSeed(seed,room.strateIndex,room.stableId)) :LayoutStampSeed(seed,room.strateIndex,room.stableId))
&0x001fffffffffffffull; &0x001fffffffffffffull;
const std::int64_t pad=room.smoothWidthMicrometres const std::int64_t pad=room.smoothWidthMicrometres
@@ -1919,8 +1919,7 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
{ {
const bool nativeLattice=isNativeLatticeStrate(edge.strateIndex); const bool nativeLattice=isNativeLatticeStrate(edge.strateIndex);
edge.irregularitySeed=(nativeLattice edge.irregularitySeed=(nativeLattice
?LayoutLatticeFeatureSeed(seed,edge.strateIndex, ?LayoutLatticeFeatureSeed(seed,edge.strateIndex,1,edge.stableId)
NativeLatticeFieldRecipeVersion,edge.stableId)
:LayoutStampSeed(seed,edge.strateIndex,edge.stableId)) :LayoutStampSeed(seed,edge.strateIndex,edge.stableId))
&0x001fffffffffffffull; &0x001fffffffffffffull;
using FCurvePoint=std::array<double,3>; 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>( const std::int64_t midRadius=std::max<std::int64_t>(
static_cast<std::int64_t>(std::llround((upperIsSurfaceWorld?3.5:1.5)*qPerMetre)), static_cast<std::int64_t>(std::llround((upperIsSurfaceWorld?3.5:1.5)*qPerMetre)),
cfg?cfg->midRadiusSubvoxels:mouthRadius); cfg?cfg->midRadiusSubvoxels:mouthRadius);
const std::int64_t reachMinimum=cfg?std::max<std::int64_t>(0,cfg->reachMinimumSubvoxels):8*qPerMetre; const std::int64_t requestedReachMinimum=cfg
const std::int64_t reachMaximum=cfg?std::max<std::int64_t>(reachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre; ?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 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::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; 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) if(!hasUpperRoute)
{ {
upperMouth[0]=xy[0];upperMouth[1]=xy[1]; upperMouth[0]=xy[0];upperMouth[1]=xy[1];
const auto reach=reachValue(PassageSeed(seed,upper.strateIndex,lower.strateIndex, const auto requestedReach=reachValue(PassageSeed(seed,upper.strateIndex,
connection,0x55505248u),reachMinimum,reachMaximum); lower.strateIndex,connection,0x55505248u),
requestedReachMinimum,requestedReachMaximum);
const auto reach=std::min(requestedReach,upperReachLimit);
upperMouth[2]=contentZQ(upper.content.bottomMicrometres)+reach; 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) if(!hasLowerRoute)
{ {
lowerMouth[0]=xy[0];lowerMouth[1]=xy[1]; lowerMouth[0]=xy[0];lowerMouth[1]=xy[1];
const auto reach=reachValue(PassageSeed(seed,upper.strateIndex,lower.strateIndex, const auto requestedReach=reachValue(PassageSeed(seed,upper.strateIndex,
connection,0x4c4f5752u),reachMinimum,reachMaximum); lower.strateIndex,connection,0x4c4f5752u),
requestedReachMinimum,requestedReachMaximum);
const auto reach=std::min(requestedReach,lowerReachLimit);
lowerMouth[2]=contentZQ(lower.content.topMicrometres)-reach; 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)) 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, std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16); upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
if(hasUpperRoute) // 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(); std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
const std::uint32_t hashedPhase=phase; const std::uint32_t hashedPhase=phase;
const std::int64_t phaseX=style==0&&anchoredIslandPassage const std::int64_t phaseX=style==0&&anchoredIslandPassage
&&!anchoredSpiral&&passageAnchor &&!anchoredSpiral&&passageAnchor
?upperMouth[0]-passageAnchor->values[0] ?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 const std::int64_t phaseY=style==0&&anchoredIslandPassage
&&!anchoredSpiral&&passageAnchor &&!anchoredSpiral&&passageAnchor
?upperMouth[1]-passageAnchor->values[1] ?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) for(std::uint32_t offset=0;offset<16;++offset)
{ {
const std::uint32_t candidate=static_cast<std::uint32_t>( const std::uint32_t candidate=static_cast<std::uint32_t>(
@@ -2464,23 +2502,6 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
{bestAlignment=alignment;phase=candidate;} {bestAlignment=alignment;phase=candidate;}
} }
} }
if(hasUpperRoute&&upperIsSurfaceWorld)
{
const auto& directionAtPhase=directions[phase];
const std::int64_t radialAlignment=upperMouth[0]*directionAtPhase[0]
+upperMouth[1]*directionAtPhase[1];
if(radialAlignment<0)
{
std::int64_t bestOutwardAlignment=std::numeric_limits<std::int64_t>::min();
for(std::uint32_t candidate=0;candidate<directions.size();++candidate)
{
const std::int64_t alignment=upperMouth[0]*directions[candidate][0]
+upperMouth[1]*directions[candidate][1];
if(alignment>bestOutwardAlignment)
{bestOutwardAlignment=alignment;phase=candidate;}
}
}
}
bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex, bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
connection,0x44495245u)&1u)!=0; connection,0x44495245u)&1u)!=0;
if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor) if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor)
@@ -2502,7 +2523,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
clockwise=clockwiseTangentX*upperTangent[0] clockwise=clockwiseTangentX*upperTangent[0]
+clockwiseTangentY*upperTangent[1]>=0.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. constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree helix contract.
const std::int64_t wormRadialWobble=std::min<std::int64_t>( const std::int64_t wormRadialWobble=std::min<std::int64_t>(
std::max<std::int64_t>(0,wander/3),2*qPerMetre); 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(centerX+radius*directions[directionIndex][0]/65536.0)),
static_cast<std::int64_t>(std::llround(centerY+radius*directions[directionIndex][1]/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; std::vector<std::array<std::int64_t,2>> xy;
const auto pushXY=[&](std::array<std::int64_t,2> p) const auto pushXY=[&](std::array<std::int64_t,2> p)
{if(xy.empty()||xy.back()!=p)xy.push_back(p);}; {if(xy.empty()||xy.back()!=p)xy.push_back(p);};
@@ -2705,6 +2987,12 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
} }
++loops; ++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) if(hasLowerTangent)
{ {
std::uint32_t lowerPhase=static_cast<std::uint32_t>(PassageSeed(seed, 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)); 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(); pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
std::size_t lowerMouthIndex=xy.size()-1; std::size_t lowerMouthIndex=xy.size()-1;
std::vector<std::array<std::int64_t,3>> lowerLandingPoints; std::vector<std::array<std::int64_t,3>> lowerLandingPoints;
@@ -2841,7 +3171,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
for(std::size_t i=0;i<xy.size();++i) for(std::size_t i=0;i<xy.size();++i)
{ {
std::int64_t radius=(i<=1||i==lowerMouthIndex 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) if(surfaceGroundDeparture&&i<lowerMouthIndex)
{ {
const double t=std::clamp(static_cast<double>(cumulative[i]) 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( radius=surfacePortalRadius+static_cast<std::int64_t>(std::llround(
(midRadius-surfacePortalRadius)*smooth)); (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; std::int64_t playerZ=0;
if(i>lowerMouthIndex) if(i>lowerMouthIndex)
playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2]; playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2];
File diff suppressed because it is too large. Load diff
@@ -11,5 +11,6 @@ namespace VoxelForgeCore
// the same command-line overrides as the viewer's reference loader. // the same command-line overrides as the viewer's reference loader.
ResolvedWorldSnapshot MakeStandaloneSnapshotFromParsedJson(const VFJson::Value& jsonRoot, ResolvedWorldSnapshot MakeStandaloneSnapshotFromParsedJson(const VFJson::Value& jsonRoot,
std::int32_t seed,std::int32_t clipRadius,std::int32_t maxLodOverride, std::int32_t seed,std::int32_t clipRadius,std::int32_t maxLodOverride,
bool roundTunnels,bool enhancedTunnelWalls,std::string& error); bool roundTunnels,bool enhancedTunnelWalls,std::string& error,
bool hasUnrealResolvedCorePlan=false);
} }
+406 -13
View File
@@ -18,6 +18,7 @@
#include <new> #include <new>
#include <stdexcept> #include <stdexcept>
#include <sstream> #include <sstream>
#include <cstdlib>
namespace vfcore::detail namespace vfcore::detail
{ {
@@ -926,10 +927,12 @@ namespace VoxelForgeCore
ResolvedWorldSnapshot MakeStandaloneSnapshotFromParsedJson(const VFJson::Value& jsonRoot, ResolvedWorldSnapshot MakeStandaloneSnapshotFromParsedJson(const VFJson::Value& jsonRoot,
std::int32_t seed,std::int32_t clipRadius,std::int32_t maxLodOverride, std::int32_t seed,std::int32_t clipRadius,std::int32_t maxLodOverride,
bool roundTunnels,bool enhancedTunnelWalls,std::string& error) bool roundTunnels,bool enhancedTunnelWalls,std::string& error,
bool hasUnrealResolvedCorePlan)
{ {
auto owner=std::make_shared<FStandaloneOwnerWorld>(); auto owner=std::make_shared<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::Resolve(jsonRoot,*owner,error))return {}; if(!StandaloneOwnerLoader::Resolve(jsonRoot,*owner,error))return {};
owner->bHasUnrealResolvedCorePlan=hasUnrealResolvedCorePlan;
std::shared_ptr<const WorldAssemblyManifest> worldAssembly; std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
if(jsonRoot.Get("world_assembly").IsObject()) if(jsonRoot.Get("world_assembly").IsObject())
{ {
@@ -975,10 +978,10 @@ namespace VoxelForgeCore
std::optional<ResolvedWorldSnapshot> ResolvedWorldSnapshot::FromJson( std::optional<ResolvedWorldSnapshot> ResolvedWorldSnapshot::FromJson(
std::string_view json,std::string& error) std::string_view json,std::string& error)
{ {
auto owner=std::make_shared<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::LoadJson(json,*owner,error))return std::nullopt;
VFJson::Value root; VFJson::Value root;
if(!VFJson::Parser(std::string(json)).Parse(root,error))return std::nullopt; if(!VFJson::Parser(std::string(json)).Parse(root,error))return std::nullopt;
auto owner=std::make_shared<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::Resolve(root,*owner,error))return std::nullopt;
std::shared_ptr<const WorldAssemblyManifest> worldAssembly; std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
if(root.Get("world_assembly").IsObject()) if(root.Get("world_assembly").IsObject())
{ {
@@ -1082,6 +1085,51 @@ namespace VoxelForgeCore
bool bEnabled=false; bool bEnabled=false;
}; };
struct FGridFillWorkerContext
{
FScopedSharedColumnCaches::FHeightfieldCache* HeightfieldCache=nullptr;
FScopedSharedColumnCaches::FIslandCache* IslandCache=nullptr;
const FStandaloneShapeWorld::FScopedPassageTileQueries* PassageQueries=nullptr;
int32 Lod=0;
bool bLegacyDirectMapped=false;
};
struct FGridFillWorkerState
{
bool bPreviousLegacyDirectMapped=false;
std::unique_ptr<FScopedSharedColumnCaches> SharedCaches;
std::unique_ptr<FStandaloneShapeWorld::FScopedPassageTileQueries> PassageQueries;
std::unique_ptr<VoxelForgeCore::Linux77Profiler::FScopedLod> Lod;
~FGridFillWorkerState()
{
Lod.reset();
PassageQueries.reset();
SharedCaches.reset();
Heightfield2D::setThreadColumnCacheLegacyDirectMapped(
bPreviousLegacyDirectMapped);
}
};
void* BeginGridFillWorkerContext(void* rawContext,std::size_t)
{
const auto& context=*static_cast<FGridFillWorkerContext*>(rawContext);
auto state=std::make_unique<FGridFillWorkerState>();
state->bPreviousLegacyDirectMapped=
Heightfield2D::threadColumnCacheLegacyDirectMapped();
Heightfield2D::setThreadColumnCacheLegacyDirectMapped(context.bLegacyDirectMapped);
state->SharedCaches=std::make_unique<FScopedSharedColumnCaches>(
context.HeightfieldCache,context.IslandCache);
if(context.PassageQueries)
state->PassageQueries=
std::make_unique<FStandaloneShapeWorld::FScopedPassageTileQueries>(
*context.PassageQueries);
state->Lod=std::make_unique<VoxelForgeCore::Linux77Profiler::FScopedLod>(context.Lod);
return state.release();
}
void EndGridFillWorkerContext(void*,void* rawState,std::size_t)
{ delete static_cast<FGridFillWorkerState*>(rawState); }
World::World(ResolvedWorldSnapshot snapshot):impl_(std::make_unique<Impl>()) World::World(ResolvedWorldSnapshot snapshot):impl_(std::make_unique<Impl>())
{ {
if(snapshot.data_) if(snapshot.data_)
@@ -1735,8 +1783,13 @@ namespace VoxelForgeCore
impl_->tileRegionCount.store(impl_->tileRegions.size(),std::memory_order_release); impl_->tileRegionCount.store(impl_->tileRegions.size(),std::memory_order_release);
} }
void World::buildFarWorld() bool World::buildFarWorld()
{ if(impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape)impl_->shape->BuildFarWorld(); } {
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)
return false;
if(!impl_->shape->HasFarWorld())impl_->shape->BuildFarWorld();
return impl_->shape->HasFarWorld();
}
bool World::isCompiled() const noexcept bool World::isCompiled() const noexcept
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire); } { return impl_&&impl_->shapeReady.load(std::memory_order_acquire); }
bool World::hasFarWorld() const noexcept bool World::hasFarWorld() const noexcept
@@ -1857,6 +1910,8 @@ namespace VoxelForgeCore
:"uncompiled"; } :"uncompiled"; }
float World::unclampedDensityAt(Vec3d position) const float World::unclampedDensityAt(Vec3d position) const
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAtUnclamped(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; } { return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAtUnclamped(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
float World::passageSourceDensityAt(Vec3d position) const
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->PassageSourceDensityAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
DetailFieldSample World::detailSampleAt(Vec3d position) const DetailFieldSample World::detailSampleAt(Vec3d position) const
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DetailSampleAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):DetailFieldSample{}; } { return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DetailSampleAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):DetailFieldSample{}; }
float World::densityAtLattice(Vec3i position) const float World::densityAtLattice(Vec3i position) const
@@ -1891,6 +1946,17 @@ namespace VoxelForgeCore
std::vector<TileKey> output(tiles.begin(),tiles.end()); std::vector<TileKey> output(tiles.begin(),tiles.end());
std::sort(output.begin(),output.end(),[&](const TileKey& a,const TileKey& b) 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); const double da=TileDistanceSquared(a,centerChunk),db=TileDistanceSquared(b,centerChunk);
if(da!=db)return da<db; if(da!=db)return da<db;
if(a.lod!=b.lod)return a.lod<b.lod; if(a.lod!=b.lod)return a.lod<b.lod;
@@ -1901,6 +1967,41 @@ namespace VoxelForgeCore
return output; return output;
} }
std::uint32_t World::passageOverlapScore(const TileKey& key) const noexcept
{
if(!impl_||!impl_->resolvedLayout||key.lod<0||key.lod>30)return 0;
constexpr std::int64_t SubvoxelsPerVoxel=WorldAssemblySubvoxelsPerVoxel;
constexpr std::int64_t ExtraInfluenceVoxels=32;
const std::int64_t Extent=static_cast<std::int64_t>(CoreTileVoxelCount)<<key.lod;
const std::int64_t TileMinimum[3]={
static_cast<std::int64_t>(key.coordinate.x)*Extent,
static_cast<std::int64_t>(key.coordinate.y)*Extent,
static_cast<std::int64_t>(key.coordinate.z)*Extent};
const std::int64_t TileMaximum[3]={TileMinimum[0]+Extent,
TileMinimum[1]+Extent,TileMinimum[2]+Extent};
std::uint32_t Score=0;
for(const auto& Passage:impl_->resolvedLayout->passages)
for(std::size_t Index=1;Index<Passage.controlPoints.size();++Index)
{
const auto& A=Passage.controlPoints[Index-1];
const auto& B=Passage.controlPoints[Index];
const std::int64_t Radius=std::max(A.radiusSubvoxels,B.radiusSubvoxels)
/SubvoxelsPerVoxel+ExtraInfluenceVoxels;
bool bOverlap=true;
for(std::size_t Axis=0;Axis<3;++Axis)
{
const std::int64_t AVoxel=A.positionSubvoxels[Axis]/SubvoxelsPerVoxel;
const std::int64_t BVoxel=B.positionSubvoxels[Axis]/SubvoxelsPerVoxel;
const std::int64_t Minimum=std::min(AVoxel,BVoxel)-Radius;
const std::int64_t Maximum=std::max(AVoxel,BVoxel)+Radius;
bOverlap=bOverlap&&Maximum>=TileMinimum[Axis]
&&Minimum<=TileMaximum[Axis];
}
if(bOverlap)++Score;
}
return Score;
}
std::vector<TileKey> World::desiredTiles(Vec3d cameraPosition) const std::vector<TileKey> World::desiredTiles(Vec3d cameraPosition) const
{ {
if(!impl_||!impl_->owner)return {}; if(!impl_||!impl_->owner)return {};
@@ -1997,6 +2098,7 @@ namespace VoxelForgeCore
std::optional<TileMesh> World::generateTile(const TileKey& key,const TileGenerationOptions& options, std::optional<TileMesh> World::generateTile(const TileKey& key,const TileGenerationOptions& options,
std::string& error) const 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 // 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. // 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. // Les tuiles proches n'avaient aucun conflit; garder leur acces simple et associer les LOD grossiers.
@@ -2162,6 +2264,50 @@ namespace VoxelForgeCore
TileMesh out;out.key=key; TileMesh out;out.key=key;
const auto CopyIslandCacheMetrics=[&]() 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.densityOffGridFloatByCaller=
QueryCounts.DensityOffGridFloatByCaller;
out.diagnostics.densityOffGridUnclampedByCaller=
QueryCounts.DensityOffGridUnclampedByCaller;
out.diagnostics.densityOffGridDetailByCaller=
QueryCounts.DensityOffGridDetailByCaller;
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; if(!options.instrumentTileDrain)return;
islandCacheInstrumentation.tileCycles=VoxelForgeCoreShared::ReadCpuCycles() islandCacheInstrumentation.tileCycles=VoxelForgeCoreShared::ReadCpuCycles()
-islandCacheInstrumentation.tileStartCycles; -islandCacheInstrumentation.tileStartCycles;
@@ -2200,16 +2346,25 @@ namespace VoxelForgeCore
line<<" native_"<<CallerNames[i]<<"="<<a.sampledNativeCalls[i]*256<<"/" line<<" native_"<<CallerNames[i]<<"="<<a.sampledNativeCalls[i]*256<<"/"
<<a.sampledNativeCalls[i]<<"/" <<a.sampledNativeCalls[i]<<"/"
<<Estimate(a.nativeCycles[i],a.sampledNativeCalls[i]*256, <<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]); a.sampledNativeCalls[i]);
line<<" field_"<<CallerNames[i]<<"="<<a.sampledFieldCalls[i]*256<<"/" line<<" field_"<<CallerNames[i]<<"="<<a.sampledFieldCalls[i]*256<<"/"
<<a.sampledFieldCalls[i]<<"/" <<a.sampledFieldCalls[i]<<"/"
<<Estimate(a.fieldCycles[i],a.sampledFieldCalls[i]*256, <<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]); a.sampledFieldCalls[i]);
} }
for(std::size_t i=0;i<BucketNames.size();++i) for(std::size_t i=0;i<BucketNames.size();++i)
line<<" comp_"<<BucketNames[i]<<"=" line<<" comp_"<<BucketNames[i]<<"="
<<Estimate(a.exclusiveCycles[i],a.scopeSamples[i]*256, <<Estimate(a.exclusiveCycles[i],a.scopeSamples[i]*256,
a.scopeSamples[i]) 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; <<"/"<<a.scopeSamples[i]<<"/"<<a.scopeSamples[i]*256;
const auto& shared=islandCacheInstrumentation.shared; const auto& shared=islandCacheInstrumentation.shared;
line<<" local_probe_calls="<<(islandCacheInstrumentation.localHits line<<" local_probe_calls="<<(islandCacheInstrumentation.localHits
@@ -2323,6 +2478,7 @@ namespace VoxelForgeCore
&&bHasNativeSurfaceHorizonPlan&&!bHasIslandPlanInTile; &&bHasNativeSurfaceHorizonPlan&&!bHasIslandPlanInTile;
const bool bSkipFarShellRepair=key.lod>=7&&bUseNativeSurfaceHorizon; const bool bSkipFarShellRepair=key.lod>=7&&bUseNativeSurfaceHorizon;
FStandaloneShapeWorld::ResetThreadClassificationStats(); FStandaloneShapeWorld::ResetThreadClassificationStats();
FStandaloneShapeWorld::SetThreadDensityQueryProfileEnabled(options.instrumentTileDrain);
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats( VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(
options.captureColumnCoordinates); options.captureColumnCoordinates);
if(options.captureColumnCoordinates) if(options.captureColumnCoordinates)
@@ -2335,6 +2491,7 @@ namespace VoxelForgeCore
static_cast<std::int64_t>(origin.Y+extent)*SubvoxelsPerVoxel); static_cast<std::int64_t>(origin.Y+extent)*SubvoxelsPerVoxel);
} }
bool bNativeSurfaceTile=false; bool bNativeSurfaceTile=false;
bool bTileProofSkipSafe=false;
bool bNativeSurfaceBandSafe=false; bool bNativeSurfaceBandSafe=false;
double classificationColumnComputeMilliseconds=0.0; double classificationColumnComputeMilliseconds=0.0;
if(bHasWorldAssembly) if(bHasWorldAssembly)
@@ -2342,10 +2499,15 @@ namespace VoxelForgeCore
const double columnMillisecondsBeforeClassification= const double columnMillisecondsBeforeClassification=
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
const auto Classification=tileShape.ClassifyBoxDetailed(origin,step,cells); 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, classificationColumnComputeMilliseconds=std::max(0.0,
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds
-columnMillisecondsBeforeClassification); -columnMillisecondsBeforeClassification);
bNativeSurfaceTile=Classification.bNativeSurface; bNativeSurfaceTile=Classification.bNativeSurface;
bTileProofSkipSafe=Classification.bSkipSafe;
if(options.profileMesherStages) if(options.profileMesherStages)
out.diagnostics.nativeSurfaceTile=bNativeSurfaceTile; out.diagnostics.nativeSurfaceTile=bNativeSurfaceTile;
// NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its // NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its
@@ -2353,7 +2515,7 @@ namespace VoxelForgeCore
// unknown; edits and cross-strate tiles are still excluded below. // unknown; edits and cross-strate tiles are still excluded below.
// NativeSurfaceDensityAt ne consomme que le champ et les volumes bornes du manifeste. // NativeSurfaceDensityAt ne consomme que le champ et les volumes bornes du manifeste.
bNativeSurfaceBandSafe=bNativeSurfaceTile; bNativeSurfaceBandSafe=bNativeSurfaceTile;
if(bNativeSurfaceTile) if(bNativeSurfaceTile||bTileProofSkipSafe)
{ {
out.diagnostics.tileWasClassified=true; out.diagnostics.tileWasClassified=true;
switch(Classification.bUnknown?EVoxelTileClass::Mixed:Classification.Verdict) switch(Classification.bUnknown?EVoxelTileClass::Mixed:Classification.Verdict)
@@ -2362,12 +2524,30 @@ namespace VoxelForgeCore
case EVoxelTileClass::AllAir:out.diagnostics.tileClass=TileClass::allAir;break; case EVoxelTileClass::AllAir:out.diagnostics.tileClass=TileClass::allAir;break;
case EVoxelTileClass::Mixed:out.diagnostics.tileClass=TileClass::mixed;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) ||out.diagnostics.tileClass==TileClass::allAir)
&&!options.hashDensity&&!options.captureDensitySamples&&!bNeedsFarShell &&(!options.hashDensity||options.allowUniformSkipsWhenHashing)
&&!options.captureDensitySamples&&!bNeedsFarShell
&&!options.auditUniformBlockProofs) &&!options.auditUniformBlockProofs)
{ {
out.diagnostics.meshingSkipped=true; 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 Counts=FStandaloneShapeWorld::GetThreadClassificationStats();
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats(); const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
out.diagnostics.solidBoxes=Counts.SolidBoxes; out.diagnostics.solidBoxes=Counts.SolidBoxes;
@@ -2401,10 +2581,12 @@ namespace VoxelForgeCore
out.densitySampleCount=0; out.densitySampleCount=0;
return out; 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(); const auto MeshingStart=std::chrono::steady_clock::now();
int32 MeshBandMinZVoxel=options.bandMinZVoxel; int32 MeshBandMinZVoxel=options.bandMinZVoxel;
@@ -2619,8 +2801,142 @@ namespace VoxelForgeCore
// queryMinimum/queryMaximum above are exactly that inclusive domain. // queryMinimum/queryMaximum above are exactly that inclusive domain.
// GenerateMesh n'échantillonne que origin..origin+extent et la marge de normale d'un pas ; // GenerateMesh n'échantillonne que origin..origin+extent et la marge de normale d'un pas ;
// queryMinimum/queryMaximum ci-dessus couvrent exactement ce domaine inclusif. // queryMinimum/queryMaximum ci-dessus couvrent exactement ce domaine inclusif.
const FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope( FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope(
tileShape,queryMinimum,queryMaximum); tileShape,queryMinimum,queryMaximum,
options.profilePassageComposer?&out.diagnostics:nullptr,key.lod,
options.diagnosticPassageSourceFloorVoxels,
options.diagnosticPassageCarveReachMetres);
passageFeatureScope.prepareSweptTubeBase(origin,MeshStep,MeshCells,
static_cast<std::uint32_t>(world.Seed));
static std::atomic<bool> l98ProjectionProbeStarted{false};
const char* l98ProjectionProbeSwitch=std::getenv(
"VOXELFORGE_L98_PASSAGE_REUSE_PROBE");
bool expectedProbeStart=false;
if(l98ProjectionProbeSwitch&&l98ProjectionProbeSwitch[0]=='1'
&&l98ProjectionProbeSwitch[1]=='\0'&&!tileShape.IsNativeOnly()
&&impl_->resolvedLayout
&&!impl_->resolvedLayout->passages.empty()
&&l98ProjectionProbeStarted.compare_exchange_strong(expectedProbeStart,true,
std::memory_order_relaxed))
{
constexpr std::int64_t ProbePadding=8
*WorldAssemblySubvoxelsPerVoxel;
std::array<std::int64_t,3> probeMinimum={
std::numeric_limits<std::int64_t>::max(),
std::numeric_limits<std::int64_t>::max(),
std::numeric_limits<std::int64_t>::max()};
std::array<std::int64_t,3> probeMaximum={
std::numeric_limits<std::int64_t>::min(),
std::numeric_limits<std::int64_t>::min(),
std::numeric_limits<std::int64_t>::min()};
const auto includeProbePoint=[&](std::size_t axis,double coordinate,
double radius)
{
if(!std::isfinite(coordinate)||!std::isfinite(radius))return;
probeMinimum[axis]=std::min(probeMinimum[axis],
static_cast<std::int64_t>(std::floor(coordinate-radius))-ProbePadding);
probeMaximum[axis]=std::max(probeMaximum[axis],
static_cast<std::int64_t>(std::ceil(coordinate+radius))+ProbePadding);
};
for(const auto& passage:impl_->resolvedLayout->passages)
for(const auto& point:passage.controlPoints)
for(std::size_t axis=0;axis<3;++axis)
includeProbePoint(axis,static_cast<double>(
point.positionSubvoxels[axis]),
static_cast<double>(point.radiusSubvoxels));
for(const auto& stratum:impl_->resolvedLayout->strates)
for(const auto& anchor:stratum.anchors)
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
for(std::size_t axis=0;axis<3;++axis)
includeProbePoint(axis,anchor.values[axis],0.0);
std::uint64_t probeRuns=0,probeSamples=0,probeMismatches=0;
if(probeMinimum[0]<=probeMaximum[0]
&&probeMinimum[1]<=probeMaximum[1]
&&probeMinimum[2]<=probeMaximum[2])
{
const FStandaloneShapeWorld::FScopedPassageTileQueries probeScope(
tileShape,probeMinimum,probeMaximum,nullptr,key.lod);
constexpr int32 ProbeCount=8;
float runDensity[ProbeCount]{};
for(const auto& passage:impl_->resolvedLayout->passages)
for(std::size_t segmentIndex=0;
segmentIndex+1<passage.controlPoints.size();++segmentIndex)
{
const auto& a=passage.controlPoints[segmentIndex];
const auto& b=passage.controlPoints[segmentIndex+1];
const auto toVoxel=[](double subvoxels)
{return static_cast<std::int32_t>(std::llround(
subvoxels/WorldAssemblySubvoxelsPerVoxel));};
const std::int32_t x=toVoxel(0.5*(
static_cast<double>(a.positionSubvoxels[0])
+static_cast<double>(b.positionSubvoxels[0])));
const std::int32_t y=toVoxel(0.5*(
static_cast<double>(a.positionSubvoxels[1])
+static_cast<double>(b.positionSubvoxels[1])));
const std::int32_t zMid=toVoxel(0.5*(
static_cast<double>(a.positionSubvoxels[2])
+static_cast<double>(b.positionSubvoxels[2])));
const std::int32_t zStart=zMid-ProbeCount/2;
tileShape.DensityAtLatticeZRun(x,y,zStart,1,ProbeCount,runDensity);
++probeRuns;
probeSamples+=ProbeCount;
for(std::int32_t lane=0;lane<ProbeCount;++lane)
{
const float scalarDensity=tileShape.DensityAtLattice(
x,y,zStart+lane);
if(std::bit_cast<std::uint32_t>(scalarDensity)
!=std::bit_cast<std::uint32_t>(runDensity[lane]))
++probeMismatches;
}
}
}
std::printf("[L98PassageReuseProbe] z_runs=%llu samples=%llu scalar_mismatches=%llu status=%s\n",
static_cast<unsigned long long>(probeRuns),
static_cast<unsigned long long>(probeSamples),
static_cast<unsigned long long>(probeMismatches),
probeRuns>0&&probeMismatches==0?"PASS":"FAIL");
}
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();
}
}
FGridFillWorkerContext GridFillWorkerContext;
if(options.gridFillScheduler&&options.gridFillWorkerLimit>1)
{
GridFillWorkerContext.HeightfieldCache=options.useSharedColumnCache
?impl_->sharedHeightfieldColumns.get():nullptr;
GridFillWorkerContext.IslandCache=options.useSharedColumnCache
?impl_->sharedIslandColumns.get():nullptr;
GridFillWorkerContext.PassageQueries=
FStandaloneShapeWorld::FScopedPassageTileQueries::CurrentFor(tileShape);
GridFillWorkerContext.Lod=key.lod;
GridFillWorkerContext.bLegacyDirectMapped=
options.legacyDirectMappedColumnCache||key.lod<=2;
FVoxelGridFillExecution Execution;
Execution.SchedulerContext=const_cast<JobScheduler*>(options.gridFillScheduler);
Execution.ParallelFor=[](void* RawScheduler,std::size_t Count,
std::size_t WorkerLimit,void* TaskContext,FVoxelGridFillExecution::FTask Task)
{
return static_cast<const JobScheduler*>(RawScheduler)->parallel_for(
Count,1,WorkerLimit,TaskContext,Task,nullptr,0);
};
Execution.WorkerLimit=std::clamp<std::size_t>(
options.gridFillWorkerLimit,2,16);
Execution.WorkerContext=&GridFillWorkerContext;
Execution.BeginWorkerContext=&BeginGridFillWorkerContext;
Execution.EndWorkerContext=&EndGridFillWorkerContext;
mesher.SetGridFillExecution(Execution);
}
const bool bDiagnosticColumnSampling=options.diagnosticGlobalOctaveDrop!=0 const bool bDiagnosticColumnSampling=options.diagnosticGlobalOctaveDrop!=0
||options.diagnosticColumnInterpolationStride>1; ||options.diagnosticColumnInterpolationStride>1;
const std::uint32_t PreviousOctaveDrop=bDiagnosticColumnSampling const std::uint32_t PreviousOctaveDrop=bDiagnosticColumnSampling
@@ -2645,6 +2961,7 @@ namespace VoxelForgeCore
options.profileMesherStages?&McStageTimings:nullptr, options.profileMesherStages?&McStageTimings:nullptr,
options.auditUniformBlockProofs?&BlockProofAudit:nullptr, options.auditUniformBlockProofs?&BlockProofAudit:nullptr,
options.forceFullDensityCapture); options.forceFullDensityCapture);
passageFeatureScope.copySweptTubeRuntimeCounts();
if(options.auditUniformBlockProofs) if(options.auditUniformBlockProofs)
{ {
out.diagnostics.uniformBlockProofsChecked=BlockProofAudit.CheckedBlocks; out.diagnostics.uniformBlockProofsChecked=BlockProofAudit.CheckedBlocks;
@@ -2678,6 +2995,7 @@ namespace VoxelForgeCore
out.diagnostics.marchingCubesExclusiveMilliseconds out.diagnostics.marchingCubesExclusiveMilliseconds
-out.diagnostics.marchingCubesSkirtMilliseconds); -out.diagnostics.marchingCubesSkirtMilliseconds);
out.diagnostics.marchingCubesDensityGridMilliseconds=McStageTimings.DensityGridMilliseconds; out.diagnostics.marchingCubesDensityGridMilliseconds=McStageTimings.DensityGridMilliseconds;
out.diagnostics.marchingCubesGridFillWorkers=McStageTimings.GridFillWorkerCount;
out.diagnostics.marchingCubesCellClassificationMilliseconds= out.diagnostics.marchingCubesCellClassificationMilliseconds=
McStageTimings.CellClassificationMilliseconds; McStageTimings.CellClassificationMilliseconds;
out.diagnostics.marchingCubesGradientNormalsMilliseconds= out.diagnostics.marchingCubesGradientNormalsMilliseconds=
@@ -2712,10 +3030,16 @@ namespace VoxelForgeCore
out.diagnostics.marchingCubesSkirtBoundaryCandidates= out.diagnostics.marchingCubesSkirtBoundaryCandidates=
McStageTimings.SkirtBoundaryCandidates; McStageTimings.SkirtBoundaryCandidates;
out.diagnostics.marchingCubesSkirtDensityProbes=McStageTimings.SkirtDensityProbes; out.diagnostics.marchingCubesSkirtDensityProbes=McStageTimings.SkirtDensityProbes;
out.diagnostics.marchingCubesSkirtDensityProbesByCaller=
McStageTimings.SkirtDensityProbesByCaller;
out.diagnostics.marchingCubesSkirtDensityOffGridProbesByCaller=
McStageTimings.SkirtDensityOffGridProbesByCaller;
} }
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>( const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-MeshingStart).count(); std::chrono::steady_clock::now()-MeshingStart).count();
out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0; 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 auto FarShellStart=std::chrono::steady_clock::now();
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape: const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:
(bShapeReady?*impl_->shape:tileShape); (bShapeReady?*impl_->shape:tileShape);
@@ -2849,6 +3173,7 @@ namespace VoxelForgeCore
AppendStratumBoundaryFloors(world,farShape,nativeKey,step,mesh); AppendStratumBoundaryFloors(world,farShape,nativeKey,step,mesh);
AppendStratumBoundaryCeilings(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)); out.densitySampleCount=static_cast<std::uint64_t>(std::max<int64>(0,sampleCount));
if(bHasWorldAssembly) if(bHasWorldAssembly)
@@ -2959,6 +3284,74 @@ namespace VoxelForgeCore
const auto& values=options.captureDensitySamples?out.debugDensitySamples:densityValues; const auto& values=options.captureDensitySamples?out.debugDensitySamples:densityValues;
for(const float value:values) for(const float value:values)
{ densityHash^=std::bit_cast<std::uint32_t>(value);densityHash*=1099511628211ull; } { 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.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh);
out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh); out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh);
out.materialHash=HashMaterialSamples(out.vertices); 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_CLAMP(v,a,b) clamp(v,a,b)
#define VF_FIELD_LERP(a,b,t) lerp(a,b,t) #define VF_FIELD_LERP(a,b,t) lerp(a,b,t)
#else #else
#include <array>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
@@ -244,11 +245,70 @@ VF_FIELD_INLINE float VF_FieldValueNoise(float x, float y, float z,
return VF_FieldLerp(VF_FieldLerp(z00, z10, ty), VF_FieldLerp(z01, z11, ty), tz); return VF_FieldLerp(VF_FieldLerp(z00, z10, ty), VF_FieldLerp(z01, z11, ty), tz);
} }
#if !defined(VF_FIELD_HLSL)
struct VF_FieldValueNoiseCacheEntry
{
VF_FieldI32 x=0,y=0,z=0,wavelength=0;
VF_FieldU32 seed=0;
bool valid=false;
std::array<float,8> corners{};
};
// Reuse the eight lattice values while adjacent voxel samples remain in the same
// value-noise cell. Only the integer hash lookups are shared; interpolation keeps
// the same inputs and operation order as VF_FieldValueNoise.
VF_FIELD_INLINE float VF_FieldCachedValueNoise(float x,float y,float z,
VF_FieldI32 wavelength,VF_FieldU32 seed)
{
static thread_local std::array<VF_FieldValueNoiseCacheEntry,64> cache{};
const float inv=1.0f/VF_FIELD_CAST_F32(wavelength);
const float fx=x*inv,fy=y*inv,fz=z*inv;
const VF_FieldI32 x0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fx));
const VF_FieldI32 y0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fy));
const VF_FieldI32 z0=VF_FIELD_CAST_I32(VF_FIELD_FLOOR(fz));
const VF_FieldU32 cellHash=VF_FieldMix(seed
^VF_FIELD_CAST_U32(x0)*0x68E31DA4u
^VF_FIELD_CAST_U32(y0)*0x1B56C4E9u
^VF_FIELD_CAST_U32(z0)*0x9E3779B9u
^VF_FIELD_CAST_U32(wavelength)*0x85EBCA77u);
auto& entry=cache[cellHash&(cache.size()-1)];
if(!entry.valid||entry.x!=x0||entry.y!=y0||entry.z!=z0
||entry.wavelength!=wavelength||entry.seed!=seed)
{
entry.x=x0;entry.y=y0;entry.z=z0;entry.wavelength=wavelength;
entry.seed=seed;entry.valid=true;
entry.corners={
VF_FieldNoiseValue(x0,y0,z0,seed),
VF_FieldNoiseValue(x0+1,y0,z0,seed),
VF_FieldNoiseValue(x0,y0+1,z0,seed),
VF_FieldNoiseValue(x0+1,y0+1,z0,seed),
VF_FieldNoiseValue(x0,y0,z0+1,seed),
VF_FieldNoiseValue(x0+1,y0,z0+1,seed),
VF_FieldNoiseValue(x0,y0+1,z0+1,seed),
VF_FieldNoiseValue(x0+1,y0+1,z0+1,seed)};
}
const float tx=VF_FieldFade(fx-VF_FIELD_CAST_F32(x0));
const float ty=VF_FieldFade(fy-VF_FIELD_CAST_F32(y0));
const float tz=VF_FieldFade(fz-VF_FIELD_CAST_F32(z0));
const float z00=VF_FieldLerp(entry.corners[0],entry.corners[1],tx);
const float z10=VF_FieldLerp(entry.corners[2],entry.corners[3],tx);
const float z01=VF_FieldLerp(entry.corners[4],entry.corners[5],tx);
const float z11=VF_FieldLerp(entry.corners[6],entry.corners[7],tx);
return VF_FieldLerp(VF_FieldLerp(z00,z10,ty),VF_FieldLerp(z01,z11,ty),tz);
}
#endif
VF_FIELD_INLINE float VF_FieldSurfaceRelief(float x, float y, float z, VF_FieldU32 seed) VF_FIELD_INLINE float VF_FieldSurfaceRelief(float x, float y, float z, VF_FieldU32 seed)
{ {
#if defined(VF_FIELD_HLSL)
const float macro = VF_FieldValueNoise(x, y, z, 32, seed ^ 0xB5297A4Du); const float macro = VF_FieldValueNoise(x, y, z, 32, seed ^ 0xB5297A4Du);
const float mid = VF_FieldValueNoise(x, y, z, 16, seed ^ 0x63D83595u); const float mid = VF_FieldValueNoise(x, y, z, 16, seed ^ 0x63D83595u);
const float fine = VF_FieldValueNoise(x, y, z, 4, seed ^ 0xA511E9B3u); const float fine = VF_FieldValueNoise(x, y, z, 4, seed ^ 0xA511E9B3u);
#else
const float macro=VF_FieldCachedValueNoise(x,y,z,32,seed^0xB5297A4Du);
const float mid=VF_FieldCachedValueNoise(x,y,z,16,seed^0x63D83595u);
const float fine=VF_FieldCachedValueNoise(x,y,z,4,seed^0xA511E9B3u);
#endif
return 1.20f * macro + (0.80f + 0.25f * macro) * (0.40f * mid + 0.60f * fine); return 1.20f * macro + (0.80f + 0.25f * macro) * (0.40f * mid + 0.60f * fine);
} }
@@ -335,12 +395,21 @@ VF_FIELD_INLINE float VF_FieldPassageWallRelief(float x,float y,float z,VF_Field
{ {
// Use long, non-commensurate rotated scales. The shared passage wall keeps broad, // Use long, non-commensurate rotated scales. The shared passage wall keeps broad,
// uneven rock relief without a short wavelength that can read as repeated rings. // uneven rock relief without a short wavelength that can read as repeated rings.
#if defined(VF_FIELD_HLSL)
const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43, const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
seed^0xB5297A4Du); seed^0xB5297A4Du);
const float mid=VF_FieldValueNoise(y-7.0f,z+19.0f,x-31.0f,29, const float mid=VF_FieldValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
seed^0x63D83595u); seed^0x63D83595u);
const float fine=VF_FieldValueNoise(z+5.0f,x+29.0f,y-13.0f,19, const float fine=VF_FieldValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
seed^0xA511E9B3u); seed^0xA511E9B3u);
#else
const float broad=VF_FieldCachedValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
seed^0xB5297A4Du);
const float mid=VF_FieldCachedValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
seed^0x63D83595u);
const float fine=VF_FieldCachedValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
seed^0xA511E9B3u);
#endif
return 0.50f*broad+0.35f*mid+0.15f*fine; return 0.50f*broad+0.35f*mid+0.15f*fine;
} }
@@ -394,9 +463,9 @@ VF_FIELD_INLINE float VF_FieldOwnerCellular(float x,float y,float z)
{ {
const VF_FieldI32 nx=cellX+dx,ny=cellY+dy,nz=cellZ+dz; const VF_FieldI32 nx=cellX+dx,ny=cellY+dy,nz=cellZ+dz;
const VF_FieldU32 h=VF_FieldVoxelHashMix( const VF_FieldU32 h=VF_FieldVoxelHashMix(
VF_FIELD_CAST_U32(nx+0x7FFFFFFF) (VF_FIELD_CAST_U32(nx)+0x7FFFFFFFu)
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(ny+0x7FFFFFFF)*2654435761u) ^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(ny)+0x7FFFFFFFu)*2654435761u)
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(nz+0x7FFFFFFF)*374761393u)); ^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(nz)+0x7FFFFFFFu)*374761393u));
const float fpx=VF_FIELD_CAST_F32(dx) const float fpx=VF_FIELD_CAST_F32(dx)
+VF_FIELD_CAST_F32(h&0xFFFFFFu)*(1.0f/16777216.0f)-fracX; +VF_FIELD_CAST_F32(h&0xFFFFFFu)*(1.0f/16777216.0f)-fracX;
const VF_FieldU32 hy=VF_FieldVoxelHashMix(h^0x12345678u); const VF_FieldU32 hy=VF_FieldVoxelHashMix(h^0x12345678u);
@@ -2653,6 +2653,9 @@ void Heightfield2D::setThreadColumnCaptureBounds(std::int64_t minimumXSubvoxels,
void Heightfield2D::setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept void Heightfield2D::setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept
{ world_assembly_detail::HeightfieldColumnCacheLegacyDirectMapped=enabled; } { world_assembly_detail::HeightfieldColumnCacheLegacyDirectMapped=enabled; }
bool Heightfield2D::threadColumnCacheLegacyDirectMapped() noexcept
{ return world_assembly_detail::HeightfieldColumnCacheLegacyDirectMapped; }
void Heightfield2D::setThreadColumnOctaveDrop(std::uint32_t octaveDrop) noexcept void Heightfield2D::setThreadColumnOctaveDrop(std::uint32_t octaveDrop) noexcept
{ world_assembly_detail::HeightfieldColumnOctaveDrop=octaveDrop; } { world_assembly_detail::HeightfieldColumnOctaveDrop=octaveDrop; }
@@ -0,0 +1,48 @@
Thank you, that was a strong first pass. Your scope warning about L76 was right and important. I need to correct my
brief: "about 75% of field time is the passage composer" was **wrong**.
- L76's term profiler covered only about 6.0M legacy samples for FI (5.6M for TN).
- L73 counts about 52M density calls per FI or TN whole view: 512 LOD0 tiles × 44,121, 448 LOD1 × 43,322, and so
on.
- The measured passage/support cost is about 6.5 of FI's ~40 field worker-seconds (~15%), not 75%. Roughly
30 worker-seconds of field time is not attributed to any term we've measured.
Your gain arithmetic for recommendation 1 inherited that error. Please redo it.
## Deeper pass (you now have more reasoning budget)
Examine **the whole generation chain**, end to end, for one cold whole view on the legacy FI/TN path. For each
stage, say what it does, how often it runs, what it costs per call (from code structure and the measured numbers),
and whether its work is necessary:
- world/plan resolution;
- tile key selection;
- `ClassifyBoxDetailed`;
- the dispatch into `AnalyticDensityAt` (and whatever runs for the ~46M calls the term profiler did not see);
- the composer;
- grid fill;
- MC;
- normals;
- skirts;
- ground/cap refinement;
- payload;
- Unreal publication.
The central question: **where do the unattributed ~30 worker-seconds most plausibly go?** For example:
- a different evaluation path the profiler doesn't instrument;
- per-call overhead outside the timed terms (dispatch, stratum lookup, virtual calls, allocation, cache misses);
- border/halo samples evaluated twice;
- normals sampling the field again;
- duplicated work between neighbouring tiles or LODs.
Give file:line for each. Then name the cheapest measurement that would confirm your top explanation, so the next
implementation round can verify it first.
Then **revise your ranked recommendations** in the light of the whole chain. Keep what still holds and drop what
doesn't. Add anything the chain review reveals.
Same rules as before:
- read-only;
- no edits anywhere, including temp files;
- web allowed;
- labels [CODE]/[MEASURED]/[PRIOR ART]/[INFERENCE].
- At most ~3,500 words.
- **Lead with the stage-by-stage cost map**, then the revised ranking.
@@ -0,0 +1,74 @@
Thank you for both earlier passes. Your rec #1 (localize passages) turned out to be right, and here is what we've
learned since. This is a focused third pass: **passage evaluation design.** Read and reason only; no edits
anywhere, including temp files. Web allowed. Labels as before.
## What we know now (all measured, ablations verified to remove the work)
- **Owner four-strate world, cold LOD0–2-ready at W12:** FI 2.27 s, TN 2.25 s. Passages off: FI 0.81 s,
TN 0.85 s. Boundary bands cost only 0.1–0.2 s. Passage composition costs **~1.4–1.6 s** of near readiness, and
it alone separates FI/TN from the ~1 s goal. SW 1.02 s and VS 1.13 s are close already.
- **Inside the passage phase** (sampled, L83): carve distance ~40%, wall relief ~21–24% (3D value noise, 3 octaves
at 43/29/19 voxels), support restoration/columns ~14%, mouth shaping ~4%, other ~11%. Top symbols:
`ApplyResolvedStratePassageCarves`, `VF_FieldCachedValueNoise`, `VisitNearestPassageSpatialNode`,
`PassagePotentialWithinNode`, the carve lambda.
- **Exact trims done** (L81–L85):
- a density-aware outer reach bound (carve = `max(source, -roughSDF/Q)`, reach = `-d·Q + 0.24 m`);
- an exact relief skip when it can't change the max;
- per-tile BVH node masks and contiguous ranges;
- heavy-tile splitting;
- heavy-first order.
Each gave 5–12%. Readiness is throughput-bound (workers 81–85% busy).
- **Two representation shortcuts failed:**
- L86: a lazy exact per-sample result cache. 24% hit rate, ~700 MB, +47% slower.
- L88: a tile-local 4-voxel trilinear cache of passage deltas, exact near the surface. Coarse-node
evaluations still paid full route cost, and carve-segment visits rose from 34.5M to 56.2M (FI), making it
+46% slower.
**Lesson:** each route evaluation is expensive in itself, and reorganising *where* we evaluate doesn't help
unless the evaluation gets cheaper.
- **Composer semantics that must keep their meaning:**
- the carve is `max(source, -roughSDF/Q)`;
- support restoration, applied after later overlapping bores;
- segment-local capsule clearance;
- mouth shaping and fades;
- the walkable tread floor;
- Catmull–Rom joins on short segments;
- wall/floor relief of ±0.21/0.20 m.
Reports: `Standalone/Research/WorldAssemblerM3/Linux81`–`Linux88/REPORT.md`; code in `ShapeWorldInternal.h`
(`ApplyResolvedStratePassageCarves` and its helpers).
## The owner's rules (unchanged)
- One seed gives the same world on every machine. Byte equality with older versions isn't required; a perf pass
may change the world slightly.
- Passages stay walkable arrival → departure (support, capsule, grade, ripple audits), and look natural.
- No visible quality regression.
- Target: **LOD0–2 ready in ~1 s** after cold arrival. The whole view is a bonus; continuous low-priority far
generation afterwards.
## Questions
1. **What makes one route evaluation expensive** (read the code path end to end)? Per sample: how many segments are
visited, what does each visit compute (closest point on a Catmull–Rom or linear segment, frames, radius
interpolation, relief noise, support capsule tests, later-bore checks)? Which parts are redundant per sample, per
segment, or per tile?
2. **Design a passage representation where per-sample cost is near-constant and small,** and the work is not merely
moved around. Candidates to evaluate (don't limit yourself):
- analytic primitives: polyline/capsule chains with precomputed per-segment frames and an arc-length table;
- relief defined in 2D (arc length, angle) with a cheap periodic or table noise;
- a precomputed narrow-band distance grid per passage, built once per world at a fixed resolution with
deterministic integer construction, plus its build cost;
- separating "is this sample within reach" (cheap) from "exact carve value" (expensive);
- vectorising across a z-run of samples sharing the same segment candidates.
For each: expected per-sample cost, build cost, memory, how exact (displacement vs today), determinism risk, and
effort.
3. **Rank by expected gain toward ~1 s,** with arithmetic from the numbers above. For the top choice, describe the
smallest prototype and the cheapest falsifier, so the implementer can prove it before building it fully.
## Output
At most ~3,000 words, **ranked recommendation first**, file:line evidence, labels [CODE]/[MEASURED]/[PRIOR ART]/
[INFERENCE].
@@ -0,0 +1,133 @@
# VoxelForge — design review: how do we get a cold whole view from ~2.5–4.6 s to ~1 s?
You're reviewing a voxel terrain generator's performance architecture. **Read and reason only.** Don't edit,
build, run tests, commit or stash. You may read any file in this repository and search the web.
We've spent about 15 rounds finding and fixing hot spots, and the gains are shrinking. We want fresh eyes on the
*approach*, not another micro-optimisation. Question our framing where it's wrong.
## The system (summary, so you don't have to rediscover it)
**VoxelForge** is a UE5 plugin for underground density-field terrain. Generation lives in a standalone C++
library, "Core" (`Standalone/Core/`), that Unreal calls through an adapter
(`Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`).
- **The field.** Density is a *function*, not stored data: negative = solid. Core voxels are 25 cm. Tiles hold
32³ voxels.
- **Clipmap.** LOD0–8; each LOD doubles the voxel step. A "whole view" is about 2,250–2,850 tiles across all LODs
around one camera.
- **Meshing.** Marching cubes per tile (about 35³ samples per LOD0/1 tile with borders), plus normals and skirts.
- **The world is a vertical stack of "strates".** Each is ceiling → content → bedrock, with its own generator:
- SurfaceWorld (SW): heightfield, native path;
- FloatingIslands (FI), TunnelNetwork (TN) and VerticalShafts (VS): the "legacy compiled-analytic" path.
- **The owner's test world** stacks four of them: SW 512 m, FI 240 m, TN 128 m, VS 128 m tall.
- **Passages.** Walkable, winding, natural-looking tunnels near (0,0) connect each strate to the next. They're
carved by a shared "composer" that also adds support treads, boundary bands and mouth shaping.
- **Multiplayer.** No geometry is replicated. Every peer regenerates from the seed, the layout and a diff of
player edits. That's why peer determinism is mandatory.
## The goal and constraints
- **Goal:** about 1 s cold whole view at any strate arrival on the owner's PC (Ryzen 5800X3D, 8 cores/16 threads,
7900 XTX). Streaming must stay smooth in VR (Quest 3) and at high travel speed.
- **Must hold:**
- peer determinism (same inputs → same world on every machine);
- no visible quality regression;
- passages stay walkable from arrival mouth to departure mouth (capsule, support and grade audits exist);
- no game-thread hitches.
- **Not required:** byte-identical output with earlier versions. The owner accepts that perf passes change the
world's shape slightly, as long as every peer gets the same result and quality holds. (We have tended to treat
hash equality as a gate anyway; it's a convenience, not a rule.)
- **On the shelf, not a speed excuse:** a disk mesh cache is an accepted feature, but perf is judged cold.
## Measurements (owner world, measured on an i9-12900H with 8 workers, headless unless stated)
**Whole view now** (it was 2.6 / 7.2 / 6.0 / 2.7 s a week ago):
| Strate | Whole view |
|---|---:|
| SW | 2.4 s |
| FI | 4.6 s |
| TN | 4.3 s |
| VS | 2.5 s |
**In Unreal (SW arrival):**
- Core work equals headless;
- initial set ready at 3.74 s vs headless 2.41 s, the extra being scheduling;
- walkable ring 0.4–1.8 s.
**Where the time goes:**
- **L73:** field evaluation inside grid fill is about 40 of FI's 46 worker-seconds (TN 33 of 39). LOD0/1 tiles
evaluate about 44k density samples each.
- **L74/L75:** sound whole-tile skip proofs now certify about 22–26% of tile visits.
- Ceiling: 46–82% of LOD0–2 tiles are truly uniform, and 90–97% of samples lie in uniform 8³ blocks.
- Dominant remaining veto: tiles crossing a strate boundary.
- Legacy sub-tile (block) proofs were unsound once (missed passage wall/floor relief), then sound but slower
than they saved.
- **L76, per legacy sample:**
- passage/support composer ≈ **1.05 µs**;
- primitive scan ≈ 0.19 µs (0.25 primitive distance evaluations per sample, binned);
- boundary band ≈ 0.06 µs;
- noise ≈ 0.01 µs.
**About 75% of field time is the passage composer, even far from passages.**
- Per-tile passage culling was tried and changed 612 density samples / 24 mesh tiles. It was rejected only for
breaking hash equality, which is a convenience, not a rule; nobody checked whether that change was harmless.
**Tried and rejected:**
- the native FI generator (fails passage walk audits on this plan);
- deeper Unreal queues (done: 4.78 → 3.74 s).
**Trap we fell into once:** a diagnostic hash mode silently disabled the skips and doubled the timings. Measure the
production path.
## What to reason about (open; don't limit yourself to these)
1. **Is "evaluate a density function at ~44k points per tile, then march cubes" the right shape** for this world?
Consider, for example:
- sparse/hierarchical evaluation (octree or brick refinement driven by interval or Lipschitz bounds, as in
libfive/Fidget/MPR-style implicit rendering);
- reusing samples across nested clipmap LODs;
- evaluating smooth large-scale terms coarsely and interpolating;
- narrow-band evaluation near the surface only.
2. **The passages are a few winding tubes.** Why does every sample pay 1 µs for them? What representation would
make "far from any passage" free and "near a passage" cheap? For example:
- bake each passage once at plan time into a sparse distance/brick structure;
- a spatial index of segments with a conservative distance early-out;
- a per-tile passage candidate list built once.
What would a correct per-tile culling need so that it doesn't change the result? And if it changes the result,
how should we judge whether that change is acceptable?
3. **Which work is needed for "ready in 1 s"?** Maybe the target should be redefined: walkable ring first,
progressive LOD refinement, far LODs from cheaper representations or impostors, fewer tiles. What do comparable
engines do (Voxel Plugin, Teardown, Dreams, No Man's Sky, Minecraft-style streaming, Nanite-era voxel work)?
4. **Hardware.** 8 CPU cores plus a 7900 XTX that's mostly idle during load. An earlier GPU port was closed, and
determinism across GPU vendors is a concern. Is there a deterministic way to use it, e.g. integer/fixed-point
evaluation or a CPU-authoritative plus GPU-display split? Is SIMD (AVX2) evaluation of the legacy field
realistic?
5. **Anything in our process or measurement** that hides the real bottleneck.
## How to work efficiently
- **Trust the numbers above; don't re-measure or re-derive them.** Spend your effort on thinking.
- **Read code only to check whether a specific idea fits.** Entry points:
- `CODEMAP.md` (navigation) and `ARCHITECTURE.md` §8.10 (performance invariants);
- `Standalone/Core/src/ShapeWorldInternal.h`: composer, `AnalyticDensityAt`, `ClassifyBoxDetailed`, passage
terms;
- `Standalone/Core/src/VoxelForgeCore.cpp`: tile generation and skips;
- reports in `Standalone/Research/WorldAssemblerM3/`: `Linux73`–`Linux76`, `Research36`, and R38–R40 notes
inside `Linux73/` and `Linux76/`.
- **Use the web** for prior art and techniques, and cite them.
- **Go broad first** (many approaches, one or two lines each), then deep on the best three.
## Output
At most ~2,500 words, plain language, **ranked recommendations first**. For each of the top three:
- what changes, and where in this codebase (file and function);
- expected gain, with arithmetic from the numbers above;
- what it risks: determinism, quality, walkability;
- effort;
- **a cheap experiment that would prove it wrong** before anyone builds it fully.
Then the shorter list of other ideas, then any questions only the owner can answer (e.g. what "ready" must mean).
Mark claims as [CODE] (read in this repo), [MEASURED] (from the numbers above), [PRIOR ART] (cited) or
[INFERENCE].
@@ -0,0 +1,130 @@
# Astra 1, pass 2 — whole-chain review (gpt-6-astra, effort medium, read-only), 2026-10-27
Claude verified: PackedDensityAt (ShapeWorldInternal.h ~5909) applies ApplyResolvedStrateComposition outside the L76 term scope; FScopedDensityQueryProfile times whole invocations crossing a 256-point boundary (batch-biased).
**Stage-by-stage cost map**
[MEASURED] The L73 stage timings below are historical, before the later skips. They describe where that run spent time; they cannot be subtracted from L76 term timings to produce a current residual. I also inspected the saved L76 results without executing generation.
[CODE] A major architectural detail: the “compiled-analytic” world **already contains precomputed packed signs and quantized surface-distance proxies**. Many lattice queries never enter `AnalyticDensityAt`.
| Stage | Work and frequency | Cost and necessity |
|---|---|---|
| **World/plan resolution** | [CODE] Headless loads the owner export, resolves/validates its layout, constructs a snapshot, then constructs `World`. Once per cold run. UE’s serialization entry is [`VoxelForgeCoreAdapter.cpp:2062`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:2062); headless startup begins at [`StreamBenchmarkHeadless.cpp:1659`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkHeadless.cpp:1659). | [MEASURED] Saved L76 FI owner-data loading takes about **25 ms**. No complete isolated resolution cost is supplied. [INFERENCE] Necessary, but not a plausible home for worker time measured inside tile-field queries. |
| **Shape compilation** | [CODE] `World::compile` builds primitives/bins, packed rock intervals, candidate bands, detailed distance proxies, sign-edge repairs, passage validation and far parents. Once for the base shape; additional pages when needed. [`VoxelForgeCore.cpp:1394`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1394), [`ShapeWorldInternal.h:2724`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2724). | [MEASURED] Saved L76 FI: **313 ms wall**, including approximately 48 ms shapes, 64 ms intervals/signs, 62 ms surface band and 119 ms far parents. [INFERENCE] Important to a one-second cold budget, but separate from the missing tile-field attribution. It already amortizes substantial base-field work. |
| **Tile-key selection** | [CODE] `desiredTiles` builds clipmap keys, vertical bounds and additional far surface columns. Once initially, then on streaming-set changes; about 2,250 tiles for FI/TN. [`VoxelForgeCore.cpp:1904`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1904). | [INFERENCE] Thousands of key/set operations, not millions of field evaluations. No measured isolated cost. Necessary selection; exact tile count remains a design choice. |
| **Whole-tile classification** | [CODE] Each assembly tile calls `ClassifyBoxDetailed`, including a one-step halo. It delegates to `ClassifyBoundsDetailed`, with conditional slicing and conservative vetoes. [`VoxelForgeCore.cpp:2377`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2377), [`ShapeWorldInternal.h:6385`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6385). | [MEASURED] L73: only **4/3 ms total FI/TN**, roughly **2/1 μs per tile**, but ineffective legacy skips then. These are not current classifier costs. [INFERENCE] Necessary only when avoided work pays for it; retain current profitable proofs. |
| **Density dispatch and base evaluation** | [CODE] Scalar lattice queries probe native/legacy-surface paths, then choose packed lookup or analytic evaluation. Batches have additional fast paths. [`ShapeWorldInternal.h:3475`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3475), [`:3602`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3602), [`:5909`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5909). | [MEASURED] L73’s approximately 52M calls and 39.9/33.2 field worker-seconds imply roughly **0.77/0.64 μs per counted point**, averaged over heterogeneous paths. [INFERENCE] Fetching/evaluating the base value is necessary; repeated dispatch is not inherently necessary. |
| **Composer** | [CODE] Packed and scalar analytic paths both call `ApplyResolvedStrateComposition`: cavern graph → boundary → passage/support → stamps → compatibility floor caps. [`ShapeWorldInternal.h:1993`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1993). Frequency depends on the actual route, not just the strate label. | [MEASURED] L76 measures approximately **6.5/5.5 worker-seconds** only within its eligible analytic scopes. [CODE] Its passage/support bucket also includes `ApplyResolvedCavernFloorCaps` at [`:2053`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2053). [INFERENCE] Composition is necessary where influential; universal invocation is not. |
| **Grid fill** | [CODE] One cached lattice per generated tile: typically **35³ = 42,875** points at LOD0/1, including halo. Scalar, X8 or Z-run traversal fills it. Storage is thread-local and reused. [`Standalone mesher:430`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:430), [`:957`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:957), [`:1296`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1296). | [MEASURED] L73: **36.14/29.37 s**, approximately **16.1/13.2 ms per visited tile**, including evaluation. [INFERENCE] Dense fill is avoidable in proven-uniform regions. This is the primary workload, not merely array writes. |
| **Marching cubes** | [CODE] Classifies cells using cached corners, computes geometry only for surface cells, interpolates edges and deduplicates vertices. LOD0 midpoint correction can issue extra unclamped queries. [`Mesher:1879`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1879), [`:1943`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1943). | [MEASURED] L73 MC aggregate: **40.58/32.63 s**, including grid work. Subtracting grid gives about **4.44/3.26 s**, not another 40 seconds. [INFERENCE] Necessary extraction, but replacing MC is not the first lever. |
| **Normals** | [CODE] Central differences normally read the existing grid. At Step 1, proxy-band fallback values trigger `DensityAtUnclamped`; optional gradient memoization avoids repeated corner gradients. [`Mesher:1654`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1654). | [MEASURED] L73: **0.306/0.246 s**, about **0.14/0.11 ms per visited tile**. [INFERENCE] Some repeated field queries exist, but six new field evaluations per vertex is not the general path. Normals cannot plausibly explain 30 seconds under these measurements. |
| **Skirts** | [CODE] Boundary-edge detection, extrusion and density probes prevent unsuitable skirt geometry. Work scales with boundary geometry, not tile volume. [`Mesher:2111`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:2111). | [MEASURED] L73: **1.079/1.091 s**, approximately **0.48/0.49 ms per visited tile**. [INFERENCE] Necessary for the current seam solution; bounded opportunity, not the missing dominant cost. |
| **Ground/cap refinement and far shells** | [CODE] Far stratum/connector repairs and native ground/cap refinement run under eligibility gates, with refinement at LOD≥4 and no overlapping edits. [`VoxelForgeCore.cpp:2792`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2792), [`:2875`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2875). | [MEASURED] L73’s combined “Other,” including these and assembly, is **2.86/2.78 s**. [INFERENCE] Required by the current far representation, but partly corrective work that a better far representation could avoid. Not an explanation for missing grid time. |
| **Payload** | [CODE] Core assembles material/vertex/index outputs; the headless wrapper converts vertices and moves indices/masks. Per output vertex/index, once per tile. [`VoxelForgeCore.cpp:2969`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2969), [`StreamingWorld.h:934`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamingWorld.h:934). | [MEASURED] The separately timed headless conversion is **12 ms total**, about **5 μs per tile**. [INFERENCE] This does not include all Core assembly or Unreal upload/collision costs. It is not a useful primary target. |
| **Unreal publication** | [CODE] Worker generation → prepared streams → result queue → budgeted game-thread component/upload/section work → asynchronous collision completion. [`VoxelWorld.cpp:13303`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13303), [`:9400`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:9400), [`:14010`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:14010), [`:14279`](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:14279). | [MEASURED] SW has **1.33 s additional readiness latency**. [INFERENCE] That is not 1.33 s of CPU publication work. Per-tile apply/cook costs are not established here. Essential to the target, but cannot explain headless field time. |
**Where the “unattributed 30 seconds” most plausibly went**
[CODE] **First: packed-field composition is outside L76’s measurement scope.** The relevant chain is:
`DensityAtLattice` → `PackedDensityAt` → `ApplyResolvedStrateComposition`.
`PackedDensityAt` reads `RockWords`, `CandidateWords` and possibly `SurfaceDistanceQ`, then applies composition. It never enters the `FScopedLegacyFieldTermScope` created later inside `AnalyticDensityAt` at [`ShapeWorldInternal.h:5967`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5967).
[CODE] Term timers immediately return when that scope is inactive: [`ShapeWorldInternal.h:2275`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2275). Thus an expensive passage query can be counted by the outer field profiler but entirely absent from L76’s passage timing.
[CODE] `DensityDispatchPathAt` returns “compiled-analytic/FloatingIslands” from the bound generator kind without distinguishing packed from analytic execution: [`ShapeWorldInternal.h:3455`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3455). That label is insufficient for performance attribution.
[CODE] There are further differences:
- X8 can return packed proxy values directly without invoking the scalar composer; other conditions make it fall back to scalar calls. [`ShapeWorldInternal.h:3675`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3675)
- FI LOD0 preferentially uses scalar vertical traversal. [`Mesher:1288`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1288)
- Z-run dispatch can fall back to scalar lattice queries, including packed queries. Its analytic term scope is reached only after those fallbacks. [`ShapeWorldInternal.h:11460`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:11460)
- Native/surface branches and non-FI/TN samples are excluded from the legacy-term population.
[INFERENCE] Therefore, the correction establishes that **6.5 seconds is measured passage cost within one subset**, not that total passage cost is only 15%. Packed composition is my leading explanation for substantial uninstrumented work. I cannot assign it 30 seconds from source alone.
[MEASURED] **Second: the residual mixes different generations of the workload.** Existing [L76 saved results](/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l76/term-profile-before/summary.json) contain:
| Same saved L76 run | FI | TN |
|---|---:|---:|
| Density point calls | 26.99M | 25.13M |
| Eligible legacy points | 6.01M | 5.56M |
| Reported field estimate | 18.19 worker-s | 15.49 worker-s |
| Tile-worker total | 33.80 worker-s | 28.56 worker-s |
| Whole-view wall | 4.621 s | 3.977 s |
[INFERENCE] The approximately 46M unseen calls belong to subtracting L76’s eligible count from L73’s pre-skip population. They are not an established current population. Likewise, `40−measured_terms` is not a coherent current residual.
[CODE] **Third: the aggregate field estimator can be biased by batching.** `FScopedDensityQueryProfile` samples when the point sequence crosses a multiple of 256, timing the entire invocation and recording its point count. The reducer pools sampled time/points and scales by all points. See [`ShapeWorldInternal.h:2355`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2355) and [`StreamBenchmarkPipeline.h:5611`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkPipeline.h:5611).
[INFERENCE] A batch of eight has approximately eight times a scalar invocation’s chance of being selected, and contributes eight sampled points when selected. Pooling both without inclusion-probability correction overrepresents batch points. If packed batches are cheap and scalar composition is expensive, the estimate can substantially understate field time. Periodic sampling also risks correlation with spatial traversal.
[MEASURED] This deserves attention: L76 FI LOD0 records **12.70 s grid fill but only 5.96 s estimated field evaluation**. Conversely, LOD3 estimates **3.04 s field time against 2.58 s total tile-worker time**. Those are diagnostic inconsistencies, not proof of where the discrepancy lies. [Saved log](/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l76/term-profile-before/strate1-w8.log:12880)
[INFERENCE] **Lower-ranked explanations:**
- **Dispatch/lookups:** real repeated work. Stratum lookup uses binary search and coordinate conversion, sometimes repeatedly per sample: [`ShapeWorldInternal.h:437`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:437). But attributing 30 seconds to this alone means roughly **0.65 μs per purported 46M calls**. That needs evidence; hidden composer execution is more concrete.
- **Allocation/cache misses:** possible contributors, but grid allocation is reused and packed access is direct indexing. There is no evidence of one heap allocation per ordinary density point.
- **Halos/neighbor duplication:** significant but bounded. `35³−33³ = 6,938`, or **16.2%** of a dense tile’s samples, is the gradient halo. Including shared faces, an ideal large same-LOD region could reduce `35³` toward `32³` unique samples per tile: approximately **23.6%** savings before edges and synchronization. This multiplies existing evaluation cost; it does not create a separate unmeasured stage.
- **Normals/midpoints/skirts:** issue additional calls, but their measured stage budgets exclude them as the dominant explanation. Cross-LOD duplication remains unquantified.
**Cheapest decisive measurement**
[INFERENCE] First take a sampling call-stack profile of the clean production run, focused on grid fill. It requires no new per-sample timers and should immediately distinguish packed composition from dispatch or memory stalls.
[INFERENCE] The next instrumentation change should be a **mutually exclusive dispatch census**, not another individual-term profiler. Count and sample time for packed-scalar, packed-X8, analytic-scalar, analytic-batch, native and legacy-surface routes, split by LOD and caller stage. Measure composition inside packed-scalar explicitly.
Use equal-probability invocation sampling with correct expansion, or stratify by route **and batch length**. Avoid nested double-counting; compare the resulting total against directly timed grid fill. Separate passage carving from compatibility floor caps.
[INFERENCE] This falsifies my leading explanation if packed-scalar composition occupies little time. Only then prioritize dispatch, cache misses or other overhead.
**Revised ranked recommendations**
**1. Establish complete attribution, then specialize the dominant lattice route.**
[INFERENCE] This replaces my unconditional “passages first” ranking. The first implementation round should produce a closed cost account and identify which route dominates LOD0/1.
If packed composition dominates, give each tile/brick a stable local composition program and an identity fast path. Extend `FScopedPassageTileQueries`, `PackedDensityAt` and `ApplyResolvedStrateComposition`. Hoist Z-only dispatch decisions into plane/run contexts. Before copying the X8 shortcut into scalar traversal, establish why their composition behavior differs and which semantics are required.
[INFERENCE] Effort: small for attribution, medium for specialization. Risks: incorrect support envelopes, changed composition order, scalar/batch disagreement. Preserve peer determinism and audit clearance, support and grade; old-version hashes remain an exactness diagnostic.
[MEASURED → INFERENCE] **Corrected passage arithmetic:** using your historical budget strictly as an illustration:
- FI, removing 80–90% of **6.5/46** worker-seconds: `4.6 × (1−0.8…0.9 × 6.5/46)` = **4.08–4.02 s**.
- TN, removing 80–90% of **5.5/39**: approximately **3.81–3.75 s**.
These replace my erroneous approximately two-second estimates. They are not forecasts because the operands span rounds. Using the contemporaneous saved L76 totals instead gives approximately **3.91–3.83 s FI**, **3.37–3.29 s TN**, under proportional scaling.
[INFERENCE] Total passage savings could be larger if the missing packed-route work is substantial. Measure it before assigning a percentage.
**2. Remove repeated composed sampling; retain MC.**
[INFERENCE] Keep the retained-proof/local-program recommendation, but build on the existing packed representation. Priorities:
- Retain successful slice/brick proofs rather than requiring a uniform whole-tile verdict.
- Reuse final composed lattice samples across neighboring tiles where inputs and semantics match.
- Evaluate exact surface endpoints and normal stencils; leave certified interiors implicit.
The relevant changes remain `ClassifyBoundsDetailed`, `World::generateTile` and the standalone mesher’s grid fill. Sample-cache keys must include evaluator version, edits and relevant resolution/context—not coordinates alone.
[INFERENCE] An alternative worth testing is applying composition once to a shared near-region lattice. Count its cold construction cost: moving the same work earlier is not a speedup. It wins through deduplication, localization or cheaper batching.
[INFERENCE] Effort: medium-to-large. Risks: stale edit dependencies, seams and false uniform proofs. Cheap falsifier: one representative region spanning adjacent tiles and a strate boundary; compare total construction plus meshing cost against independent tiles.
[INFERENCE] Withdraw the earlier combined one-second projection. At unchanged throughput, FI’s current 4.6 seconds requires approximately **78% critical-path reduction**. Border sharing alone cannot supply that; broad sampling elimination still has to prove its economics.
**3. Preserve a separate arrival deadline and reduce end-to-end latency.**
[INFERENCE] Keep dependency-aware scheduling through collision publication, rather than merely increasing queue depth. The existing near-priority and hot-swap mechanisms already provide the foundation.
Shape compilation’s approximately 0.31 seconds matters once generation approaches one second. Far repair/refinement becomes material too. Optimize these after the dominant grid route is understood; do not hide their cost outside the cold timer.
[MEASURED → INFERENCE] SW’s scheduling gap remains a possible **0–1.33-second** latency opportunity, not a guaranteed CPU saving. Scheduling cannot remove required whole-view work.
[INFERENCE] Effort: medium. Risks: starving far coverage or publishing visuals before safe collision. Cheap falsifier: an arrival-only admission experiment plus prerequisite/publication tracing. If its uncontended critical path exceeds one second, scheduling alone cannot meet arrival readiness.
[INFERENCE] GPU porting, SIMD, native-generator replacement and a new mesher stay below these three. The newly visible distinction is between **cheap cached base density and potentially expensive repeated composition**, not simply “52M analytic evaluations.”
Read-only inspection completed; no edits, builds, tests, commits or stashes.
@@ -0,0 +1,161 @@
# Astra 1, pass 3 — passage evaluation design (gpt-6-astra, medium, read-only), 2026-10-28
**[INFERENCE] Ranked recommendation: replace recursive route queries with a compiled spatial passage evaluator—not another cache of evaluated densities.**
1. **Compile short lists of prepared carve, tread and clearance primitives into spatial bricks.** Evaluate those lists directly, retaining ordered support restoration and explicit mouth exceptions.
2. **Add a compact relief representation**, preferably sampled from today’s relief on the passage surface. Treat this as part of recommendation 1’s performance budget, not a sufficient standalone fix.
3. **Consider a sparse distance grid only if built directly from prepared primitives**, without calling today’s composer at every grid node. Its cold construction cost makes it a less attractive first prototype.
[MEASURED → INFERENCE] The target requires a substantial replacement:
\[
T_{\text{FI}}\approx0.81+1.46r+B,\qquad
T_{\text{TN}}\approx0.85+1.40r+B
\]
Here \(r\) is the fraction of passage cost remaining and \(B\) is added cold critical-path construction time. This is an ablation-based planning model, not a guaranteed linear prediction.
| Passage cost remaining | FI, before added construction | TN |
|---|---:|---:|
| 30% | 1.25 s | 1.27 s |
| 15% | 1.03 s | 1.06 s |
| 10% | 0.96 s | 0.99 s |
| 5% | 0.88 s | 0.92 s |
[INFERENCE] **Tenfold passage acceleration barely reaches one second before construction.** With 30 ms additional construction, TN needs approximately **11.7×**. Another 5–12% trim cannot close this gap.
**What one evaluation actually does**
[CODE] The entry point is [`ApplyResolvedStratePassageCarves:1115`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1115). Its cost is a sequence of different geometric questions:
1. **Reach screening.** Check tile flags, incoming density, passage Z range and floor-profile ranges. Solid samples can invoke `PassageMayAffectDensity` before entering the main passage loop. Inside that loop, another reach test may occur.
2. **Column/support preparation.** Scan eligible void crossings and possibly evaluate column distance.
3. **Nearest carve feature.** Search four ranges: descending body, upper endpoint, lower-mouth neighborhood and departure tail.
4. **Relief and mouth shaping.** Compute wall orientation, route-relative distances, entrance projection, relief and fades.
5. **Tread selection.** Find the floor-owning segment separately from the nearest carve segment; evaluate up to three neighboring support segments.
6. **Clearance and composition.** Check the passage’s player capsules, later bores and other passages’ capsules; apply, cancel or defer support. Finally restore mouth support and deferred support.
[CODE] **A carve segment visit is not an iterative closest-point solve on Catmull–Rom.** It computes endpoint differences, squared length, a clamped projection onto the straight chord, then optionally evaluates a Catmull–Rom position at that same parameter. Radius remains interpolated from the endpoints. Distance requires a deterministic square root, followed by stable tie-breaking. [`ShapeWorldInternal.h:1497`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1497)
[INFERENCE] Consequently, replacing this with a mathematically accurate spline-distance solver would likely increase cost and change the field. Compiling the existing formula—or approximating it deliberately—is the appropriate comparison.
[CODE] **Traversal itself is substantial.** Each nearest-node bound computes distance to an AABB using a square root, subtracts maximum radius, checks range membership and visits children. Potential-within queries perform a separate recursive search. Tile masks and contiguous ranges already remove some overhead; they do not eliminate per-sample traversal. [`:5590`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5590), [`:5676`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5676)
[MEASURED → INFERENCE] L88’s approximately 34.5M/33.1M carve visits divided by approximately 14.06M/12.43M passage iterations gives about **2.45/2.66 carve visits per iteration**, assuming those census populations align. This is not visits per density query or per successful route evaluation: iterations include rejected passages, and the visit count excludes internal BVH nodes and separate capsule searches. **The problem is not necessarily hundreds of evaluated segments; it is expensive repeated search around relatively few winners.**
[CODE] **Support is a different geometry query.** It uses horizontal projection, extended tread segments, distinct smoothing rules, native-density-dependent mouth offsets and floor-depth gates. Its floor-profile search deliberately avoids choosing a neighboring helix coil merely because that coil supplies the nearest wall. [`:1833`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1833), [`:2127`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2127)
[CODE] Capsule checks project horizontally and test a vertical capsule with 0.34 m radius and 0.54 m axial half-length. Later-bore checks already have precomputed overlap candidates, but can fall back to BVH searches. Deferred support is restored after the passage loop. [`:5708`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5708), [`:5810`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5810), [`:2163`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2163)
[CODE] **Relief still costs work on cache hits.** Each of three octaves computes lattice coordinates, a cache hash, fades and seven interpolations. A miss additionally generates eight corner values. The cache has 64 direct-mapped entries. Wall relief uses wavelengths 43/29/19 and weights 0.50/0.35/0.15. [`VoxelForgeFieldShared.h:260`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeFieldShared.h:260), [`:394`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeFieldShared.h:394)
[CODE] Mouth work additionally evaluates noise at projected route positions. Some calculations happen before determining whether the sample belongs to the mouth’s small affected region. [`ShapeWorldInternal.h:1683`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1683)
[INFERENCE] The reusable information is:
- **Per segment:** chord vectors, inverse squared lengths, lengths, radius slopes, cubic coefficients, support flags, overlap extension and prefix/suffix arc lengths.
- **Per spatial brick:** possible carve winners, floor owners, clearance capsules, column features and ordered passage dependencies.
- **Per sample:** projections, distances, source-dependent support blending and final min/max operations.
- **Across a Z-run:** candidate lists and XY projections; not generally the winning segment or final support decision.
[CODE] `BuildPassageComposerConstants` currently prepares passage-level metadata, not this complete segment representation. [`:5147`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5147)
**1. Compile a spatial passage program**
[INFERENCE] Build immutable sparse bricks containing contiguous references to prepared features. Use separate lists for **carve, tread, clearance and mouth/column operations**. A single nearest-segment identifier cannot represent their different ownership rules.
For each brick:
- Eliminate segments that cannot win its carve query, using conservative lower/upper distance bounds.
- Retain every potentially relevant overlapping coil.
- Store floor candidates independently.
- Store later-bore and other-capsule dependencies in stable passage order.
- Subdivide crowded bricks or use an explicit slow fallback; never silently cap candidates.
[INFERENCE] The common query becomes one brick lookup, typically a few direct projections/distances, one relief lookup and a small ordered composition sequence. No reach-BVH traversal followed by nearest-BVH traversal followed by clearance-BVH traversal.
**Important limitation:** “two to four candidates” is a design target to falsify, not something the current route guarantees. Nearby coils and short, densely authored segments may produce much larger lists.
[INFERENCE] Prepare the existing chord/Catmull formulas first. This avoids multiplying segment count through premature tessellation. Subsequently merge or approximate pieces only where a measured error budget permits. Precomputed coefficients and reciprocals may change floating-point rounding; version the evaluator and enforce the same arithmetic across peers.
[PRIOR ART] If route-coordinate relief needs frames, compute stable frames once. Rotation-minimizing frames are established for swept surfaces; ordinary Frenet frames can become discontinuous at inflections. **Keep the walkable floor gravity-referenced**, regardless of the wall-texture frame. [Wang et al., rotation-minimizing frames](https://www.microsoft.com/en-us/research/publication/computation-rotation-minimizing-frames/)
[INFERENCE] **Do not discard the density-dependent reach rule.** A narrow geometric envelope alone is insufficient when `max(source, -roughSDF/Q)` can change deeply negative source values outside the visible tube. Outside a brick’s detailed region, return identity only with a valid source-aware proof. Otherwise use a wider prepared candidate set or an explicit fallback. Count that fallback in the speed budget.
[INFERENCE] Cost model:
- **Query:** target roughly **0.1–0.3 μs** for an ordinary active query with 2–4 prepared candidates, excluding rare complex exceptions. This is an engineering target, not measured performance.
- **Build:** \(O(S+R)\) segment preparation/reference emission, plus candidate-certification work; \(S\) segments and \(R\) brick references. Target **under 30 ms added cold critical path** initially.
- **Memory:** approximately 128–256 bytes per prepared segment, plus roughly `16 + 4K` bytes per simple brick list, before multiple semantic lists and directory overhead. Thousands of segments should require megabytes, not L86’s hundreds of megabytes.
- **Exactness:** prepared original formulas can preserve geometry closely; segment simplification has no automatic displacement guarantee. Keep mouth/support equations source-dependent.
- **Effort:** medium-to-large; the candidate compiler and semantic separation are the substantial work.
[INFERENCE] Leaving the entire old support phase untouched is unlikely to suffice: **14% of the 1.4-second passage contribution is approximately 0.20 seconds**, already exceeding TN’s available 0.15-second margin. Preserve support’s meaning, but accelerate its feature selection too.
**2. Replace repeated 3D relief evaluation with a small surface field**
[INFERENCE] Use coordinates along the route and around its wall, with periodic angular indexing and continuous frames. A table can combine all three octaves into one lookup. Avoid evaluating general-purpose `atan2` unnecessarily; a specified octant-based angular mapping is an option, but its distortion must be checked.
Prefer initially **sampling today’s 3D relief at the reference wall surface** rather than inventing unrelated 2D noise. This retains much of its spatial appearance. Extend the value radially only over a controlled band; preserve the floor-alignment weights and separate mouth-floor treatment.
[INFERENCE] Costs and risks:
- **Query:** target **20–80 ns** for coordinate mapping plus a warm four-value lookup; cold memory or frame interpolation can exceed this.
- **Build/memory:** illustrative 1 km of route, 0.25 m longitudinal spacing, 64 angular samples and int16 values: **256k samples, approximately 0.5 MB**. Build requires those relief evaluations once, not full composer evaluations.
- **Exactness:** independently regenerated noise can differ by **0.42 m in wall offset** between opposite extrema despite retaining ±0.21 m amplitude. Equal amplitude does not establish equal quality. Surface sampling reduces this risk but does not prove a displacement bound.
- **Determinism:** fixed integer coordinates, specified interpolation/rounding, periodic endpoints and stable frame construction.
- **Effort:** medium; seam handling, frame continuity and floor/mouth validation dominate.
[MEASURED → INFERENCE] Even removing **all** the measured 21–24% relief share saves only roughly **0.3 seconds**. An 80% relief reduction alone predicts roughly **2.0 seconds**, not one second. It belongs in the combined representation.
[INFERENCE] Cheapest falsifier: evaluate table interpolation error and normal/ripple changes on one curved wall, one mouth and one self-overlapping coil. Reject a table that needs near-original per-sample 3D noise to hide seams or preserve appearance.
**3. Sparse narrow-band distance grids**
[INFERENCE] A grid gives genuinely near-constant queries, but only if its construction avoids L88’s failure: **do not fill it by invoking the existing full route evaluator per node**.
Rasterize prepared local primitives into owned bricks, reduce candidates deterministically, and construct the band from those primitives. Store separate carve and support/clearance information where needed. A baked final delta cannot represent all source-density-dependent mouth restoration.
[PRIOR ART] OpenVDB’s narrow-band representation stores active distances near the surface and constant signed values elsewhere. That is useful representation prior art, but its clamped exterior is not automatically compatible with your composer. [OpenVDB representation](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/doc/doc.txt)
[INFERENCE] For route length \(L\), radius \(r\), band half-width \(w\) and spacing \(h\), band sample count is approximately:
\[
N \approx 4\pi r w L/h^3.
\]
Illustratively, \(L=1{,}000\) m, \(r=3\) m, \(w=0.75\) m:
- **25 cm spacing:** approximately 1.8M samples, **3.6 MB per int16 channel**.
- **12.5 cm spacing:** approximately 14.5M samples, **29 MB per channel**.
Overlapping passages, mouth volumes, brick padding and multiple channels add memory.
[INFERENCE] **Query:** roughly **0.05–0.25 μs per interpolated channel** as a target, with locality decisive. **Build:** millions of local distance operations; even 0.1–0.3 μs per sample means 0.18–0.54 CPU-seconds for the coarser example, before indexing, extra channels or allocation. Parallel wall time must be measured; it is not free.
[INFERENCE] Grid spacing is not a displacement bound. At 25 cm, narrow tread/clearance transitions need analytic treatment or finer data. Integer storage does not ensure deterministic construction: integer distance/projection arithmetic, tie-breaking, reduction order and interpolation must also be specified.
[INFERENCE] Effort: large. This is attractive only if direct construction plus all arrival queries beats the compiled analytic representation. A tiny benchmark of grid lookups alone proves nothing.
**Other candidates**
- **[INFERENCE] Cheap reach versus exact value:** retain the distinction, but answer reach from brick metadata or squared primitive bounds. The current distinction already exists; repeating its recursive BVH search is the expense.
- **[INFERENCE] SIMD Z-runs:** useful after candidate compilation. XY projection is reusable for tread/capsule queries, while 3D carve projection varies affinely with Z before clamping. Expect amortized lookup and arithmetic benefits, not eightfold speedup. Memory adds only scratch arrays; construction is unchanged. Deterministic lane arithmetic and stable winner selection are required. Medium effort.
- **[INFERENCE] Pure capsule-chain replacement:** cheap per primitive, but tessellation can increase candidate count and introduce curvature/radius artifacts. Preserve short-join shape with bounded approximation; do not equate a visually smooth centerline with a walkable tread.
**Smallest prototype and decisive falsifier**
[INFERENCE] Implement one prepared passage evaluator for the **FI/TN junction passage**, including an overlapping coil and tread—not a straight isolated tube.
1. Compile segment constants and brick candidate lists; retain existing geometry formulas and relief.
2. Run the complete carve/support/clearance sequence from those lists.
3. Measure construction, query time, candidate-count distribution and fallback time separately.
4. Add the relief table only after direct evaluation beats the old traversal.
[INFERENCE] The cheapest first falsifier is the **candidate compiler census**: if commonly sampled bricks still need many segments or fallback retains more than approximately 10% of original passage time, the proposed common path cannot meet the target even if nearly free.
[INFERENCE] Then replay representative samples with their incoming densities and original spatial order. Include misses, mouths, support and overlapping bores; exclude benchmark-only allocations. A promising kernel must finally survive cold arrival timing including construction, peer comparisons, capsule/support/grade/ripple audits and visual inspection.
[INFERENCE] Budget acceptance around **90–95% total passage-cost removal**, not an attractive isolated kernel speedup. The measured ceiling makes approximately one second plausible, but only if the new representation replaces the search-heavy semantic pipeline broadly enough.
Read-only review; no files changed or generation/tests run.
@@ -0,0 +1,137 @@
# Astra 1 — design review (gpt-6-astra, effort low, read-only), 2026-10-27
Brief: see BRIEF.md. Note from Claude: the brief's "~75% of field time is the passage composer" was WRONG — L76's term profiler covered ~6.0M of ~52M density calls per view (FI); measured passage cost is ~6.5 of ~40 worker-s (~15%). Astra flagged the scope mismatch itself. Its gain arithmetic in rec. 1 inherits the wrong share.
**[INFERENCE] Ranked recommendations:**
1. **Compile passages into spatially local operations.** Make most samples bypass the composer entirely; simplify the remaining work.
2. **Replace repeated block classification with a retained hierarchy of proofs and local field programs.** Keep marching cubes; stop densely evaluating regions that cannot produce geometry.
3. **Give arrival readiness its own deadline-driven pipeline.** Preserve whole-view completion as a separate benchmark; scheduling improvements alone cannot meet it.
**[INFERENCE]** These are complementary. Passage localization is the strongest first investment, but does not plausibly deliver one second alone. A wholesale mesher replacement, GPU rewrite, or native-generator migration is premature.
### 1. Make passage influence local by construction
**[CODE]** There is already a spatial index: `BuildPassageFeatureTileList` queries passage feature bounds. `FScopedPassageTileQueries` turns route candidates into per-passage flags, while retaining segment lists for mouth support. Nevertheless, `ApplyResolvedStratePassageCarves` still iterates all passages and scans crossings. After finding a route distance, it calculates relief and mouth-related terms. See [ShapeWorldInternal.h](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:989).
**[INFERENCE] What changes:** compile a small, immutable passage program for each spatial region:
- Empty region: no passage invocation.
- Route region: ordered candidate segments, precomputed geometry and distances along the route.
- Mouth/support region: explicit support, transition-fade and later-bore dependencies.
- Overlapping features: retain the original composition order.
Use `BuildPassageComposerConstants`, `BuildPassageFeatureTileList`, `FScopedPassageTileQueries`, and `ApplyResolvedStratePassageCarves`; connect the result through `World::generateTile` in [VoxelForgeCore.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1997). This is stronger than filtering one loop: the region program should remove entire categories of work.
**[INFERENCE] Exact culling requires an identity proof**, not merely “the tube does not intersect this tile.” For every omitted operation, prove that it leaves the incoming density unchanged over the query domain. Include:
- Spline extrema, variable radius, wall/floor relief and smooth-blend reach.
- Mouth shaping, support treads, columns, boundary fade and dependencies on other passages.
- All sample positions used by normals, borders and skirts.
- Deterministic ordering and tie-breaking among retained features.
A feature can leave density signs unchanged while altering marching-cubes interpolation or normals. Conversely, changing far-away density magnitudes may be harmless. These are different correctness claims.
**[CODE]** L75 already corrected bounds for passage relief and spline extrema. The composer also conditionally restores support and defers support composition. Therefore, baking one distance to the nearest tube would not reproduce its semantics. [L75 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux75/REPORT.md:22)
**[INFERENCE]** If exact influence is awkward or effectively nonlocal, introduce a versioned, compact-support passage operator: identical in the protected walkable corridor and mouth regions, smoothly becoming identity outside an explicit envelope. That is a legitimate world-generation change under your constraints.
**[MEASURED → INFERENCE] Expected gain:** passage work is approximately `0.75 × 40 = 30` of FI’s 46 worker-seconds, and `0.75 × 33 = 24.75` of TN’s 39. Removing 80–90% gives proportional wall-time scenarios:
| View | 80% passage reduction | 90% passage reduction |
|---|---:|---:|
| FI | `4.6 × (46−24)/46 = 2.20 s` | `4.6 × (46−27)/46 = 1.90 s` |
| TN | `4.3 × (39−19.8)/39 = 2.12 s` | `4.3 × (39−22.275)/39 = 1.84 s` |
These assume critical-path time follows work reduction; they are not predictions for the Ryzen. Even eliminating passage cost entirely leaves approximately **1.6 seconds** in both views.
**[INFERENCE] Risks and effort:** medium effort for regional programs; larger for a new passage representation. Stable candidate ordering and versioned arithmetic protect determinism. Underestimated envelopes threaten clearance and support; overly generous envelopes merely lose speed. Run capsule, grade and support audits across entire passages, including landings and seams.
**[INFERENCE] Cheap falsifier:** resurrect the rejected culling candidate as an experiment. First inspect all 612 changed samples and 24 changed mesh tiles: location, zero crossings, surface displacement, normals, support and audit outcomes. Then time the production path including candidate construction. Reject this implementation if conservative lists remain large or composer savings are small. Hash differences alone are neither acceptance nor rejection.
### 2. Retain spatial proofs and simplify the field inside each region
**[INFERENCE]** A continuous density function remains a good world definition. Evaluating its complete program at every lattice point is the questionable implementation. Marching cubes is not currently the dominant problem.
**[CODE]** `ClassifyBoxDetailed` delegates to `ClassifyBoundsDetailed`. The latter already performs conditional strate slicing, but combines slices into a whole-box verdict and abandons the skip when a slice is unknown or has a different sign. L75’s legacy block certificates became audit-only after timing regressions. Simply enabling more block checks repeats a failed approach. [Classifier](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6385), [L75 final disposition](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux75/REPORT.md:64)
**[INFERENCE] What changes:** introduce a compact region tree or brick table whose entries retain:
- A solid/air certificate, or an unresolved range.
- Relevant primitive and passage candidates.
- Terms proven inactive or dominated.
- Child regions requiring further evaluation.
Split first at known strate and transition-band boundaries, then refine unresolved regions spatially. Keep successful proofs even if a neighboring slice fails. Share candidate filtering and bounds down the hierarchy; do not repeatedly invoke the full classifier from scratch.
Start tile-local and retain the regular marching-cubes lattice. Extend `ClassifyBoundsDetailed`, the evaluation feeding `AnalyticDensityAt`, `World::generateTile`, and the grid/proof path in `UVoxelMarchingCubesMesher::GenerateMesh` in [VoxelMarchingCubesMesher.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:102).
**[PRIOR ART]** Fidget combines interval evaluation with regional expression simplification: even unresolved regions can use shorter programs. That second benefit matters here as much as empty-space skipping. It also supports bulk SIMD evaluation. [Fidget’s author documentation](https://www.mattkeeter.com/projects/fidget/)
**[INFERENCE]** Do not assume the composed density is a signed distance field with Lipschitz constant one. Warps, blends and relief require their actual bounds. Uniform corners do not prove a uniform interior.
Similarly, do not fill certified interiors with arbitrary constants and then use those constants at surface edges or in normal stencils. Evaluate the exact endpoint and halo values wherever geometry or gradients consume them. A narrow band must be *discovered conservatively*, not guessed from sparse signs.
**[MEASURED → INFERENCE] Expected gain:** the 90–97% uniform-block sample census is an opportunity ceiling, not a realizable skip rate. A useful experiment target is **50–70% reduction of remaining field work, net of classification cost**.
After a 90% passage reduction, FI has approximately 13 field-work units plus 6 others. Reducing those 13 by 50–70% gives:
`4.6 × [6 + 13 × (0.5 to 0.3)] / 46 = 1.25 to 0.99 s`.
TN similarly gives approximately **1.25–1.02 seconds**. Thus the combination approaches the headless target, with little budget left for Unreal overhead. Do not multiply independent headline speedups: both techniques remove overlapping work.
**[INFERENCE] Risks and effort:** medium-to-large effort, substantially less than adaptive meshing with new topology. Incorrect proofs can silently delete geometry; edits must invalidate affected certificates, including their dependency halos. Quality and walkability can remain unchanged if surface samples and stencils remain exact. Unknown bounds must fall back.
**[INFERENCE] Cheap falsifier:** implement one tile-local hierarchy for representative boundary-crossing and passage-adjacent tiles, retaining the existing mesher. Compare total classification-plus-fill cost against dense evaluation, after passage localization. Reject expansion if net field savings fall well below 50%, or bookkeeping and conservative halos consume the benefit. The prior slow block experiment makes this an essential early gate.
### 3. Schedule for an explicit arrival deadline
**[MEASURED]** SW’s Unreal overhead is `3.74−2.41 = 1.33 s`; walkable readiness ranges from 0.4 to 1.8 seconds.
**[INFERENCE]** Keep two separate commitments:
- **Arrival ready:** safe collision and sufficient visible terrain for immediate movement.
- **Whole view complete:** the full requested set at final quality.
If the owner insists both happen within one second, say so explicitly. Progressive refinement cannot be counted as meeting the second commitment.
**[CODE]** §8.10 already specifies occupied/support-tile priority, heading priority, dynamic near reserve, independent region admission, cancellation, bounded result backlog and hot-swap. “Make nearby tiles higher priority” is therefore not a new recommendation. [Performance invariants](/home/jahni/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:572)
**[INFERENCE] What changes:** make readiness a dependency graph with deadlines: required region compilation → field/mesh → collision preparation → bounded publication. Prioritize prerequisites of the arrival set, not just its tile jobs. Reserve capacity through publication, and schedule travel-ahead work by time-to-arrival.
Inspect and change `AVoxelWorld::UpdateChunksAroundPosition`, `LoadTile`, and `ProcessPendingChunks` in [VoxelWorld.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:9400). Preserve the existing epoch, cancellation and collision-handoff rules. Refinement should retain covering geometry until replacements are ready.
**[PRIOR ART]** Voxel Plugin separates collision/navmesh demand through invokers. Geometry clipmaps use incremental updates, coarse-to-fine processing and continuity between levels. These support separate readiness domains, not a universal one-second guarantee. [Voxel Plugin](https://docs.voxelplugin.com/2.0p6/knowledgebase/blueprints/collision-navmesh-and-invokers), [Geometry clipmaps](https://developer.nvidia.com/gpugems/gpugems2/part-i-geometric-complexity/chapter-2-terrain-rendering-using-gpu-based-geometry)
**[MEASURED → INFERENCE] Expected gain:** removing half the observed scheduling gap saves about **0.67 seconds**; removing all of it still leaves SW at 2.41 seconds before Core changes. Improving a 1.8-second walkable arrival to one second requires eliminating or deferring 0.8 seconds from that critical path.
**[INFERENCE] Risks and effort:** small-to-medium for dependency scheduling; larger for new far representations. Timing must never change generation inputs. Coarse collision can violate walkability; coarse visuals can pop, particularly in VR. Preserve authoritative near collision and accept temporary visual substitutes only if they satisfy the owner’s quality criterion.
**[INFERENCE] Cheap falsifier:** first replay existing job timings against the dependency graph, then try an arrival-only admission experiment. If prerequisites plus bounded publication already exceed one second without competing work, scheduling cannot solve arrival latency. If the coarse cover visibly degrades the arrival, it cannot redefine “ready.”
**Other approaches, in priority order**
- **[INFERENCE] Bake passage geometry, not necessarily final density.** Precompute spline coefficients, arc lengths, frames and conservative segment bounds first. Sparse sampled bricks are a later option; retain separate carve/support operands and conditions. Count baking in cold latency. Interpolated distance alone can erase treads or alter mouths.
- **[INFERENCE] SIMD after localization.** AVX2 evaluation of batches sharing a local program is realistic; divergent all-passage loops are a poor starting point. Eight lanes do not imply eightfold speedup. Specify arithmetic and reduction order across scalar/vector paths.
- **[INFERENCE] Sample sharing.** Share same-LOD borders first; cross-LOD reuse requires identical coordinates *and field semantics*. Nested lattices overlap, but clipmap shells need not cover the same volume. Compare synchronization cost against duplicate evaluation before building a global cache.
- **[INFERENCE] Coarse smooth terms.** Interpolate bounded macro terms while evaluating fine relief near surfaces. Density error does not imply equal position error where gradients are weak. L76’s 0.01-µs noise row is too small to justify making noise approximation the main project.
- **[INFERENCE] GPU.** A specified integer/fixed-point evaluator can be deterministic: define rounding, overflow, square roots, interpolation and stable emission order. Quantizing floating-point results afterward is insufficient. CPU-authoritative collision plus GPU display is easier, provided visual disagreement is bounded and display results never affect gameplay. GPU readbacks and VR contention can erase gains.
- **[PRIOR ART → INFERENCE]** HLSL `precise` controls arithmetic refactoring; it is not a complete cross-vendor world-generation specification. Prototype one bounded integer passage kernel before reopening a full port. [Microsoft documentation](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/precise)
- **[PRIOR ART → INFERENCE] Alternative renderers.** Dreams’ published architecture evaluates CSG into multiresolution point clouds; Teardown uses a voxel renderer without polygons. These demonstrate representation choices, not transferable cold-generation timings. Nanite addresses rendering downstream of field generation. [Dreams presentation](https://www.advances.realtimerendering.com/s2015/index.html), [Teardown FAQ](https://www.teardowngame.com/faq.html)
- **[PRIOR ART → INFERENCE] Continuous streaming.** No Man’s Sky’s architecture is explicitly organized around continuous generation. That supports travel-ahead planning, but does not prove cold teleport latency. Disk caching likewise remains useful without counting toward this target. [Hello Games’ GDC talk](https://www.youtube.com/watch?v=sCRzxEEcO2Y)
**Process changes that matter**
**[INFERENCE]** Separate three gates: peer determinism for the *new version*; gameplay/visual acceptance against the previous version; and performance on the production path. Old-version hash identity should only gate changes advertised as exact.
**[CODE]** L76’s term profiler covers selected legacy evaluations; its passage total is about 6.3 worker-seconds, whereas L73 reports broader grid-field totals. Those scopes must not be silently equated. The arithmetic above uses your supplied aggregate attribution as a scenario, not reconciled absolute profiling data. [L76 profiler scope](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux76/REPORT.md:5)
**[INFERENCE]** Report cold-start boundaries, critical-path stages, worker occupancy, publication latency and worst frame time alongside whole-view time. Include plan compilation and fresh in-memory structures. Validate the winning prototype on the 5800X3D; no reliable conversion follows from the laptop measurements alone.
**Questions only the owner can settle**
- Must every final-quality tile exist at one second, or can safe arrival precede complete refinement?
- What travel speed, teleport distance and VR frame budget must streaming sustain?
- Does “cold” include world planning, and must supported peers reproduce density bits, mesh bits, or an explicitly defined gameplay representation?
Read-only review completed; no edits, builds, tests, commits or stashes.
@@ -0,0 +1,64 @@
# L100 — swept-tube boundary regression
Branch: `wip/l99-swept-tube` (`fa6363b`, unchanged HEAD). L100 source stamp: `7224bf6e668d06ed71a92f0857820e3ae2b0eca40574a491331ab5979a88f698`.
## Cause and fix
**Measured:** the 1–2 midpoint query at `(-8.5, 12.5, -741.989990 m)` has band density `−0.499936938`; stamps and caverns are absent, and the cap leaves that value unchanged. The direct route query finds segment 8 at SDF `−1.83728031 m`, with reach, floor, and combined weights all 1, and returns `+7.35348845`. The bounded sweep leaves `−0.499936938` at this point. At the nearby exact `z=−742 m` lattice point both paths carve; at `z=−741.989990 m` the sweep has no exact cache entry. The strate field itself is `−1032.04004` and is masked by the boundary band. This identifies the band as the flat sheet; the 0.5 m blend, stamps, and caps are not the cause.
**Code-path conclusion:** L99 treated a missing exact-grid sweep sample as proof that no route affected the query and skipped route evaluation. Off-grid seam/field queries therefore exposed the band inside the tube. L100 evaluates the exact route on an in-bound cache miss. A conservative route-feature bound still skips misses outside candidate extents; mouth and column supports keep their separate paths. Composition stays `graph → bands → passage → stamps → floor caps`, so the tunnel cuts bands while authored stamps and landing caps retain precedence. This adds the missing ownership without restoring the old full route scan.
## Comparison metric
The comparison now counts added and removed edge crossings per tile, alongside moved shared crossings, and reports added/removed non-skirt triangles. The old mean/max displacement summarized only crossings shared by both meshes; a missing or new crossing had no displacement to average. `changed_mesh` could flag the tile but did not say whether surface appeared or disappeared.
Final direct-route versus bounded-sweep audit (LOD 0–2 near sets):
| Strate | Tiles | Added / removed crossings | Added / removed triangles | Moved crossings | Mean / max displacement (cm) | Pairing failures | Audit |
|---|---:|---:|---:|---:|---:|---:|---|
| SW | 1,408/1,408 | 0 / 0 | 0 / 0 | 9,214 | 0.0180 / 20.1114 | 5 | exit 14 |
| FI | 1,216/1,216 | 0 / 0 | 56 / 0 | 10,238 | 0.0185 / 19.7997 | 37 | exit 14 |
| TN | 1,344/1,344 | 0 / 0 | 0 / 0 | 11,370 | 0.0387 / 21.7442 | 41 | exit 14 |
| VS | 1,344/1,344 | 0 / 0 | 0 / 0 | 5,087 | 0.0178 / 25.8798 | 4 | exit 14 |
All four comparison commands hit the legacy strict displacement/normal acceptance gate (1 cm maximum displacement, 2° maximum normal change, and no normal-pairing failures); they completed and found no added/removed crossing edges. Exit 14 is that diagnostic result, not a build or timeout failure. FI has 56 net added triangles.
## Readiness and timing
Fast tier: **PASS**, 274 blocking checks across 80 runs, including all resolved-passage and adjacent-interface walkability audits. Evaluator version 216 hashes are re-pinned. Determinism: 24/24 Clang/GCC runs at W1/W8/W12 and 20/20 cross-worker/compiler comparisons match exactly for all four strates.
L80 timings are medians of three measured repetitions in rotating L98/L99/L100 order, after a warmup for each version, stratum, and worker count. Values are seconds (`L98 / L99 / L100`). All 72 measured invocations completed; each L100 run reported source stamp `7224bf6e…`.
| Workers | Strate | Near ready, LOD 0–2 | Whole view |
|---:|---|---:|---:|
| 8 | SW | 1.364 / 0.940 / 1.425 | 3.035 / 2.413 / 3.429 |
| 8 | FI | 2.792 / 1.719 / 2.157 | 4.385 / 2.931 / 3.409 |
| 8 | TN | 2.746 / 1.723 / 2.175 | 4.022 / 2.790 / 3.294 |
| 8 | VS | 1.631 / 1.352 / 1.522 | 2.779 / 2.553 / 2.745 |
| 12 | SW | 1.174 / 0.949 / 1.039 | 2.509 / 2.148 / 2.280 |
| 12 | FI | 2.617 / 1.783 / 2.054 | 3.919 / 2.902 / 3.197 |
| 12 | TN | 2.358 / 1.547 / 1.802 | 3.458 / 2.539 / 2.813 |
| 12 | VS | 1.158 / 1.032 / 1.083 | 2.181 / 2.027 / 2.097 |
On FI/TN, L100 remains 18–24% faster than L98 for near-ready and whole-view medians. It is 10–26% slower than L99, reflecting the exact fallback needed for off-grid misses.
## Captures
[`captures/INDEX.md`](captures/INDEX.md) lists all 15 paired views: both mouths and 25/50/75 route stations for each passage. All 30 PNGs pass the programmatic geometry gate (over 3,000 non-dominant-background pixels and at least two RGB colors); the index reports each pair’s changed-pixel count. It also records the 1–2 same-camera three-version check: visible geometry is 1,407,415 px in L98, 1,206,362 in L99, and 1,407,415 in L100; changed pixels are 1,135,959 L98→L99 and 1,134,150 L99→L100. L98→L100 changes 32,217 px. The 0–1 mid-25 and 2–3 mid-25/mid-50 cameras use documented nearest-surface targets where the forward route view had no visible L98 surface.
## Failed runs
- The four density-comparison audits exit 14 on their legacy strict geometry gate; the new added/removed crossing counts are zero in all four.
- An intermediate composition-order experiment put passage carving after floor caps and failed the landmark walk audit. Restoring the L99 stamp/cap precedence made the final fast tier green.
- The first Unreal build attempt exited before compilation because UBT could not rotate its log under read-only `/home/jahni/.config`. The retry redirected UBT config and logging under `Saved/Linux/l100` and succeeded (27/27 actions; three RealtimeMesh deprecation warnings).
No benchmark or capture process timed out.
## Evidence files
- Source point trace: `Saved/Linux/l100/logs/column-sweep.log`, `column-direct.log`.
- Density comparison: `Saved/Linux/l100/density-compare/summary.json`.
- Hash matrix: `Saved/Linux/l100/hashes/summary.json`.
- Fast tier: `Saved/Linux/l100/fast-summary.json`.
- L80 triple: `Saved/Linux/l100/readiness/l98-l99-l100/summary.json`.
- Build, Clang, GCC, captures, and per-run logs: `Saved/Linux/l100/logs/` and `Saved/Linux/l100/`.
@@ -0,0 +1,602 @@
#!/usr/bin/env python3
"""Capture matched L98/L100 passage mouth and mid-route views."""
from __future__ import annotations
import json
import math
import struct
import subprocess
import time
import zlib
from array import array
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l100/captures"
IMAGE_DIR = Path(__file__).resolve().parent / "captures"
DATA = ROOT / "Standalone/data/owner_world.json"
LAYOUT_PATH = ROOT / "Saved/Linux/l99/resolved-sw.json"
BINARIES = {
"before-l98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19",
"after-l100": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang",
}
RENDERER = ROOT / "Saved/Linux/l100/bin/VoxelForgeOffscreenCapture-l100-clang"
VOXEL_Q = 256.0
CORE_UM_PER_VOXEL = 250_000.0
ROUTE_POINTS = {}
ROUTE_LENGTHS = {}
def add(a, b):
return tuple(x + y for x, y in zip(a, b))
def sub(a, b):
return tuple(x - y for x, y in zip(a, b))
def mul(a, s):
return tuple(x * s for x in a)
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
def norm(a):
length = math.sqrt(dot(a, a))
if length < 1.0e-8:
raise ValueError("degenerate capture direction")
return tuple(x / length for x in a)
def fmt(values):
return ",".join(f"{value:.9f}" for value in values)
def angles(camera, target):
delta = sub(target, camera)
return (math.degrees(math.atan2(delta[0], delta[1])),
math.degrees(math.atan2(delta[2], math.hypot(delta[0], delta[1]))))
def interpolate(points, distance):
remaining = max(0.0, distance)
for a, b in zip(points, points[1:]):
delta = sub(b[0], a[0])
length = math.sqrt(dot(delta, delta))
if length < 1.0e-8:
continue
if remaining <= length:
t = remaining / length
return (tuple(a[0][i] + delta[i] * t for i in range(3)),
a[1] + (b[1] - a[1]) * t)
remaining -= length
return points[-1]
def route_frame(points, distance):
center, radius = interpolate(points, distance)
before, _ = interpolate(points, max(0.0, distance - 0.25))
after, _ = interpolate(points, min(sum(
math.sqrt(dot(sub(b[0], a[0]), sub(b[0], a[0])))
for a, b in zip(points, points[1:])), distance + 0.25))
tangent = norm(sub(after, before))
horizontal = norm((tangent[0], tangent[1], 0.0))
side = (-horizontal[1], horizontal[0], 0.0)
return center, radius, tangent, side
def route_length(points):
return sum(math.sqrt(dot(sub(b[0], a[0]), sub(b[0], a[0])))
for a, b in zip(points, points[1:]))
def nearest_route(points, point):
best = None
distance_along = 0.0
for a, b in zip(points, points[1:]):
delta = sub(b[0], a[0])
length_sq = dot(delta, delta)
if length_sq < 1.0e-10:
continue
segment_length = math.sqrt(length_sq)
t = max(0.0, min(1.0, dot(sub(point, a[0]), delta) / length_sq))
center = add(a[0], mul(delta, t))
tangent = norm(delta)
radial = sub(point, center)
radial = sub(radial, mul(tangent, dot(radial, tangent)))
distance = math.sqrt(dot(sub(point, center), sub(point, center)))
if best is None or distance < best[0]:
radius = a[1] + (b[1] - a[1]) * t
best = (distance, center, tangent, radial, radius,
distance_along + t * segment_length)
distance_along += segment_length
if best is None:
raise ValueError("passage route has no non-degenerate segment")
return best
def run(name, command, headless):
log = OUT / f"{name}.log"
wrapped = ["taskset", "-c", "0-11"]
if headless:
wrapped += ["prlimit", "--as=6442450944", "--"]
wrapped += ["timeout", "1740s", *map(str, command)]
started = time.monotonic()
with log.open("w", encoding="utf-8") as stream:
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
stderr=subprocess.STDOUT, check=False, timeout=1745)
row = {"name": name, "exit_code": result.returncode,
"wall_s": round(time.monotonic() - started, 3),
"log": str(log.relative_to(ROOT))}
print("[L100CaptureRun] " + json.dumps(row, sort_keys=True), flush=True)
if result.returncode != 0:
raise RuntimeError(f"capture step failed: {name}; see {log}")
return row
def nearest_surface_anchor(mesh, point, tile_search_margin_m=20.0):
"""Return the nearest surface vertex and outward mesh normal from a VFMSH scene."""
best_distance_sq = float("inf")
best_position = best_normal = None
with mesh.open("rb") as stream:
magic = stream.read(8)
has_materials = magic == b"VFMSH002"
if magic not in (b"VFMSH001", b"VFMSH002"):
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
if version != (2 if has_materials else 1):
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
stream.seek(40, 1) # stored camera position, yaw, and pitch
for _ in range(tile_count):
key = struct.unpack("<4i", stream.read(16))
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
"<5I", stream.read(20))
extent = (32 << key[3]) * voxel_um / 1_000_000.0
origin = (key[0] * extent, key[1] * extent, key[2] * extent)
if any(point[axis] < origin[axis] - tile_search_margin_m or
point[axis] > origin[axis] + extent + tile_search_margin_m
for axis in range(3)):
stream.seek(vertex_count * 24 + (10 * vertex_count if has_materials else 0)
+ index_count * 4 + mask_count, 1)
continue
positions = array("f")
positions.frombytes(stream.read(vertex_count * 12))
normals = array("f")
normals.frombytes(stream.read(vertex_count * 12))
if has_materials:
stream.seek(vertex_count * 10, 1)
stream.seek(index_count * 4 + mask_count, 1)
if struct.pack("=I", 1) != struct.pack("<I", 1):
positions.byteswap()
normals.byteswap()
for index in range(min(surface_count, vertex_count)):
offset = index * 3
# VFMSH stores vertex coordinates in world space; tile origin
# is metadata for edge rendering and must not be added again.
position = tuple(float(positions[offset + axis]) for axis in range(3))
delta = sub(position, point)
distance_sq = dot(delta, delta)
if distance_sq >= best_distance_sq:
continue
raw_normal = tuple(float(normals[offset + axis]) for axis in range(3))
if dot(raw_normal, raw_normal) < 1.0e-10:
continue
best_distance_sq = distance_sq
best_position = position
best_normal = norm(raw_normal)
if best_position is None:
raise RuntimeError(f"no nearby surface vertices in mesh scene: {mesh}")
return {"position_m": best_position, "normal": best_normal,
"distance_m": math.sqrt(best_distance_sq)}
def make_passages(layout):
q_to_m = layout["core_voxel_size_um"] / (VOXEL_Q * 1_000_000.0)
passages = []
all_routes = []
for passage in layout["passages"]:
controls = [(tuple(float(q) * q_to_m for q in p["position_q"]),
float(p["radius_q"]) * q_to_m)
for p in passage["control_points"]]
upper = tuple(float(q) * q_to_m for q in passage["upper_mouth_q"])
lower = tuple(float(q) * q_to_m for q in passage["lower_mouth_q"])
ui = min(range(len(controls)), key=lambda i: dot(sub(controls[i][0], upper),
sub(controls[i][0], upper)))
li = min(range(len(controls)), key=lambda i: dot(sub(controls[i][0], lower),
sub(controls[i][0], lower)))
points = controls[ui:li + 1]
if len(points) < 2:
raise RuntimeError(f"invalid mouth path: {passage['stable_id']}")
upper_offset = math.sqrt(dot(sub(points[0][0], upper), sub(points[0][0], upper)))
lower_offset = math.sqrt(dot(sub(points[-1][0], lower), sub(points[-1][0], lower)))
length = route_length(points)
passage_views = []
for endpoint, distance, camera_distance, outward_sign in (
("upper-mouth", 0.0, min(length, 7.4), -1.0),
("lower-mouth", length, max(0.0, length - 5.5), 1.0)):
center, _, tangent, side = route_frame(points, distance)
camera_center, _, _, camera_side = route_frame(points, camera_distance)
camera = (camera_center[0] + camera_side[0] * 2.5,
camera_center[1] + camera_side[1] * 2.5, camera_center[2])
outward = mul(tangent, outward_sign)
target = (center[0] + outward[0] * 8.0 + side[0] * 2.5,
center[1] + outward[1] * 8.0 + side[1] * 2.5, center[2])
yaw, pitch = angles(camera, target)
passage_views.append({"name": f"{passage['stable_id']}-{endpoint}",
"kind": "mouth", "focus": center, "camera": camera, "target": target,
"yaw": yaw, "pitch": pitch, "clip": None, "fov": 80.0,
"focus_radius": 20.0, "passage": passage["stable_id"],
"camera_location": "passage-air"})
for fraction in (0.25, 0.50, 0.75):
distance = length * fraction
center, radius, _, side = route_frame(points, distance)
camera = (center[0] + side[0] * 0.75,
center[1] + side[1] * 0.75,
center[2] - radius + 1.6)
target_pos, target_radius = interpolate(
points, min(length, distance + 14.0))
target = (target_pos[0] + side[0] * 2.35,
target_pos[1] + side[1] * 2.35,
target_pos[2] - target_radius + 1.8)
yaw, pitch = angles(camera, target)
pct = int(round(fraction * 100))
passage_views.append({"name": f"{passage['stable_id']}-mid-{pct:02d}",
"kind": "mid-tunnel", "route_fraction": fraction,
"focus": center, "camera": camera, "target": target,
"yaw": yaw, "pitch": pitch, "clip": None,
"fov": 78.0, "focus_radius": 24.0,
"passage": passage["stable_id"]})
passages.append({"stable_id": passage["stable_id"], "route_length_m": length,
"control_points": len(points), "upper_mouth_anchor_offset_m": upper_offset,
"lower_mouth_anchor_offset_m": lower_offset, "views": passage_views})
all_routes.append((passage["stable_id"], points))
return passages, all_routes
def make_hotspot_views(routes, reach_summary):
views = []
for series, rows, value_key, location_key, region_key in (
("displacement", reach_summary["top_displacement_tiles"],
"max_displacement_cm", "displacement_location_metres", "displacement_region"),
("normal", reach_summary["top_normal_tiles"],
"max_normal_degrees", "normal_location_metres", "normal_region"),
):
for index, row in enumerate(rows, 1):
point = tuple(row[location_key])
candidates = [(nearest_route(route, point), name, route)
for name, route in routes]
(distance, center, tangent, radial, radius, along), nearest_id, route = min(
candidates, key=lambda item: item[0][0])
if math.sqrt(dot(radial, radial)) < 0.25:
horizontal = norm((tangent[0], tangent[1], 0.0))
radial = (-horizontal[1], horizontal[0], 0.0)
outward = norm(radial)
camera = add(point, mul(outward, 20.0))
target = point
plane = None
yaw, pitch = angles(camera, target)
views.append({"name": f"top-{series}-{index:02d}", "kind": f"top-{series}",
"focus": point, "camera": camera, "target": target,
"yaw": yaw, "pitch": pitch, "clip": plane, "fov": 68.0,
"focus_radius": 18.0, "strate": row["strate"],
"value": row[value_key], "region": row[region_key],
"camera_anchor": "nearest-surface-l98-v5", "nearest_passage": nearest_id,
"route_distance_m": distance})
return views
def decode_png(path):
"""Decode 8-bit RGB/RGBA PNG pixels using only the Python standard library."""
data = path.read_bytes()
if data[:8] != b"\x89PNG\r\n\x1a\n":
raise RuntimeError(f"not a PNG: {path}")
width = height = bit_depth = color_type = interlace = None
compressed = bytearray()
offset = 8
while offset < len(data):
length = struct.unpack_from(">I", data, offset)[0]
kind = data[offset + 4:offset + 8]
chunk = data[offset + 8:offset + 8 + length]
if kind == b"IHDR":
width, height, bit_depth, color_type, compression, filtering, interlace = \
struct.unpack(">IIBBBBB", chunk)
if compression or filtering or interlace:
raise RuntimeError(f"unsupported PNG coding in {path}")
elif kind == b"IDAT":
compressed.extend(chunk)
elif kind == b"IEND":
break
offset += 12 + length
if bit_depth != 8 or color_type not in (2, 6):
raise RuntimeError(f"expected 8-bit RGB/RGBA PNG: {path}")
channels = 3 if color_type == 2 else 4
stride = width * channels
encoded = zlib.decompress(compressed)
pixels = bytearray(height * stride)
source_offset = 0
for y in range(height):
filter_type = encoded[source_offset]
source_offset += 1
row = bytearray(encoded[source_offset:source_offset + stride])
source_offset += stride
prior_offset = (y - 1) * stride
for index in range(stride):
left = row[index - channels] if index >= channels else 0
above = pixels[prior_offset + index] if y else 0
upper_left = pixels[prior_offset + index - channels] \
if y and index >= channels else 0
if filter_type == 1:
row[index] = (row[index] + left) & 255
elif filter_type == 2:
row[index] = (row[index] + above) & 255
elif filter_type == 3:
row[index] = (row[index] + ((left + above) >> 1)) & 255
elif filter_type == 4:
estimate = left + above - upper_left
left_distance = abs(estimate - left)
above_distance = abs(estimate - above)
upper_left_distance = abs(estimate - upper_left)
if left_distance <= above_distance and left_distance <= upper_left_distance:
predictor = left
elif above_distance <= upper_left_distance:
predictor = above
else:
predictor = upper_left
row[index] = (row[index] + predictor) & 255
elif filter_type != 0:
raise RuntimeError(f"unsupported PNG filter {filter_type}: {path}")
start = y * stride
pixels[start:start + stride] = row
if channels == 3:
return width, height, pixels
rgb = bytearray(width * height * 3)
rgb[0::3] = pixels[0::4]
rgb[1::3] = pixels[1::4]
rgb[2::3] = pixels[2::4]
return width, height, rgb
def image_geometry_metrics(path):
width, height, rgb = decode_png(path)
color_counts = {}
for offset in range(0, len(rgb), 3):
color = (rgb[offset], rgb[offset + 1], rgb[offset + 2])
color_counts[color] = color_counts.get(color, 0) + 1
total = width * height
visible = total - max(color_counts.values())
return {"width": width, "height": height, "unique_rgb": len(color_counts),
"visible_geometry_pixels": visible,
"visible_geometry_fraction": visible / total}
def changed_pixel_count(before, after):
before_width, before_height, before_rgb = decode_png(before)
after_width, after_height, after_rgb = decode_png(after)
if (before_width, before_height) != (after_width, after_height):
raise RuntimeError(f"capture dimensions differ: {before} / {after}")
return sum(before_rgb[offset] != after_rgb[offset]
or before_rgb[offset + 1] != after_rgb[offset + 1]
or before_rgb[offset + 2] != after_rgb[offset + 2]
for offset in range(0, len(before_rgb), 3))
def mesh_triangle_count(mesh):
with mesh.open("rb") as stream:
magic = stream.read(8)
has_materials = magic == b"VFMSH002"
if magic not in (b"VFMSH001", b"VFMSH002"):
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
version, _, tile_count = struct.unpack("<3I", stream.read(12))
if version != (2 if has_materials else 1):
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
stream.seek(40, 1)
triangles = 0
for _ in range(tile_count):
stream.seek(16, 1)
_, _, vertex_count, index_count, mask_count = struct.unpack(
"<5I", stream.read(20))
triangles += index_count // 3
stream.seek(vertex_count * 24 + (10 * vertex_count if has_materials else 0)
+ index_count * 4 + mask_count, 1)
return triangles
def set_route_station(view, distance):
points = ROUTE_POINTS[view["passage"]]
length = ROUTE_LENGTHS[view["passage"]]
center, radius, _, side = route_frame(points, distance)
camera = (center[0] + side[0] * 0.75,
center[1] + side[1] * 0.75,
center[2] - radius + 1.6)
target_pos, target_radius = interpolate(points, min(length, distance + 14.0))
target = (target_pos[0] + side[0] * 2.35,
target_pos[1] + side[1] * 2.35,
target_pos[2] - target_radius + 1.8)
yaw, pitch = angles(camera, target)
if "route_fraction_requested" not in view:
view["route_fraction_requested"] = view["route_fraction"]
view["route_requested_focus"] = view["focus"]
view["route_fraction"] = distance / length
view["route_station_adjustment_m"] = distance - length * view["route_fraction_requested"]
view["focus"] = center
view["camera"] = camera
view["target"] = target
view["yaw"], view["pitch"] = yaw, pitch
view["clip"] = None
def render_command(view, mesh, image, camera, yaw, pitch):
args = [RENDERER, mesh, image,
"--size=1600x900", f"--fov-deg={view['fov']:.1f}",
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}",
f"--pitch-deg={pitch:.6f}", "--shade=readable", "--headlamp=on",
"--height-tint=on", "--tile-edges=off", "--tile-skirts=off",
f"--height-center-m={view['focus'][2]:.6f}", "--height-range-m=10",
f"--focus-region={fmt((*view['focus'], view['focus_radius']))}"]
if view["clip"] is not None:
args.append(f"--clip-plane={fmt(view['clip'])}")
return args
def export_scene(view, binary, mesh, stem):
camera = view["camera"]
yaw, pitch = angles(camera, view["target"])
export_args = [binary, DATA, "--world-assembler-prototype", "--stream-benchmark",
"--stream-pipeline", "--workers=8", "--force-generate", "--clip-radius=3",
"--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0",
f"--camera={fmt((*camera, yaw, pitch))}", f"--mesh-export={mesh}"]
row = run(f"{stem}-export", export_args, headless=True)
row["scene_bytes"] = mesh.stat().st_size if mesh.exists() else 0
row["scene_triangles"] = mesh_triangle_count(mesh) if row["scene_bytes"] else 0
return row
def export_and_render(view, label, binary):
safe = view["name"].replace("/", "-")
stem = f"{safe}-{label}"
mesh = OUT / f"{stem}.vfmesh"
image = IMAGE_DIR / f"{safe}-{label}.png"
camera = view["camera"]
yaw, pitch = angles(camera, view["target"])
export_row = export_scene(view, binary, mesh, stem)
if export_row["scene_bytes"] <= 0:
raise RuntimeError(f"empty mesh scene: {mesh}")
if export_row["scene_triangles"] == 0:
if label != "before-l98" or view.get("kind") != "mid-tunnel":
raise RuntimeError(f"route station exports no surface triangles: {mesh}")
points = ROUTE_POINTS[view["passage"]]
length = ROUTE_LENGTHS[view["passage"]]
requested_fraction = view["route_fraction"]
requested_distance = length * requested_fraction
found_surface = False
for offset in (5.0, -5.0, 10.0, -10.0, 20.0, -20.0, 35.0, -35.0,
50.0, -50.0, 75.0, -75.0, 100.0, -100.0, 150.0,
-150.0, 200.0, -200.0):
distance = max(0.0, min(length, requested_distance + offset))
if distance == requested_distance:
continue
set_route_station(view, distance)
adjusted_stem = f"{stem}-station-{view['route_fraction']:.5f}"
export_row = export_scene(view, binary, mesh, adjusted_stem)
if export_row["scene_triangles"] > 0:
view["camera_anchor"] = "route-station-shifted-to-nearby-geometry"
found_surface = True
break
if not found_surface:
raise RuntimeError(f"no geometry along nearby route stations: {mesh}")
camera = view["camera"]
yaw, pitch = angles(camera, view["target"])
if label == "before-l98" and view.get("camera_anchor") == "nearest-surface-l98-v5":
anchor = nearest_surface_anchor(mesh, view["focus"], tile_search_margin_m=256.0)
camera = add(anchor["position_m"], mul(anchor["normal"], 8.0))
view["camera"] = camera
view["target"] = anchor["position_m"]
view["yaw"], view["pitch"] = angles(camera, view["target"])
view["clip"] = None
view["surface_anchor"] = anchor
yaw, pitch = view["yaw"], view["pitch"]
render_args = render_command(view, mesh, image, camera, yaw, pitch)
try:
render_row = run(f"{stem}-render", render_args, headless=True)
except RuntimeError:
if view["clip"] is None:
raise
retry_args = [arg for arg in render_args
if not (isinstance(arg, str) and arg.startswith("--clip-plane="))]
render_row = run(f"{stem}-render-no-clip-retry", retry_args, headless=True)
render_row["clip_fallback"] = True
render_row["image"] = str(image.relative_to(ROOT))
render_row["image_bytes"] = image.stat().st_size if image.exists() else 0
if render_row["image_bytes"] <= 0:
raise RuntimeError(f"empty image: {image}")
geometry = image_geometry_metrics(image)
if label == "before-l98" and (
geometry["visible_geometry_pixels"] < 3000 or geometry["unique_rgb"] < 2):
# At a few deep route stations the default forward view looks into empty
# sky. Keep the camera on the route station and aim at the nearest L98
# surface in the exported scene; retain that matched target for L100.
anchor = nearest_surface_anchor(mesh, view["focus"], tile_search_margin_m=256.0)
if anchor["distance_m"] > 60.0:
raise RuntimeError(f"no nearby surface for fallback camera: {image} {anchor}")
camera = view["camera"]
target = anchor["position_m"]
yaw, pitch = angles(camera, target)
view["target"] = target
view["yaw"], view["pitch"] = yaw, pitch
view["clip"] = None
view["camera_anchor"] = "route-station-nearest-surface-target"
view["surface_anchor"] = anchor
retry_args = render_command(view, mesh, image, camera, yaw, pitch)
render_row = run(f"{stem}-render-surface-fallback", retry_args, headless=True)
render_row["image"] = str(image.relative_to(ROOT))
render_row["image_bytes"] = image.stat().st_size if image.exists() else 0
geometry = image_geometry_metrics(image)
render_row["geometry"] = geometry
if geometry["visible_geometry_pixels"] < 3000 or geometry["unique_rgb"] < 2:
raise RuntimeError(f"camera sees sky only or too little geometry: {image} {geometry}")
mesh.unlink()
return export_row, render_row
def main():
OUT.mkdir(parents=True, exist_ok=True)
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
for path in (*BINARIES.values(), RENDERER, DATA, LAYOUT_PATH):
if not path.is_file():
raise SystemExit(f"missing input: {path}")
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
passages, routes = make_passages(layout)
ROUTE_POINTS.clear()
ROUTE_LENGTHS.clear()
for stable_id, points in routes:
ROUTE_POINTS[stable_id] = points
ROUTE_LENGTHS[stable_id] = route_length(points)
views = [view for passage in passages for view in passage["views"]]
partial_path = OUT / "runs-partial.json"
existing_records = {}
if partial_path.is_file():
for item in json.loads(partial_path.read_text(encoding="utf-8")):
existing_records[item["view"]["name"]] = item
records = []
for view in views:
prior = existing_records.get(view["name"])
camera_matches = (prior is not None and
prior["view"].get("camera_anchor") == view.get("camera_anchor"))
if camera_matches:
for key in ("camera", "target", "yaw", "pitch", "clip", "surface_anchor"):
if key in prior["view"]:
view[key] = prior["view"][key]
if camera_matches and all(version in prior and
(ROOT / prior[version]["render"]["image"]).is_file()
for version in BINARIES):
records.append(prior)
print(f"[L100CaptureSkip] complete_pair={view['name']}", flush=True)
continue
pair = {"view": view}
for label, binary in BINARIES.items():
export_row, render_row = export_and_render(view, label, binary)
pair[label] = {"export": export_row, "render": render_row}
(OUT / "runs-partial.json").write_text(json.dumps(records + [pair], indent=2) + "\n",
encoding="utf-8")
pair["comparison"] = {"changed_pixels": changed_pixel_count(
ROOT / pair["before-l98"]["render"]["image"],
ROOT / pair["after-l100"]["render"]["image"])}
(OUT / "runs-partial.json").write_text(json.dumps(records + [pair], indent=2) + "\n",
encoding="utf-8")
records.append(pair)
summary = {"source_layout_identity": layout["stable_identity"],
"view_count": len(views), "paired_png_count": len(views) * 2,
"passages": passages, "views": records}
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
encoding="utf-8")
print("[L100CaptureSummary] " + json.dumps({
"views": len(views), "paired_png_count": len(views) * 2,
"images": [row[version]["render"]["image"] for row in records
for version in BINARIES]}, sort_keys=True), flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Compare the direct passage evaluator with the bounded L100 swept tube."""
from __future__ import annotations
import json
import re
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l100/density-compare"
DATA = ROOT / "Standalone/data/owner_world.json"
HEADLESS = ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang"
CAMERAS = {
"SW": (8.0, 16.0, -372.2626953125),
"FI": (25.8369140625, 3.19921875, -737.0),
"TN": (19.166015625, -14.2001953125, -753.3740234375),
"VS": (-20.0625, -5.068359375, -900.353515625),
}
SUMMARY = re.compile(r"^\[L93PassageReachSummary\] reach_m=4\.00 (.*)$", re.M)
COVERAGE = re.compile(r"^\[L93PassageReachCoverage\] (.*)$", re.M)
TILE = re.compile(
r"^\[L93PassageReachTile\] reach_m=4\.00 key=\(([-\d]+),([-\d]+),([-\d]+),lod(\d+)\) "
r"samples=(\d+) changed=(\d+) sign_flips=(\d+) moved_edges=(\d+) "
r"added_crossings=(\d+) removed_crossings=(\d+) "
r"added_surface_triangles=(\d+) removed_surface_triangles=(\d+) "
r"max_displacement_cm=([\deE+.-]+) max_displacement_xyz_vox=\(([^)]*)\) "
r"displacement_region=(\d+) normal_vertices=(\d+) max_normal_degrees=([\deE+.-]+) "
r"max_normal_xyz_vox=\(([^)]*)\) normal_region=(\d+) changed_mesh=(\d+)$", re.M)
VOXEL_SIZE_M = 0.25
def xyz(text: str) -> list[float]:
return [float(part) for part in text.split(",")]
def parse_locations(output: str, strate: str) -> tuple[list[dict], list[dict]]:
rows = []
for match in TILE.finditer(output):
(x, y, z, lod, samples, changed, flips, moved, added, removed,
added_triangles, removed_triangles, displacement, displacement_xyz,
displacement_region, normal_vertices, angle, normal_xyz, normal_region,
changed_mesh) = match.groups()
disp_vox = xyz(displacement_xyz)
normal_vox = xyz(normal_xyz)
rows.append({
"strate": strate, "tile": [int(x), int(y), int(z), int(lod)],
"samples": int(samples), "changed_samples": int(changed),
"sign_flips": int(flips), "moved_edges": int(moved),
"surface_added_crossings": int(added),
"surface_removed_crossings": int(removed),
"surface_added_triangles": int(added_triangles),
"surface_removed_triangles": int(removed_triangles),
"max_displacement_cm": float(displacement),
"displacement_location_voxels": disp_vox,
"displacement_location_metres": [value * VOXEL_SIZE_M for value in disp_vox],
"displacement_region": int(displacement_region),
"normal_vertices": int(normal_vertices), "max_normal_degrees": float(angle),
"normal_location_voxels": normal_vox,
"normal_location_metres": [value * VOXEL_SIZE_M for value in normal_vox],
"normal_region": int(normal_region), "changed_mesh": bool(int(changed_mesh)),
})
top_displacement = sorted(rows, key=lambda row: row["max_displacement_cm"], reverse=True)[:5]
top_normals = sorted(rows, key=lambda row: row["max_normal_degrees"], reverse=True)[:5]
return top_displacement, top_normals
def top_unique(rows: list[dict], value_key: str, location_key: str) -> list[dict]:
chosen, seen = [], set()
for row in sorted(rows, key=lambda item: item[value_key], reverse=True):
if row[value_key] <= 0.0:
continue
location = tuple(round(value, 4) for value in row[location_key])
if location in seen:
continue
seen.add(location)
chosen.append(row)
if len(chosen) == 5:
break
return chosen
def run(strate: str) -> dict:
OUT.mkdir(parents=True, exist_ok=True)
log = OUT / f"reach-{strate.lower()}.log"
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
command = [
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"timeout", "1740s", str(HEADLESS), str(DATA), "--world-assembler-prototype",
"--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3",
"--max-clip-level=8", "--max-level=8", f"--camera={camera}",
"--audit-passage-carve-reach",
]
start = time.monotonic()
with log.open("w", encoding="utf-8") as stream:
result = subprocess.run(command, cwd=ROOT, stdout=stream,
stderr=subprocess.STDOUT, check=False, timeout=1740)
output = log.read_text(encoding="utf-8", errors="replace")
summary_match, coverage_match = SUMMARY.search(output), COVERAGE.search(output)
if summary_match is None or coverage_match is None or result.returncode in (-9, 124):
raise RuntimeError(f"incomplete carve-reach audit: exit={result.returncode} {log}")
reach = dict(re.findall(r"([a-zA-Z0-9_]+)=([^ ]+)", summary_match.group(1)))
coverage = dict(re.findall(r"([a-zA-Z0-9_]+)=([^ ]+)", coverage_match.group(1)))
top_displacement, top_normals = parse_locations(output, strate)
row = {
"strate": strate, "exit_code": result.returncode,
"wall_s": round(time.monotonic() - start, 3), "coverage": coverage,
"bounded_sweep_summary": reach,
"bounded_sweep_tile_rows": len(TILE.findall(output)),
"top_displacement_tiles": top_displacement,
"top_normal_tiles": top_normals,
"log": str(log.relative_to(ROOT)),
}
print("[L100DensityCompareRun] " + json.dumps(row, sort_keys=True), flush=True)
return row
def main() -> int:
if not HEADLESS.is_file() or not DATA.is_file():
raise SystemExit(f"missing input: {HEADLESS if not HEADLESS.is_file() else DATA}")
if "--summarize-only" in sys.argv:
rows = json.loads((OUT / "runs-partial.json").read_text(encoding="utf-8"))
else:
rows = []
for strate in CAMERAS:
rows.append(run(strate))
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n",
encoding="utf-8")
all_displacements = [row for result in rows for row in result["top_displacement_tiles"]]
all_normals = [row for result in rows for row in result["top_normal_tiles"]]
top_displacement = top_unique(all_displacements, "max_displacement_cm",
"displacement_location_metres")
top_normals = top_unique(all_normals, "max_normal_degrees", "normal_location_metres")
summary = {"runs": rows, "top_displacement_tiles": top_displacement,
"top_normal_tiles": top_normals}
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Check L100 whole-view tile hashes across W1/W8/W12 and Clang/GCC."""
from __future__ import annotations
import json
import re
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l100/hashes"
DATA = ROOT / "Standalone/data/owner_world.json"
BINARIES = {
"Clang": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang",
"GCC": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-gcc",
}
CAMERAS = {
"SW": (8.0, 16.0, -372.2626953125),
"FI": (25.8369140625, 3.19921875, -737.0),
"TN": (19.166015625, -14.2001953125, -753.3740234375),
"VS": (-20.0625, -5.068359375, -900.353515625),
}
TILE_HASH = re.compile(
r"^\[PipelineTileHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) "
r"density=(0x[0-9a-f]+) mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) "
r"topology=(0x[0-9a-f]+)$", re.M)
WHOLE_HASH = re.compile(
r"^\[PipelineWholeViewHash\] tiles=(\d+) density=(0x[0-9a-f]+) "
r"mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) topology=(0x[0-9a-f]+)$", re.M)
FIELDS = ("density", "mesh", "payload", "topology")
def run(strate: str, compiler: str, workers: int) -> dict:
OUT.mkdir(parents=True, exist_ok=True)
log = OUT / f"{strate.lower()}-{compiler.lower()}-w{workers}.log"
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
command = [
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"timeout", "1740s", str(BINARIES[compiler]), str(DATA),
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
"--max-level=8", f"--camera={camera}", "--stream-hash-tiles",
]
start = time.monotonic()
with log.open("w", encoding="utf-8") as stream:
result = subprocess.run(command, cwd=ROOT, stdout=stream,
stderr=subprocess.STDOUT, check=False, timeout=1740)
output = log.read_text(encoding="utf-8", errors="replace")
hashes = {
(int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))):
m.group(5, 6, 7, 8) for m in TILE_HASH.finditer(output)
}
whole = WHOLE_HASH.search(output)
stamp = re.search(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", output, re.M)
if (result.returncode or whole is None or stamp is None
or len(hashes) != int(whole.group(1))):
raise RuntimeError(f"hash run failed or incomplete: exit={result.returncode} {log}")
row = {
"strate": strate, "compiler": compiler, "workers": workers,
"source_stamp": stamp.group(1), "exit_code": result.returncode,
"wall_s": round(time.monotonic() - start, 3), "tile_count": len(hashes),
"tiles_by_lod": {str(lod): sum(1 for key in hashes if key[0] == lod)
for lod in sorted({key[0] for key in hashes})},
"whole_hash": list(whole.groups()), "tile_hashes": hashes,
"log": str(log.relative_to(ROOT)),
}
print("[L100HashRun] " + json.dumps(
{key: value for key, value in row.items() if key != "tile_hashes"},
sort_keys=True), flush=True)
return row
def compare(strate: str, reference: dict, candidate: dict) -> dict:
keys = set(reference["tile_hashes"]) | set(candidate["tile_hashes"])
mismatches = {
field: sum(reference["tile_hashes"].get(key, (None,) * 4)[index]
!= candidate["tile_hashes"].get(key, (None,) * 4)[index]
for key in keys)
for index, field in enumerate(FIELDS)
}
row = {
"strate": strate, "reference": f"{reference['compiler']}-w{reference['workers']}",
"candidate": f"{candidate['compiler']}-w{candidate['workers']}",
"tile_counts_match": reference["tile_count"] == candidate["tile_count"],
"whole_hashes_match": reference["whole_hash"] == candidate["whole_hash"],
"tile_hashes_match": all(value == 0 for value in mismatches.values())
and reference["tile_hashes"].keys() == candidate["tile_hashes"].keys(),
"component_mismatch_tiles": mismatches,
"tiles_reference": reference["tile_count"], "tiles_candidate": candidate["tile_count"],
"reference_whole_hash": reference["whole_hash"],
"candidate_whole_hash": candidate["whole_hash"],
}
print("[L100HashComparison] " + json.dumps(row, sort_keys=True), flush=True)
return row
def main() -> int:
for binary in (*BINARIES.values(), DATA):
if not binary.is_file():
raise SystemExit(f"missing input: {binary}")
detail, comparisons = [], []
for strate in CAMERAS:
group = {(compiler, workers): run(strate, compiler, workers)
for compiler in BINARIES for workers in (1, 8, 12)}
reference = group[("Clang", 1)]
for key, candidate in group.items():
if key == ("Clang", 1):
continue
comparisons.append(compare(strate, reference, candidate))
detail.extend({key: value for key, value in row.items() if key != "tile_hashes"}
for row in group.values())
(OUT / "runs-partial.json").write_text(json.dumps(
{"runs": detail, "comparisons": comparisons}, indent=2) + "\n",
encoding="utf-8")
summary = {"runs": detail, "comparisons": comparisons}
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
encoding="utf-8")
return 0 if all(row["tile_counts_match"] and row["whole_hashes_match"]
and row["tile_hashes_match"] for row in comparisons) else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""L80 cold-readiness protocol: three interleaved L98/L99 pairs at W8 and W12."""
from __future__ import annotations
import json
import re
import statistics
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT_ROOT = ROOT / "Saved/Linux/l100/readiness"
DATA = ROOT / "Standalone/data/owner_world.json"
ALL_BINARIES = {
"L98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19",
"L99": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang",
"L100": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang",
}
OUT = OUT_ROOT
BINARIES = {}
# Resolved owner camera index -> stratum. Keep the labels tied to the canonical layout.
CAMERAS = {
"SW": (8.0, 16.0, -372.2626953125),
"FI": (25.8369140625, 3.19921875, -737.0),
"TN": (19.166015625, -14.2001953125, -753.3740234375),
"VS": (-20.0625, -5.068359375, -900.353515625),
}
PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
NEAR = re.compile(r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) "
r"cold_ready_s=([0-9.]+)$", re.M)
def run(strate: str, workers: int, label: str, phase: str, rep: int) -> dict:
OUT.mkdir(parents=True, exist_ok=True)
log = OUT / f"w{workers}-{strate.lower()}-{label.lower()}-{phase}{rep}.log"
camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0"
command = [
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"env", "timeout", "1740s", str(BINARIES[label]), str(DATA),
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
"--max-level=8", f"--camera={camera}", "--profile-lod-work",
"--stream-scheduler-policy=3",
]
start = time.monotonic()
with log.open("w", encoding="utf-8") as stream:
result = subprocess.run(command, cwd=ROOT, stdout=stream,
stderr=subprocess.STDOUT, check=False, timeout=1740)
output = log.read_text(encoding="utf-8", errors="replace")
pipeline_match, near_match = PIPELINE.search(output), NEAR.search(output)
if result.returncode or pipeline_match is None or near_match is None:
raise RuntimeError(f"readiness run failed: exit={result.returncode} {log}")
pipeline = dict(FIELDS.findall(pipeline_match.group(1)))
row = {
"strate": strate, "workers": workers, "binary": label, "phase": phase,
"replicate": rep, "exit_code": result.returncode,
"wall_s": round(time.monotonic() - start, 3),
"lod02_tiles": f"{near_match.group(1)}/{near_match.group(2)}",
"lod02_ready_s": float(near_match.group(3)),
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
"whole_tiles": pipeline["whole_tiles"].split("/")[0],
"log": str(log.relative_to(ROOT)),
}
print("[L100ReadinessRun] " + json.dumps(row, sort_keys=True), flush=True)
return row
def main() -> int:
global BINARIES, OUT
pair = sys.argv[1:] or ["L98", "L100"]
if len(pair) != 2 or len(set(pair)) != 2 or any(item not in ALL_BINARIES for item in pair):
raise SystemExit("usage: RunL100Readiness.py L98 L100 | L99 L100")
BINARIES = {label: ALL_BINARIES[label] for label in pair}
OUT = OUT_ROOT / f"{pair[0].lower()}-{pair[1].lower()}"
for binary in (*BINARIES.values(), DATA):
if not binary.is_file():
raise SystemExit(f"missing input: {binary}")
rows = []
first, second = tuple(BINARIES)
orders = ((first, second), (second, first), (first, second))
for workers in (8, 12):
for strate in CAMERAS:
for label in BINARIES:
run(strate, workers, label, "warmup", 0)
for strate in CAMERAS:
for rep, order in enumerate(orders, 1):
for label in order:
rows.append(run(strate, workers, label, "measured", rep))
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n",
encoding="utf-8")
summary = []
for workers in (8, 12):
for strate in CAMERAS:
group = [row for row in rows
if row["workers"] == workers and row["strate"] == strate]
item = {"workers": workers, "strate": strate, "measured_pairs": 3}
for label in BINARIES:
subset = [row for row in group if row["binary"] == label]
for metric in ("lod02_ready_s", "whole_view_ready_s"):
values = [row[metric] for row in subset]
item[f"{label}_{metric}_runs"] = values
item[f"{label}_{metric}_median"] = statistics.median(values)
item[f"{label}_process_wall_s_runs"] = [row["wall_s"] for row in subset]
summary.append(item)
(OUT / "summary.json").write_text(json.dumps(
{"comparison_pair": pair, "runs": rows, "summary": summary}, indent=2) + "\n",
encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""L80 protocol with balanced, rotating L98/L99/L100 order."""
from __future__ import annotations
import json
import re
import statistics
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l100/readiness/l98-l99-l100"
DATA = ROOT / "Standalone/data/owner_world.json"
BINARIES = {
"L98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19",
"L99": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang",
"L100": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang",
}
CAMERAS = {
"SW": (8.0, 16.0, -372.2626953125),
"FI": (25.8369140625, 3.19921875, -737.0),
"TN": (19.166015625, -14.2001953125, -753.3740234375),
"VS": (-20.0625, -5.068359375, -900.353515625),
}
PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
NEAR = re.compile(r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) "
r"cold_ready_s=([0-9.]+)$", re.M)
STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M)
def run(strate: str, workers: int, label: str, phase: str, rep: int) -> dict:
OUT.mkdir(parents=True, exist_ok=True)
log = OUT / f"w{workers}-{strate.lower()}-{label.lower()}-{phase}{rep}.log"
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
command = [
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"env", "timeout", "1740s", str(BINARIES[label]), str(DATA),
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
"--max-level=8", f"--camera={camera}", "--profile-lod-work",
"--stream-scheduler-policy=3",
]
start = time.monotonic()
with log.open("w", encoding="utf-8") as stream:
result = subprocess.run(command, cwd=ROOT, stdout=stream,
stderr=subprocess.STDOUT, check=False, timeout=1740)
output = log.read_text(encoding="utf-8", errors="replace")
pipeline_match, near_match, stamp_match = (
PIPELINE.search(output), NEAR.search(output), STAMP.search(output))
if result.returncode or pipeline_match is None or near_match is None or stamp_match is None:
raise RuntimeError(f"readiness run failed: exit={result.returncode} {log}")
pipeline = dict(FIELDS.findall(pipeline_match.group(1)))
row = {
"strate": strate, "workers": workers, "binary": label, "phase": phase,
"replicate": rep, "exit_code": result.returncode,
"source_stamp": stamp_match.group(1), "wall_s": round(time.monotonic() - start, 3),
"lod02_tiles": f"{near_match.group(1)}/{near_match.group(2)}",
"lod02_ready_s": float(near_match.group(3)),
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
"whole_tiles": pipeline["whole_tiles"].split("/")[0],
"log": str(log.relative_to(ROOT)),
}
print("[L100ReadinessTripleRun] " + json.dumps(row, sort_keys=True), flush=True)
return row
def main() -> int:
for binary in (*BINARIES.values(), DATA):
if not binary.is_file():
raise SystemExit(f"missing input: {binary}")
rows = []
labels = tuple(BINARIES)
orders = (labels, labels[1:] + labels[:1], labels[2:] + labels[:2])
for workers in (8, 12):
for strate in CAMERAS:
for label in labels:
run(strate, workers, label, "warmup", 0)
for strate in CAMERAS:
for rep, order in enumerate(orders, 1):
for label in order:
rows.append(run(strate, workers, label, "measured", rep))
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n",
encoding="utf-8")
summary = []
for workers in (8, 12):
for strate in CAMERAS:
group = [row for row in rows if row["workers"] == workers
and row["strate"] == strate]
item = {"workers": workers, "strate": strate, "measured_runs": 3}
for label in labels:
subset = [row for row in group if row["binary"] == label]
for metric in ("lod02_ready_s", "whole_view_ready_s"):
values = [row[metric] for row in subset]
item[f"{label}_{metric}_runs"] = values
item[f"{label}_{metric}_median"] = statistics.median(values)
item[f"{label}_process_wall_s_runs"] = [row["wall_s"] for row in subset]
summary.append(item)
(OUT / "summary.json").write_text(json.dumps(
{"comparison_triple": labels, "runs": rows, "summary": summary}, indent=2) + "\n",
encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Profile L100 swept-tube lookups and direct route fallback work."""
from __future__ import annotations
import json
import re
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l100/sweep-counts"
DATA = ROOT / "Standalone/data/owner_world.json"
HEADLESS = ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang"
CAMERAS = {
"SW": (8.0, 16.0, -372.2626953125),
"FI": (25.8369140625, 3.19921875, -737.0),
"TN": (19.166015625, -14.2001953125, -753.3740234375),
"VS": (-20.0625, -5.068359375, -900.353515625),
}
LINE = re.compile(r"^\[PassageComposerTile\] key=\([^,]+,[^,]+,[^,]+,lod(\d+)\) (.*)$", re.M)
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
COUNT_KEYS = (
"route_candidates", "sweep_segments", "sweep_pairs", "sweep_outside_pairs",
"sweep_min_updates", "sweep_base_samples", "sweep_detail_samples",
"sweep_grid_samples", "sweep_no_influence_samples", "sweep_lookups",
"sweep_cache_hits", "sweep_base_hits", "sweep_outside_hits", "sweep_missing",
"carve_segment_visits", "support_segment_visits", "mouth_route_segment_checks",
)
def run(strate: str) -> dict:
OUT.mkdir(parents=True, exist_ok=True)
log = OUT / f"profile-sweep-{strate.lower()}.log"
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
command = [
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"timeout", "1740s", str(HEADLESS), str(DATA), "--world-assembler-prototype",
"--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3",
"--max-clip-level=8", "--max-level=8", f"--camera={camera}",
"--profile-passage-composer",
]
start = time.monotonic()
with log.open("w", encoding="utf-8") as stream:
result = subprocess.run(command, cwd=ROOT, stdout=stream,
stderr=subprocess.STDOUT, check=False, timeout=1740)
output = log.read_text(encoding="utf-8", errors="replace")
if result.returncode != 0:
raise RuntimeError(f"sweep count run failed: exit={result.returncode} {log}")
rows = []
for level_text, tail in LINE.findall(output):
level = int(level_text)
if level > 2:
continue
fields = dict(FIELDS.findall(tail))
rows.append({"lod": level, **{key: int(fields.get(key, 0)) for key in COUNT_KEYS}})
if not rows:
raise RuntimeError(f"no profiled near-set passage tiles: {log}")
totals = {key: sum(row[key] for row in rows) for key in COUNT_KEYS}
row = {
"strate": strate, "workers": 8, "near_profiled_tiles": len(rows),
"near_tiles_by_lod": {str(level): sum(1 for item in rows if item["lod"] == level)
for level in range(3)},
"counters": totals, "wall_s": round(time.monotonic() - start, 3),
"log": str(log.relative_to(ROOT)),
}
print("[L100SweepCounts] " + json.dumps(row, sort_keys=True), flush=True)
return row
def main() -> int:
rows = [run(strate) for strate in CAMERAS]
(OUT / "summary.json").write_text(json.dumps({"runs": rows}, indent=2) + "\n",
encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,37 @@
# L100 passage captures
Each route has paired L98 and L100 views at both mouths and at 25%, 50%, and 75% stations. The camera gate decodes each PNG and requires visible geometry (>3,000 pixels after excluding the dominant background color, with at least two RGB colors). Changed pixels count RGB pixels with any channel difference between a matched pair.
L100 source stamp: `7224bf6e668d06ed71a92f0857820e3ae2b0eca40574a491331ab5979a88f698`.
| View | L98 geometry px | L100 geometry px | Changed px | Images |
|---|---:|---:|---:|---|
| `0-1 · upper-mouth` | 1,063,522 | 1,063,522 | 41,441 | [L98](passage-0-1-0-upper-mouth-before-l98.png) / [L100](passage-0-1-0-upper-mouth-after-l100.png) |
| `0-1 · lower-mouth` | 1,420,316 | 1,420,316 | 23,644 | [L98](passage-0-1-0-lower-mouth-before-l98.png) / [L100](passage-0-1-0-lower-mouth-after-l100.png) |
| `0-1 · mid-25 (27.5%); station +50 m to surface; surface-target fallback` | 422,920 | 422,935 | 706 | [L98](passage-0-1-0-mid-25-before-l98.png) / [L100](passage-0-1-0-mid-25-after-l100.png) |
| `0-1 · mid-50 (50.0%)` | 1,375,241 | 1,375,118 | 20,082 | [L98](passage-0-1-0-mid-50-before-l98.png) / [L100](passage-0-1-0-mid-50-after-l100.png) |
| `0-1 · mid-75 (75.0%)` | 1,368,802 | 1,368,801 | 12,919 | [L98](passage-0-1-0-mid-75-before-l98.png) / [L100](passage-0-1-0-mid-75-after-l100.png) |
| `1-2 · upper-mouth` | 1,425,620 | 1,426,619 | 292,188 | [L98](passage-1-2-0-upper-mouth-before-l98.png) / [L100](passage-1-2-0-upper-mouth-after-l100.png) |
| `1-2 · lower-mouth` | 223,508 | 223,508 | 57,795 | [L98](passage-1-2-0-lower-mouth-before-l98.png) / [L100](passage-1-2-0-lower-mouth-after-l100.png) |
| `1-2 · mid-25 (25.0%)` | 1,409,914 | 1,409,907 | 15,713 | [L98](passage-1-2-0-mid-25-before-l98.png) / [L100](passage-1-2-0-mid-25-after-l100.png) |
| `1-2 · mid-50 (50.0%)` | 1,407,415 | 1,407,415 | 32,217 | [L98](passage-1-2-0-mid-50-before-l98.png) / [L100](passage-1-2-0-mid-50-after-l100.png) |
| `1-2 · mid-75 (75.0%)` | 1,261,589 | 1,264,705 | 263,048 | [L98](passage-1-2-0-mid-75-before-l98.png) / [L100](passage-1-2-0-mid-75-after-l100.png) |
| `2-3 · upper-mouth` | 60,506 | 60,506 | 0 | [L98](passage-2-3-0-upper-mouth-before-l98.png) / [L100](passage-2-3-0-upper-mouth-after-l100.png) |
| `2-3 · lower-mouth` | 60,257 | 60,257 | 0 | [L98](passage-2-3-0-lower-mouth-before-l98.png) / [L100](passage-2-3-0-lower-mouth-after-l100.png) |
| `2-3 · mid-25 (25.0%); surface-target fallback` | 631,345 | 631,345 | 0 | [L98](passage-2-3-0-mid-25-before-l98.png) / [L100](passage-2-3-0-mid-25-after-l100.png) |
| `2-3 · mid-50 (50.0%); surface-target fallback` | 629,698 | 629,698 | 4,208 | [L98](passage-2-3-0-mid-50-before-l98.png) / [L100](passage-2-3-0-mid-50-after-l100.png) |
| `2-3 · mid-75 (75.0%)` | 1,163,903 | 1,163,903 | 0 | [L98](passage-2-3-0-mid-75-before-l98.png) / [L100](passage-2-3-0-mid-75-after-l100.png) |
## Same-camera 1–2 regression check
The L99 and L100 midpoint records have identical focus, camera, target, yaw, and pitch. This three-version check decodes every image and passes the same geometry gate.
| Version | Image | Visible geometry px | Changed px from L98 |
|---|---|---:|---:|
| L98 | [before](../../Linux99/captures/passage-1-2-0-mid-tunnel-before-l98.png) | 1,407,415 | — |
| L99 | [flat-tunnel capture](../../Linux99/captures/passage-1-2-0-mid-tunnel-after-l99.png) | 1,206,362 | 1,135,959 |
| L100 | [fixed capture](passage-1-2-0-mid-50-after-l100.png) | 1,407,415 | 32,217 |
L99→L100 changed 1,134,150 pixels. The L100 view returns to the L98 image at this camera (L98→L100: 32,217).
All 30 L98/L100 PNGs passed the geometry gate. Capture logs, camera coordinates, exports, and raw metadata are under `Saved/Linux/l100/captures/`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

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