wip(assembler): M1 round 5 -- native SurfaceWorld manifest delivered into the Unreal Core route

Manifest imported into UVoxelSettings (hash 8933693d9560ba9f), folded into the season
content hash only when Core is on. BuildHost: W1/W8/W16 stable, 2553 surface tiles match
standalone, 432 LOD transitions, collision on all 36 expected tiles. Matched timing
96.8 s native vs 197.3 s bridge (2.04x). Collision support tiles wait for Core region admission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-09 15:31:48 +02:00
1 parent 2d0cec90e4
commit f67b519e75
11 files changed
+1002 -45

No files matched your search

+61 -13
View File
@@ -1157,8 +1157,8 @@ fidelity fallback while a native heightfield with fixed-order noise and per-colu
caching is developed. caching is developed.
The probe table in the [integration report](Standalone/UNREAL_INTEGRATION_REPORT.md) covers all The probe table in the [integration report](Standalone/UNREAL_INTEGRATION_REPORT.md) covers all
eight generator types, five passage types, four passage styles, and three transitions. Current route eight generator types, five passage types, four passage styles, and three transitions. These are the
choices for the next implementation pass: round-4 recommendations; the SurfaceWorld row is superseded by §8.20:
| Field | Recommended Core route | Reason and current gap | | Field | Recommended Core route | Reason and current gap |
|---|---|---| |---|---|---|
@@ -1172,19 +1172,20 @@ choices for the next implementation pass:
| Passage types and styles | Keep the generated control-point chains, but preserve each type's radius/floor contract and assert walkability. | The broad chain is already available. Worm and Cascading have solid centerline samples in the current Core audit. | | Passage types and styles | Keep the generated control-point chains, but preserve each type's radius/floor contract and assert walkability. | The broad chain is already available. Worm and Cascading have solid centerline samples in the current Core audit. |
| Strate transitions | Implement the three boundary functions in the Core layer field. | Gradient, Hard, and Interleaved produced different owner hashes but the same Core hash in the probe. | | Strate transitions | Implement the three boundary functions in the Core layer field. | Gradient, Hard, and Interleaved produced different owner hashes but the same Core hash in the probe. |
The copied evaluator remains a reasonable temporary route when exact density is required quickly; The copied evaluator remains a compatibility route when exact density is required and no reviewed
for low-cost discrete fields such as slabs, mazes, shafts, and islands, native support keeps the native manifest is attached. For low-cost discrete fields such as slabs, mazes, shafts, and islands,
worker path explicit and avoids charging every sample for a second field implementation. BuildHost native support keeps the worker path explicit and avoids charging every sample for a second field
continues to verify the opt-in Unreal route and its 25 cm Core scale; it does not certify the missing implementation. BuildHost verifies the opt-in Unreal route and its 25 cm Core scale; it does not
archetypes. certify the missing archetypes.
**Standalone SurfaceWorld M1 progress (2026-10-09):** `WorldAssemblyManifest` evaluator 4 now **Standalone SurfaceWorld M1 progress (2026-10-09):** `WorldAssemblyManifest` evaluator 4 now
serializes a finite-strate surface route with stable waypoint IDs, a 0.34 m × 1.76 m player capsule, serializes a finite-strate surface route with stable waypoint IDs, a 0.34 m × 1.76 m player capsule,
a 15° grade limit and underfoot support constraints. The final composed field must pass a capsule a 15° grade limit and underfoot support constraints. The final composed field must pass a capsule
sweep before a route candidate is accepted. Cliff shelves carry a two-Core-voxel minimum thickness. sweep before a route candidate is accepted. Cliff shelves carry a two-Core-voxel minimum thickness.
The route law applies to generation and automatic changes; player carve/fill edits remain unrestricted. The route law applies to generation and automatic changes; player carve/fill edits remain unrestricted.
This is standalone Core evidence; it is not yet connected to the season asset/hash path or reviewed in At the end of round 4 this was standalone evidence, not yet connected to the settings/content-hash
the owner's game. path. Round 5 connects the reviewed manifest to `UVoxelSettings`; see §8.20. The owner still needs to
build and review it in his game.
The round-4 horizon audit traced the false low slab to the LOD8 three-dimensional marching-cubes cap The round-4 horizon audit traced the false low slab to the LOD8 three-dimensional marching-cubes cap
surface sampled on a 256-voxel lattice; the Core mesh's cap classification also consulted the legacy surface sampled on a 256-voxel lattice; the Core mesh's cap classification also consulted the legacy
@@ -1195,6 +1196,51 @@ accepts `--frame-limit=1..60`, including while its window is occluded. See
[`Standalone/ASSEMBLER_M1_REPORT.md`](Standalone/ASSEMBLER_M1_REPORT.md) for the matched up-and-out [`Standalone/ASSEMBLER_M1_REPORT.md`](Standalone/ASSEMBLER_M1_REPORT.md) for the matched up-and-out
captures, cap tile counts, determinism hashes, route sweep and query/collision readiness split. captures, cap tile counts, determinism hashes, route sweep and query/collision readiness split.
### 8.20 Native SurfaceWorld manifest in Unreal — integration round 5
`UVoxelSettings` now owns optional canonical world-assembly JSON plus its Core digest. In the editor,
`SourceWorldAssemblyManifestJson` selects the reviewed JSON and `ImportWorldAssemblyManifestJson`
validates, canonicalizes and stores it in the cooked settings asset. Import rejects a recipe seed that
differs from the effective settings seed. `GetCurrentSeasonContentHash()` includes the assembly
digest only when `bUseVoxelForgeCore` is enabled; with Core disabled, it returns the existing season
hash before reading optional assembly data. That keeps the legacy world identity stable on switch-off.
During Core initialization, `FVoxelForgeCoreAdapter` validates the manifest seed and requires each
planned stratum to match exactly one resolved `SurfaceWorld` slot by stratum index and top/bottom
chunk bounds. It inserts the canonical manifest and hash into the Core snapshot before the snapshot
is moved to Core. A missing manifest leaves the existing cave-only Core layout untouched; malformed,
wrong-seed, unmatched, duplicate or incomplete plans fail initialization. Core workers still receive
only the immutable resolved snapshot, not UObjects.
Core mesh/collision ordering has one SurfaceWorld-specific guard: a collision-support tile requested
outside the current desired set is promoted into the next desired rebuild, then deferred until Core
region compilation admits it. This prevents an early isolated support job from racing the native
region compile and publishing a tile whose hash differs from the standalone region snapshot. The
existing `Epoch` and MPSC result paths remain in force, and collision bodies are cooked by Unreal's
RealtimeMesh/Chaos route on the game side.
BuildHost passed the reviewed manifest `8933693d9560ba9f` (recipe `c37de78b759c0a4c`, plan
`1ca84619c8600ee5`) with W1/W8/W16 determinism, cave streaming, anchor movement and carve. At the
surface travel point `(37,165,-2011)` Core voxels, 2,553 expected visual hashes matched independent
standalone Core output; the check covered 573 local tiles, 3,438 adjacent faces and 432 LOD
transitions. Collision expectations covered 125 near tiles: all 36 non-empty cases had cooked bodies
and all 89 empty cases had none. The native field sampler visited 4,096 points over 3,200 voxels and
found three biome IDs. The complete pass log is under `Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/`.
The switch-off audit was a source-level branch/hash review, not a flag-off actor replay. It confirms
the legacy generation branch and previous content hash remain unchanged when `bUseVoxelForgeCore`
is false. Same-session BuildHost timing on the same 2,553 keys with hashes enabled was 96.816887 s
native versus 197.346153 s through the legacy bridge, including region compilation (2.038× native
speedup). This is serial headless tile generation. The final actor rerun passed in 9m20s with a
3.61 GiB peak sampled private working set. The approximately 1.57 s full-view aim is still
unmet: the last matched 529-tile LOD8 stream took 4.446 s. No viewer benchmark ran in round 5.
The owner handoff sets the manifest on the existing `DA_Settings` asset (seed 0), saves it, and
enables `AVoxelWorld::bUseVoxelForgeCore`. Coordinates and expected surface/coast/cliff/cap views are
in [`Standalone/ASSEMBLER_M1_REPORT.md`](Standalone/ASSEMBLER_M1_REPORT.md). BuildHost does not
replace the owner's project build or visual walk; cap lighting and the pointed native ridge peaks
remain owner review items.
## 9. Multiplayer model (listen-server first, dedicated-friendly) ## 9. Multiplayer model (listen-server first, dedicated-friendly)
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
@@ -1207,10 +1253,12 @@ captures, cap tile counts, determinism hashes, route sweep and query/collision r
The world is a pure function of **(seed, strate layout)** (§8.4). So terrain is reconstructed identically The world is a pure function of **(seed, strate layout)** (§8.4). So terrain is reconstructed identically
on every peer from a tiny amount of shared state — it is **never streamed as geometry over the wire**: on every peer from a tiny amount of shared state — it is **never streamed as geometry over the wire**:
- Replicate the **effective seed + strate layout identity** ONCE (at join). For a cooked season the - Replicate the **effective seed + strate layout identity** ONCE (at join). For a cooked season the
layout is the reviewed manifest, not a pool shuffle: peers must load the same season asset and compare layout is the reviewed season manifest; when Core is enabled with a native SurfaceWorld, the
`AVoxelWorld::GetCurrentSeasonContentHash()` before accepting play. For the legacy path the layout remains reviewed world-assembly digest is also part of `AVoxelWorld::GetCurrentSeasonContentHash()`. Peers
deterministic from the effective seed. Every peer then generates terrain locally; geometry never crosses compare that active content hash before accepting play. With Core disabled the optional assembly
the wire. The transport/RPC that performs this join handshake remains out of tree. does not change the legacy hash. For the legacy path, layout remains deterministic from the
effective seed. Every peer generates terrain locally; geometry never crosses the wire. The
transport/RPC that performs this join handshake remains out of tree.
- The **diff layer is the ONLY non-deterministic terrain state** (§3.9, [[voxelforge-difflayer-threading]]) - The **diff layer is the ONLY non-deterministic terrain state** (§3.9, [[voxelforge-difflayer-threading]])
→ it is the only thing that must sync. Since carving is a minor feature, this traffic is small. → it is the only thing that must sync. Since carving is a minor feature, this traffic is small.
+24 -9
View File
@@ -145,7 +145,7 @@ remains `0`, and lateral regions remain gated off.
| File | Role | | File | Role |
|------|------| |------|------|
| `../../VoxelForge.uplugin` | Plugin manifest. Runtime modules `VoxelForge` and `VoxelForgeCore`, plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. | | `../../VoxelForge.uplugin` | Plugin manifest. Runtime modules `VoxelForge` and `VoxelForgeCore`, plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. |
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**, `VoxelForgeCore`. The Core dependency is linked; `AVoxelWorld` does not route through it yet. | | `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**, `VoxelForgeCore`. `AVoxelWorld::bUseVoxelForgeCore` opts into the Core runtime route; it defaults off. |
| `../../VoxelForgeCore/VoxelForgeCore.Build.cs` | C++20 runtime module. Reads `Standalone/Core/core_sources.txt`, validates one include shim per canonical source, and fails UBT on missing, extra, or mismatched shims. No unity/PCH; private copied implementation stays under `vfcore::detail`. | | `../../VoxelForgeCore/VoxelForgeCore.Build.cs` | C++20 runtime module. Reads `Standalone/Core/core_sources.txt`, validates one include shim per canonical source, and fails UBT on missing, extra, or mismatched shims. No unity/PCH; private copied implementation stays under `vfcore::detail`. |
| `../../VoxelForgeCore/Private/Manifest/VFCore_*.cpp` | Eighteen one-source UBT shims that include canonical files under `Standalone/`; they do not copy Core implementation. The API-boundary row includes `Core/src/VoxelForgeCore.cpp` directly. | | `../../VoxelForgeCore/Private/Manifest/VFCore_*.cpp` | Eighteen one-source UBT shims that include canonical files under `Standalone/`; they do not copy Core implementation. The API-boundary row includes `Core/src/VoxelForgeCore.cpp` directly. |
| `../../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. |
@@ -153,7 +153,9 @@ remains `0`, and lateral regions remain gated off.
| `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. | `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. | | 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`; also depends on `VoxelForgeCore` for the schema-v1 owner-asset export check. `FStandaloneCoreAdapterScheduler` runs Core compile work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability without touching the game runtime. This is validation infrastructure, not the `AVoxelWorld` route. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. | | `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; schema-v1 owner-asset export check and BuildHost Core actor tests. `-coreworldassembly=PATH` supplies a reviewed manifest transiently to `-coreworldtest=1`; it does not save a data asset. `FStandaloneCoreAdapterScheduler` runs Core work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. |
| `Source/VoxelForge/Public/VoxelSettings.h` / `Private/VoxelSettings.cpp` | `UVoxelSettings` owns optional canonical world-assembly JSON and its Core digest. The editor file picker plus `ImportWorldAssemblyManifestJson` imports the reviewed manifest into the cooked settings asset and validates the recipe seed against the effective settings seed. |
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Validates the optional manifest against each resolved `SurfaceWorld` slot (unique strate index and exact top/bottom chunks), then places canonical `world_assembly` JSON and its hash in the Core snapshot. Invalid plans fail Core initialization. |
| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. | | `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. |
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. | | `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. |
| `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. | | `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. |
@@ -381,7 +383,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
| `ApplyModification` | 10248 | Single funnel for all brushes: DiffLayer → when a negative brush intersects the pawn's capsule bounds and affects its ready support tile, disable that old body and withdraw readiness before remesh → **sync-remesh the brush-centre level-0 tile** (`SyncRemeshTile`, instant hole; skipped if that tile is mid-gen — would race a stale in-flight result) → `RemeshDirtyChunks(..., ExcludeTile)` for the neighbours → `RemoveDecorationsInSphere`. A successful replacement cook restores collision/readiness; neighboring edits that miss the capsule bounds keep the same-tile recook handoff. | | `ApplyModification` | 10248 | Single funnel for all brushes: DiffLayer → when a negative brush intersects the pawn's capsule bounds and affects its ready support tile, disable that old body and withdraw readiness before remesh → **sync-remesh the brush-centre level-0 tile** (`SyncRemeshTile`, instant hole; skipped if that tile is mid-gen — would race a stale in-flight result) → `RemeshDirtyChunks(..., ExcludeTile)` for the neighbours → `RemoveDecorationsInSphere`. A successful replacement cook restores collision/readiness; neighboring edits that miss the capsule bounds keep the same-tile recook handoff. |
| `ClearAllModifications` | 8696 | Clears diff layer, regenerates. | | `ClearAllModifications` | 8696 | Clears diff layer, regenerates. |
| `ChangeSeed` | 8710 | **Legacy season reset**: new seed everywhere, clear diffs, bump season, reload. Rejected while a cooked season owns the seed. | | `ChangeSeed` | 8710 | **Legacy season reset**: new seed everywhere, clear diffs, bump season, reload. Rejected while a cooked season owns the seed. |
| `GetCurrentSeed` / `GetCurrentSeason` / `GetCurrentSeasonContentHash` | — | Effective cooked-or-legacy identity; the hash is the join-time stale-season comparison value. | | `GetCurrentSeed` / `GetCurrentSeason` / `GetCurrentSeasonContentHash` | — | Effective cooked-or-legacy identity; the hash is the join-time stale-season comparison value. The optional world-assembly digest joins the hash only while `bUseVoxelForgeCore` is active, so switch-off retains the previous legacy hash. |
| `RemeshDirtyChunks` | 8801 | Queue loaded level-0 dirty tiles onto `DirtyRemeshQueue` (async re-mesh, no pop) + `MarkDirtyVoxelBox` the volume. Optional `ExcludeTile` = the sync'd centre. Drained FIRST in the submit loop at **BackgroundHigh** (ahead of streaming/band) so a dig never waits behind streaming; in-flight tiles stay QUEUED (not dropped) so a stale pre-carve result is corrected once it lands — fixes "hole shows up a beat late / not until I move". | | `RemeshDirtyChunks` | 8801 | Queue loaded level-0 dirty tiles onto `DirtyRemeshQueue` (async re-mesh, no pop) + `MarkDirtyVoxelBox` the volume. Optional `ExcludeTile` = the sync'd centre. Drained FIRST in the submit loop at **BackgroundHigh** (ahead of streaming/band) so a dig never waits behind streaming; in-flight tiles stay QUEUED (not dropped) so a stale pre-carve result is corrected once it lands — fixes "hole shows up a beat late / not until I move". |
> **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_ClassifyTile` (T1.d) / `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)". > **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_ClassifyTile` (T1.d) / `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)".
@@ -820,7 +822,7 @@ second density path.
| `VoxelForgeWormBlockSkipTest.cpp` | `VoxelForge.Determinism.WormBlockSkipSoundness` | Soundness and coverage of the worker-local worm N1 block proof (`voxel.WormBlockSkip`). | | `VoxelForgeWormBlockSkipTest.cpp` | `VoxelForge.Determinism.WormBlockSkipSoundness` | Soundness and coverage of the worker-local worm N1 block proof (`voxel.WormBlockSkip`). |
| `VoxelForgePlayerFitWindow.h` | — | Shared measurement-window policy for player-fit diagnostics (test/editor side only). | | `VoxelForgePlayerFitWindow.h` | — | Shared measurement-window policy for player-fit diagnostics (test/editor side only). |
### 3.13 Core runtime integration (rounds 2–4) ### 3.13 Core runtime integration (rounds 2–5)
The public Core API is linked as the `VoxelForgeCore` runtime module, with canonical implementation The public Core API is linked as the `VoxelForgeCore` runtime module, with canonical implementation
sources still under `Standalone/`. UBT validates the 18-row manifest and its shims; BuildHost linked sources still under `Standalone/`. UBT validates the 18-row manifest and its shims; BuildHost linked
the module and passed `CheckCoreSymbols.ps1` against the actual shim objects. See the module and passed `CheckCoreSymbols.ps1` against the actual shim objects. See
@@ -852,12 +854,15 @@ affected keys under a new epoch. Existing streaming anchors participate; the hea
covers `CollisionOnly` policy. covers `CollisionOnly` policy.
`-coreworldtest=1` in the BuildHost editor commandlet validates initial tiles, expected collision `-coreworldtest=1` in the BuildHost editor commandlet validates initial tiles, expected collision
bodies/empty tiles, anchor movement, a carve transaction, remote travel to the B landing and surface bodies/empty tiles, anchor movement, a carve transaction, travel to the B landing and native surface
strate, full desired visual-key coverage, neighbour/LOD face coverage, and W1/W8/W16 hash parity. strate, full desired visual-key coverage, neighbour/LOD face coverage, and W1/W8/W16 hash parity.
It also probes the finite origin landing room and Core-mode biome query. `VoxelForge.Determinism.CrossPlatformDigest` `-coreworldassembly=PATH` supplies a reviewed manifest transiently for this test. The native surface
is the narrow switch-off baseline; it passes, but does not instantiate an actor with the switch false. probe samples `Heightfield2D` directly from the Core snapshot; every one of its 2,553 expected visual
See the report for that limitation, the changed world hashes, classification evidence, and owner-editor tile hashes is compared with an independent standalone Core reference. The 125 collision-desired
instructions. The Core route is opt-in; the legacy path remains the default. surface tiles are checked for expected cooked bodies or empty geometry. `VoxelForge.Determinism.CrossPlatformDigest`
remains the standalone legacy digest check. Switch-off was reviewed at the source branch and hash
boundary; there is no separate flag-off actor replay. See the report for exact hashes, timing, run
ledger, and owner-editor instructions. Core remains opt-in and the legacy path remains default.
Round 4 adds a temporary SurfaceWorld density bridge in Round 4 adds a temporary SurfaceWorld density bridge in
[ShapeWorldInternal.h](Standalone/Core/src/ShapeWorldInternal.h). SurfaceWorld samples, including [ShapeWorldInternal.h](Standalone/Core/src/ShapeWorldInternal.h). SurfaceWorld samples, including
@@ -867,6 +872,16 @@ tested LOD0–LOD3 tiles, and the BuildHost actor layout identifies Biome_Surfac
travel point. The CPU bridge costs approximately the legacy field route: 23.085 s wall / 180.562 travel point. The CPU bridge costs approximately the legacy field route: 23.085 s wall / 180.562
worker-seconds for the matched 1,294-tile CPU view. worker-seconds for the matched 1,294-tile CPU view.
Round 5 adds the native surface route. `UVoxelSettings` stores canonical reviewed manifest JSON and
its Core digest. The `SourceWorldAssemblyManifestJson` file picker plus
`ImportWorldAssemblyManifestJson` editor button populate the cooked data asset. The Core adapter
requires an exact unique match for every planned `SurfaceWorld` stratum and adds the canonical plan
to the resolved Core snapshot. `GetCurrentSeasonContentHash()` includes this digest only while
`bUseVoxelForgeCore` is enabled; disabling Core retains the legacy generation route and legacy
content hash. The collision gate defers out-of-desired-set Core support tiles until Core region
compilation admits them, so collision work cannot publish a competing tile before the native
surface region snapshot. Existing cave Core strates still use the no-manifest snapshot path.
[OwnerWorldLoader.h](Standalone/Source/OwnerWorldLoader.h) now reads SlabParams, MazeParams, [OwnerWorldLoader.h](Standalone/Source/OwnerWorldLoader.h) now reads SlabParams, MazeParams,
VerticalShaftParams, and FloatingIslandParams. The shape-world evaluator still lacks their native VerticalShaftParams, and FloatingIslandParams. The shape-world evaluator still lacks their native
terrain fields: FlatPlain, CrystalChamber, Maze, VerticalShafts, FloatingIslands, and water terrain fields: FlatPlain, CrystalChamber, Maze, VerticalShafts, FloatingIslands, and water
@@ -2,9 +2,12 @@
#include "Dom/JsonObject.h" #include "Dom/JsonObject.h"
#include "Policies/CondensedJsonPrintPolicy.h" #include "Policies/CondensedJsonPrintPolicy.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h" #include "Serialization/JsonWriter.h"
#include "UObject/UnrealType.h" #include "UObject/UnrealType.h"
#include "UObject/TextProperty.h" #include "UObject/TextProperty.h"
#include <VoxelForgeCore/VoxelForgeCore.h>
#include "VoxelSeasonAsset.h" #include "VoxelSeasonAsset.h"
#include "VoxelSettings.h" #include "VoxelSettings.h"
#include "VoxelStrateDefinition.h" #include "VoxelStrateDefinition.h"
@@ -271,6 +274,95 @@ void WriteCoreJsonValue(FCoreJsonWriter& Writer, const TCHAR* Identifier,
Writer.WriteNull(); Writer.WriteNull();
} }
} }
bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
const UVoxelStrateManager* StrateManager,
const TSharedRef<FJsonObject>& Root,
FString& OutError)
{
FString ManifestJson;
FString ManifestHash;
if (!Settings->GetWorldAssemblyManifestJson(ManifestJson, ManifestHash, OutError))
{
return false;
}
if (ManifestJson.IsEmpty())
{
return true;
}
FTCHARToUTF8 Utf8Json(*ManifestJson);
std::string CoreError;
const std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest =
VoxelForgeCore::WorldAssemblyManifest::FromJson(
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
if (!Manifest)
{
OutError = FString::Printf(TEXT("Core world assembly validation failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
if (Manifest->recipe.seed != static_cast<uint32>(StrateManager->GetWorldSeed()))
{
OutError = TEXT("Core world assembly recipe seed does not match the resolved strate layout.");
return false;
}
int32 SurfaceSlotCount = 0;
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
if (Slot.Definition != nullptr
&& Slot.Definition->GeneratorType == ECaveGeneratorType::SurfaceWorld)
{
++SurfaceSlotCount;
}
}
if (SurfaceSlotCount == 0
|| Manifest->plan.strates.size() != static_cast<std::size_t>(SurfaceSlotCount))
{
OutError = FString::Printf(
TEXT("Core world assembly has %d plans for %d resolved SurfaceWorld strates."),
static_cast<int32>(Manifest->plan.strates.size()), SurfaceSlotCount);
return false;
}
TSet<int32> MatchedStrateIndices;
for (const VoxelForgeCore::WorldAssemblyStratumPlanV1& Plan : Manifest->plan.strates)
{
const FStrateSlot* MatchingSlot = StrateManager->GetLayout().FindByPredicate(
[&Plan](const FStrateSlot& Slot) { return Slot.StrateIndex == Plan.strateIndex; });
if (MatchingSlot == nullptr || MatchingSlot->Definition == nullptr
|| MatchingSlot->Definition->GeneratorType != ECaveGeneratorType::SurfaceWorld
|| MatchingSlot->TopChunkZ != Plan.topChunkZ
|| MatchingSlot->BottomChunkZ != Plan.bottomChunkZ
|| MatchedStrateIndices.Contains(Plan.strateIndex))
{
OutError = FString::Printf(
TEXT("Core world assembly plan for strate %d does not exactly match a unique resolved SurfaceWorld slot."),
Plan.strateIndex);
return false;
}
MatchedStrateIndices.Add(Plan.strateIndex);
}
const std::string CanonicalJson = Manifest->ToJson();
if (CanonicalJson.empty())
{
OutError = TEXT("Core world assembly manifest could not be canonicalized for the snapshot.");
return false;
}
const FString CanonicalText = UTF8_TO_TCHAR(CanonicalJson.c_str());
TSharedPtr<FJsonObject> ManifestObject;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(CanonicalText);
if (!FJsonSerializer::Deserialize(Reader, ManifestObject) || !ManifestObject.IsValid())
{
OutError = TEXT("Core world assembly canonical JSON could not be embedded in the snapshot.");
return false;
}
Root->SetObjectField(TEXT("world_assembly"), ManifestObject.ToSharedRef());
Root->SetStringField(TEXT("world_assembly_content_hash"), ManifestHash);
return true;
}
} }
bool VoxelForgeCoreAdapter::SerializeResolvedWorld( bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
@@ -380,6 +472,10 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
Passages.Add(MakeShared<FJsonValueObject>(CorePassageJson(Passage))); Passages.Add(MakeShared<FJsonValueObject>(CorePassageJson(Passage)));
} }
Root->SetArrayField(TEXT("passages"), MoveTemp(Passages)); Root->SetArrayField(TEXT("passages"), MoveTemp(Passages));
if (!AttachValidatedWorldAssembly(Settings, StrateManager, Root, OutError))
{
return false;
}
Root->SetArrayField(TEXT("density_samples"), {}); Root->SetArrayField(TEXT("density_samples"), {});
TSharedRef<FJsonObject> ViewerStart = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> ViewerStart = MakeShared<FJsonObject>();
ViewerStart->SetBoolField(TEXT("found_open_space"), false); ViewerStart->SetBoolField(TEXT("found_open_space"), false);
+127
View File
@@ -2,6 +2,13 @@
#include "VoxelSeasonAsset.h" #include "VoxelSeasonAsset.h"
#if WITH_EDITOR
#include "Misc/FileHelper.h"
#endif
#include <VoxelForgeCore/VoxelForgeCore.h>
#include <string>
int32 UVoxelSettings::GetEffectiveWorldSeed() const int32 UVoxelSettings::GetEffectiveWorldSeed() const
{ {
if (!Season.IsNull()) if (!Season.IsNull())
@@ -49,3 +56,123 @@ int32 UVoxelSettings::GetEffectiveSeasonNumber() const
} }
return CurrentSeason; return CurrentSeason;
} }
bool UVoxelSettings::SetWorldAssemblyManifestJson(const FString& InJson, FString& OutReport)
{
OutReport.Reset();
if (InJson.IsEmpty())
{
OutReport = TEXT("world assembly manifest JSON is empty");
return false;
}
FTCHARToUTF8 Utf8Json(*InJson);
std::string CoreError;
const std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest =
VoxelForgeCore::WorldAssemblyManifest::FromJson(
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
if (!Manifest)
{
OutReport = FString::Printf(TEXT("world assembly manifest is invalid: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
if (Manifest->recipe.seed != static_cast<uint32>(GetEffectiveWorldSeed()))
{
OutReport = FString::Printf(
TEXT("world assembly seed %u does not match the effective settings seed %d"),
Manifest->recipe.seed, GetEffectiveWorldSeed());
return false;
}
const std::string CanonicalJson = Manifest->ToJson();
if (CanonicalJson.empty())
{
OutReport = TEXT("world assembly manifest could not be canonicalized");
return false;
}
Modify();
WorldAssemblyManifestJson = UTF8_TO_TCHAR(CanonicalJson.c_str());
WorldAssemblyManifestHash = FString::Printf(TEXT("%016llx"),
static_cast<unsigned long long>(Manifest->contentHash));
MarkPackageDirty();
OutReport = FString::Printf(TEXT("imported native world assembly %s (%d stratum plans)"),
*WorldAssemblyManifestHash, static_cast<int32>(Manifest->plan.strates.size()));
return true;
}
bool UVoxelSettings::GetWorldAssemblyManifestJson(
FString& OutCanonicalJson, FString& OutContentHash, FString& OutError) const
{
OutCanonicalJson.Reset();
OutContentHash.Reset();
OutError.Reset();
if (WorldAssemblyManifestJson.IsEmpty())
{
if (!WorldAssemblyManifestHash.IsEmpty())
{
OutError = TEXT("world assembly hash is set but its manifest JSON is empty");
return false;
}
return true;
}
FTCHARToUTF8 Utf8Json(*WorldAssemblyManifestJson);
std::string CoreError;
const std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest =
VoxelForgeCore::WorldAssemblyManifest::FromJson(
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
if (!Manifest)
{
OutError = FString::Printf(TEXT("cooked world assembly manifest is invalid: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
if (Manifest->recipe.seed != static_cast<uint32>(GetEffectiveWorldSeed()))
{
OutError = TEXT("cooked world assembly seed no longer matches the effective settings seed");
return false;
}
const FString ComputedHash = FString::Printf(TEXT("%016llx"),
static_cast<unsigned long long>(Manifest->contentHash));
if (!ComputedHash.Equals(WorldAssemblyManifestHash, ESearchCase::IgnoreCase))
{
OutError = FString::Printf(
TEXT("cooked world assembly hash mismatch (stored %s, computed %s)"),
*WorldAssemblyManifestHash, *ComputedHash);
return false;
}
const std::string CanonicalJson = Manifest->ToJson();
if (CanonicalJson.empty())
{
OutError = TEXT("cooked world assembly manifest could not be canonicalized");
return false;
}
OutCanonicalJson = UTF8_TO_TCHAR(CanonicalJson.c_str());
OutContentHash = ComputedHash;
return true;
}
void UVoxelSettings::ImportWorldAssemblyManifestJson()
{
#if WITH_EDITOR
FString JsonText;
FString Report;
if (SourceWorldAssemblyManifestJson.FilePath.IsEmpty()
|| !FFileHelper::LoadFileToString(JsonText, *SourceWorldAssemblyManifestJson.FilePath))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelSettings] Could not read world assembly manifest '%s'."),
*SourceWorldAssemblyManifestJson.FilePath);
return;
}
if (!SetWorldAssemblyManifestJson(JsonText, Report))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelSettings] World assembly import failed: %s"), *Report);
return;
}
UE_LOG(LogTemp, Log, TEXT("[VoxelSettings] %s"), *Report);
#else
UE_LOG(LogTemp, Error, TEXT("[VoxelSettings] World assembly JSON import is editor-only."));
#endif
}
+63 -1
View File
@@ -40,6 +40,7 @@
#include "Engine/World.h" #include "Engine/World.h"
#include "Misc/IQueuedWork.h" #include "Misc/IQueuedWork.h"
#include "Misc/QueuedThreadPool.h" #include "Misc/QueuedThreadPool.h"
#include "Misc/SecureHash.h"
#include "RealtimeMeshCollisionLibrary.h" #include "RealtimeMeshCollisionLibrary.h"
#include "Camera/PlayerCameraManager.h" #include "Camera/PlayerCameraManager.h"
#include "Async/Async.h" #include "Async/Async.h"
@@ -7259,6 +7260,15 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
UTF8_TO_TCHAR(CoreError.c_str())); UTF8_TO_TCHAR(CoreError.c_str()));
return false; return false;
} }
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreRuntime] world_assembly_hash=%016llx recipe_hash=%016llx plan_hash=%016llx strates=%d"),
static_cast<unsigned long long>(Assembly->contentHash),
static_cast<unsigned long long>(Assembly->recipe.contentHash),
static_cast<unsigned long long>(Assembly->plan.contentHash),
static_cast<int32>(Assembly->plan.strates.size()));
}
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State =
MakeShared<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe>(); MakeShared<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe>();
@@ -7786,6 +7796,18 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
} }
bForceDesiredRebuild |= bNeedsDesiredRebuild; bForceDesiredRebuild |= bNeedsDesiredRebuild;
// Core surface tiles must enter through the desired-set region compile before collision work.
// Les tuiles de surface Core doivent passer par la compilation de région avant la collision.
if (bUseVoxelForgeCore && !bDesiredAtRequest)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGate] support_request_deferred tile=(%d,%d,%d) "
"reason=core_region_admission desired=0"),
SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z);
bPawnGateSupportRequestReported = true;
return;
}
// Preserve the established critical floor ordering when it already belongs to this desired // Preserve the established critical floor ordering when it already belongs to this desired
// epoch. Before the first build there is no array to promote into; LoadTile below still uses // epoch. Before the first build there is no array to promote into; LoadTile below still uses
// the same CollisionCritical work class and priority queue. // the same CollisionCritical work class and priority queue.
@@ -13222,7 +13244,47 @@ int32 AVoxelWorld::GetCurrentSeason() const
FString AVoxelWorld::GetCurrentSeasonContentHash() const FString AVoxelWorld::GetCurrentSeasonContentHash() const
{ {
return StrateManager ? StrateManager->GetSeasonContentHash() : FString(); const FString SeasonHash = StrateManager ? StrateManager->GetSeasonContentHash() : FString();
// Only active Core generation includes the optional native surface manifest in world identity.
// Seul le Core actif inclut le manifeste de surface natif dans l'identite du monde.
if (!Settings || !bUseVoxelForgeCore)
{
return SeasonHash;
}
FString AssemblyJson;
FString AssemblyHash;
FString AssemblyError;
if (!Settings->GetWorldAssemblyManifestJson(AssemblyJson, AssemblyHash, AssemblyError))
{
return FString();
}
if (AssemblyHash.IsEmpty())
{
return SeasonHash;
}
FString Payload = SeasonHash;
if (Payload.IsEmpty())
{
Payload = FString::Printf(TEXT("seed=%d"), StrateManager ? StrateManager->GetWorldSeed()
: Settings->GetEffectiveWorldSeed());
if (StrateManager)
{
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
Payload += FString::Printf(TEXT("|strate=%d,%d,%d,%s"), Slot.StrateIndex,
Slot.TopChunkZ, Slot.BottomChunkZ,
Slot.Definition ? *Slot.Definition->GetPathName() : TEXT("missing"));
}
}
}
Payload += TEXT("|world_assembly=");
Payload += AssemblyHash;
FTCHARToUTF8 Utf8Payload(*Payload);
uint8 Digest[FSHA1::DigestSize];
FSHA1::HashBuffer(Utf8Payload.Get(), Utf8Payload.Length(), Digest);
return BytesToHex(Digest, FSHA1::DigestSize);
} }
//============================================================================= //=============================================================================
+23
View File
@@ -28,6 +28,14 @@ public:
float GetEffectiveWorldRadiusVoxels() const; float GetEffectiveWorldRadiusVoxels() const;
int32 GetEffectiveSeasonNumber() const; int32 GetEffectiveSeasonNumber() const;
/** Import a reviewed native SurfaceWorld plan; empty preserves the existing Core/legacy routes. */
VOXELFORGE_API bool SetWorldAssemblyManifestJson(const FString& InJson, FString& OutReport);
VOXELFORGE_API bool GetWorldAssemblyManifestJson(FString& OutCanonicalJson, FString& OutContentHash,
FString& OutError) const;
UFUNCTION(CallInEditor, Category = "Voxel|Strates")
void ImportWorldAssemblyManifestJson();
//========================================================================= //=========================================================================
// STREAMING (distance de vue) // STREAMING (distance de vue)
//========================================================================= //=========================================================================
@@ -365,6 +373,21 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Voxel|Strates") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Voxel|Strates")
TSoftObjectPtr<UVoxelSeasonAsset> Season; TSoftObjectPtr<UVoxelSeasonAsset> Season;
// Optional reviewed Core surface plan. The canonical JSON stays in this cooked settings asset;
// its Core FNV digest is included in GetCurrentSeasonContentHash when present.
// Plan de surface Core facultatif; le JSON canonique est cuit avec les reglages et son hash
// Core est inclus dans GetCurrentSeasonContentHash lorsqu'il est present.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Voxel|Strates", meta = (MultiLine = true))
FString WorldAssemblyManifestJson;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Voxel|Strates")
FString WorldAssemblyManifestHash;
#if WITH_EDITORONLY_DATA
UPROPERTY(EditAnywhere, Category = "Voxel|Strates|World Assembly", meta = (FilePathFilter = "json"))
FFilePath SourceWorldAssemblyManifestJson;
#endif
// Numéro de saison (incrementé par ChangeSeed). Purement informatif côté gameplay. // Numéro de saison (incrementé par ChangeSeed). Purement informatif côté gameplay.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
int32 CurrentSeason = 1; int32 CurrentSeason = 1;
@@ -66,6 +66,7 @@
#include <atomic> #include <atomic>
#include <array> #include <array>
#include <chrono> #include <chrono>
#include <cmath>
#include <cstdint> #include <cstdint>
#include <limits> #include <limits>
#include <string> #include <string>
@@ -341,6 +342,7 @@ struct FExploreArguments
int32 Slot = 4; int32 Slot = 4;
FString StrateReference; FString StrateReference;
FString SettingsReference; FString SettingsReference;
FString CoreWorldAssemblyManifestPath;
bool bRender = true; bool bRender = true;
bool bWalk = true; bool bWalk = true;
bool bExport = true; bool bExport = true;
@@ -457,6 +459,7 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot); FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
FParse::Value(*Params, TEXT("strateref="), OutArguments.StrateReference); FParse::Value(*Params, TEXT("strateref="), OutArguments.StrateReference);
FParse::Value(*Params, TEXT("settingsref="), OutArguments.SettingsReference); FParse::Value(*Params, TEXT("settingsref="), OutArguments.SettingsReference);
FParse::Value(*Params, TEXT("coreworldassembly="), OutArguments.CoreWorldAssemblyManifestPath);
FParse::Value(*Params, TEXT("modes="), ModesText); FParse::Value(*Params, TEXT("modes="), ModesText);
// FParse::Value treats a comma as a command-line separator in some UE commandlet // FParse::Value treats a comma as a command-line separator in some UE commandlet
// launch paths. Recover the complete token so both -modes=walk,export and the // launch paths. Recover the complete token so both -modes=walk,export and the
@@ -987,6 +990,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
OutError = TEXT("coreworldtest requires settingsref and must omit strateref."); OutError = TEXT("coreworldtest requires settingsref and must omit strateref.");
return false; return false;
} }
if (!OutArguments.CoreWorldAssemblyManifestPath.IsEmpty() && !OutArguments.bCoreWorldTest)
{
OutError = TEXT("coreworldassembly is accepted only by coreworldtest.");
return false;
}
if (OutText.IsEmpty()) if (OutText.IsEmpty())
{ {
@@ -13197,6 +13205,30 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
return false; return false;
} }
if (!Arguments.CoreWorldAssemblyManifestPath.IsEmpty())
{
const FString ManifestPath = FPaths::ConvertRelativePathToFull(
Arguments.CoreWorldAssemblyManifestPath);
if (!IsUnderVoxelForgeSaved(ManifestPath))
{
OutError = TEXT("Core world assembly test input must be staged under the plugin Saved directory.");
return false;
}
FString ManifestJson;
if (!FFileHelper::LoadFileToString(ManifestJson, *ManifestPath))
{
OutError = FString::Printf(TEXT("Could not read staged Core world assembly '%s'."), *ManifestPath);
return false;
}
FString ImportReport;
if (!AuthoredSettings->SetWorldAssemblyManifestJson(ManifestJson, ImportReport))
{
OutError = FString::Printf(TEXT("Could not inject native SurfaceWorld plan: %s"), *ImportReport);
return false;
}
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreWorldTest] %s"), *ImportReport);
}
TStrongObjectPtr<UVoxelStrateManager> Manager(NewObject<UVoxelStrateManager>( TStrongObjectPtr<UVoxelStrateManager> Manager(NewObject<UVoxelStrateManager>(
GetTransientPackage(), NAME_None, RF_Transient)); GetTransientPackage(), NAME_None, RF_Transient));
int32 ResolvedSeed = AuthoredSettings->Seed; int32 ResolvedSeed = AuthoredSettings->Seed;
@@ -13227,6 +13259,9 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
TSet<FVoxelTileKey> ExpectedInitialDesiredSet; TSet<FVoxelTileKey> ExpectedInitialDesiredSet;
TSet<FVoxelTileKey> ExpectedTravelDesiredSet; TSet<FVoxelTileKey> ExpectedTravelDesiredSet;
TSet<FVoxelTileKey> ExpectedSurfaceTravelDesiredSet; TSet<FVoxelTileKey> ExpectedSurfaceTravelDesiredSet;
TSet<FVoxelTileKey> ExpectedSurfaceCollisionSet;
int32 ExpectedSurfaceCollisionBodyCount = 0;
int32 ExpectedSurfaceCollisionNoBodyCount = 0;
TArray<FVoxelTileKey> ExpectedAffectedTiles; TArray<FVoxelTileKey> ExpectedAffectedTiles;
FVector CarveLocalPositionCm = FVector::ZeroVector; FVector CarveLocalPositionCm = FVector::ZeroVector;
FVoxelTileKey CarveTargetTile; FVoxelTileKey CarveTargetTile;
@@ -13234,10 +13269,18 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
constexpr double CoreVoxelSizeCm = 25.0; constexpr double CoreVoxelSizeCm = 25.0;
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);
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{};
std::array<std::uint64_t, 4> SurfaceTravelAggregates{}; std::array<std::uint64_t, 4> SurfaceTravelAggregates{};
std::vector<VoxelForgeCore::TileKey> MatchedTimingInitialDesired;
std::vector<VoxelForgeCore::TileKey> MatchedTimingSurfaceDesired;
std::vector<VoxelForgeCore::TileKey> MatchedTimingSurfaceMissingFine;
VoxelForgeCore::ShapeBuildRequest MatchedTimingInitialRequest;
double NativeSurfaceRegionCompileSeconds = 0.0;
double NativeSurfaceTileGenerationSeconds = 0.0;
std::uint64_t NativeSurfaceTriangleCount = 0;
std::size_t ExpectedInitialTileCount = 0; std::size_t ExpectedInitialTileCount = 0;
std::size_t ExpectedTravelTileCount = 0; std::size_t ExpectedTravelTileCount = 0;
std::size_t TravelFineTilesCompiled = 0; std::size_t TravelFineTilesCompiled = 0;
@@ -13256,8 +13299,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
VoxelForgeCore::TileRegionCompileStats SurfaceTravelCompileStats; VoxelForgeCore::TileRegionCompileStats SurfaceTravelCompileStats;
const VoxelForgeCore::Vec3d TravelCameraVoxels{-156.75164794921875, 25.093801498413086, const VoxelForgeCore::Vec3d TravelCameraVoxels{-156.75164794921875, 25.093801498413086,
-823.67364501953125}; -823.67364501953125};
const VoxelForgeCore::Vec3d SurfaceCameraVoxels{-156.75164794921875, 25.093801498413086, const VoxelForgeCore::Vec3d SurfaceCameraVoxels{37.0, 165.0, -2011.0};
-1024.0}; uint32 NativeSurfaceBiomeId = 0;
int32 NativeBiomeIdsSampled = 0;
int32 NativeBiomeSampleCount = 0;
bool bNativeBiomeFieldValidated = false;
{ {
FTCHARToUTF8 Utf8Json(*Json); FTCHARToUTF8 Utf8Json(*Json);
std::string CoreError; std::string CoreError;
@@ -13271,6 +13317,102 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
UTF8_TO_TCHAR(CoreError.c_str())); UTF8_TO_TCHAR(CoreError.c_str()));
return false; return false;
} }
FString CheckedAssemblyJson;
FString ExpectedAssemblyHash;
FString AssemblyValidationError;
if (!AuthoredSettings->GetWorldAssemblyManifestJson(
CheckedAssemblyJson, ExpectedAssemblyHash, AssemblyValidationError))
{
OutError = FString::Printf(TEXT("Core actor test settings manifest failed validation: %s"),
*AssemblyValidationError);
return false;
}
if (!ExpectedAssemblyHash.IsEmpty())
{
const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly();
const FString SnapshotHash = Assembly == nullptr ? FString() : FString::Printf(
TEXT("%016llx"), static_cast<unsigned long long>(Assembly->contentHash));
if (Assembly == nullptr || !SnapshotHash.Equals(ExpectedAssemblyHash, ESearchCase::IgnoreCase))
{
OutError = TEXT("Core runtime adapter did not preserve the settings world assembly manifest.");
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] world_assembly_hash=%s plans=%d native=1"),
*SnapshotHash, static_cast<int32>(Assembly->plan.strates.size()));
}
else if (Snapshot->worldAssembly() != nullptr)
{
OutError = TEXT("Core runtime adapter unexpectedly attached a world assembly manifest.");
return false;
}
// Validate the native Core field's biome assignment, not the legacy decoration query.
// Valider les biomes du champ Core natif, pas la requete historique des decorations.
const VoxelForgeCore::WorldAssemblyManifest* NativeAssembly = Snapshot->worldAssembly();
const VoxelForgeCore::WorldAssemblyStratumPlanV1* NativeSurfacePlan = nullptr;
if (NativeAssembly != nullptr)
{
for (const VoxelForgeCore::WorldAssemblyStratumPlanV1& Plan : NativeAssembly->plan.strates)
{
if (Plan.strateIndex == 1)
{
NativeSurfacePlan = &Plan;
break;
}
}
}
if (NativeAssembly == nullptr || NativeSurfacePlan == nullptr)
{
OutError = TEXT("Core runtime snapshot has no native biome plan for SurfaceWorld strate 1.");
return false;
}
const VoxelForgeCore::Heightfield2D NativeSurfaceField(
NativeAssembly->recipe, *NativeSurfacePlan);
if (!NativeSurfaceField.isValid())
{
OutError = TEXT("Core runtime snapshot's native SurfaceWorld heightfield is invalid.");
return false;
}
const int64 SubvoxelsPerVoxel = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const VoxelForgeCore::HeightfieldPointSample SurfaceBiomeSample = NativeSurfaceField.point(
static_cast<int64>(std::llround(SurfaceCameraVoxels.x * SubvoxelsPerVoxel)),
static_cast<int64>(std::llround(SurfaceCameraVoxels.y * SubvoxelsPerVoxel)), 0);
if (SurfaceBiomeSample.biomeId == 0)
{
OutError = TEXT("Native SurfaceWorld heightfield returned no biome at the route camera.");
return false;
}
TSet<uint32> NativeBiomeIds;
constexpr int32 BiomeProbeSide = 64;
constexpr double BiomeProbeSpanVoxels = 3200.0;
for (int32 Y = 0; Y < BiomeProbeSide; ++Y)
for (int32 X = 0; X < BiomeProbeSide; ++X)
{
const double XVoxel = (static_cast<double>(X) / (BiomeProbeSide - 1) - 0.5)
* BiomeProbeSpanVoxels;
const double YVoxel = (static_cast<double>(Y) / (BiomeProbeSide - 1) - 0.5)
* BiomeProbeSpanVoxels;
const VoxelForgeCore::HeightfieldPointSample Sample = NativeSurfaceField.point(
static_cast<int64>(std::llround(XVoxel * SubvoxelsPerVoxel)),
static_cast<int64>(std::llround(YVoxel * SubvoxelsPerVoxel)), 0);
if (Sample.biomeId != 0) NativeBiomeIds.Add(Sample.biomeId);
if (Sample.neighborBiomeId != 0 && Sample.biomeWeightQ16 > 0)
NativeBiomeIds.Add(Sample.neighborBiomeId);
++NativeBiomeSampleCount;
}
if (NativeBiomeIds.Num() < 2)
{
OutError = TEXT("Native SurfaceWorld heightfield did not assign at least two biomes across its review domain.");
return false;
}
NativeSurfaceBiomeId = SurfaceBiomeSample.biomeId;
NativeBiomeIdsSampled = NativeBiomeIds.Num();
bNativeBiomeFieldValidated = true;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] native_biome_field=1 surface_biome_id=%u sampled_biomes=%d samples=%d span_voxels=%.0f"),
NativeSurfaceBiomeId, NativeBiomeIdsSampled, NativeBiomeSampleCount,
BiomeProbeSpanVoxels);
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot)); VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({}); const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({});
@@ -13314,6 +13456,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
Request.workerLimit = 8; Request.workerLimit = 8;
Request.detail = VoxelForgeCore::DetailVariant::coherent; Request.detail = VoxelForgeCore::DetailVariant::coherent;
Request.includeFarWorld = true; Request.includeFarWorld = true;
MatchedTimingInitialDesired = Desired;
MatchedTimingInitialRequest = Request;
FStandaloneCoreAdapterScheduler Scheduler; FStandaloneCoreAdapterScheduler Scheduler;
if (!ReferenceWorld.compile(Request, Scheduler, CoreError)) if (!ReferenceWorld.compile(Request, Scheduler, CoreError))
{ {
@@ -13651,6 +13795,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
} }
std::vector<VoxelForgeCore::TileKey> MissingSurfaceFineTiles; std::vector<VoxelForgeCore::TileKey> MissingSurfaceFineTiles;
MissingSurfaceFineTiles.reserve(SurfaceTravelDesired.size()); MissingSurfaceFineTiles.reserve(SurfaceTravelDesired.size());
MatchedTimingSurfaceDesired = SurfaceTravelDesired;
for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired) for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired)
{ {
const FVoxelTileKey RuntimeKey( const FVoxelTileKey RuntimeKey(
@@ -13659,8 +13804,10 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
if (Key.lod < ReferenceWorld.configuration().fullResolutionLodCount) if (Key.lod < ReferenceWorld.configuration().fullResolutionLodCount)
MissingSurfaceFineTiles.push_back(Key); MissingSurfaceFineTiles.push_back(Key);
} }
MatchedTimingSurfaceMissingFine = MissingSurfaceFineTiles;
// Keep the retained reference clipmap bounded while compiling the second remote region. // Keep the retained reference clipmap bounded while compiling the second remote region.
// Garder la référence bornée avant de compiler la seconde région distante. // Garder la référence bornée avant de compiler la seconde région distante.
const double NativeSurfaceRegionCompileStart = FPlatformTime::Seconds();
ReferenceWorld.retainTileRegions(SurfaceTravelDesired); ReferenceWorld.retainTileRegions(SurfaceTravelDesired);
if (!ReferenceWorld.compileTileRegion(MissingSurfaceFineTiles, 16, Scheduler, CoreError, if (!ReferenceWorld.compileTileRegion(MissingSurfaceFineTiles, 16, Scheduler, CoreError,
&SurfaceTravelFineTilesCompiled, &SurfaceTravelCompileStats)) &SurfaceTravelFineTilesCompiled, &SurfaceTravelCompileStats))
@@ -13669,11 +13816,14 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
UTF8_TO_TCHAR(CoreError.c_str())); UTF8_TO_TCHAR(CoreError.c_str()));
return false; return false;
} }
NativeSurfaceRegionCompileSeconds = FPlatformTime::Seconds()
- NativeSurfaceRegionCompileStart;
SurfaceTravelRegionsBuilt = SurfaceTravelCompileStats.regionsBuilt; SurfaceTravelRegionsBuilt = SurfaceTravelCompileStats.regionsBuilt;
std::vector<FStandaloneCoreAdapterTileHashes> SurfaceTravelRows; std::vector<FStandaloneCoreAdapterTileHashes> SurfaceTravelRows;
SurfaceTravelRows.reserve(SurfaceTravelDesired.size()); SurfaceTravelRows.reserve(SurfaceTravelDesired.size());
for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired) for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired)
{ {
const double TileGenerationStart = FPlatformTime::Seconds();
const std::optional<VoxelForgeCore::TileMesh> Mesh = const std::optional<VoxelForgeCore::TileMesh> Mesh =
ReferenceWorld.generateTile(Key, TravelOptions, CoreError); ReferenceWorld.generateTile(Key, TravelOptions, CoreError);
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
@@ -13683,6 +13833,9 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
UTF8_TO_TCHAR(CoreError.c_str())); UTF8_TO_TCHAR(CoreError.c_str()));
return false; return false;
} }
NativeSurfaceTileGenerationSeconds += FPlatformTime::Seconds()
- TileGenerationStart;
NativeSurfaceTriangleCount += Mesh->indices.size() / 3;
const FVoxelTileKey RuntimeKey( const FVoxelTileKey RuntimeKey(
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod); FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
FVoxelForgeCoreTileHashRecord Record; FVoxelForgeCoreTileHashRecord Record;
@@ -13698,6 +13851,27 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
{Record.Density, Record.Mesh, Record.Payload, Record.Topology}}); {Record.Density, Record.Mesh, Record.Payload, Record.Topology}});
if (Mesh->groups.skirtTriangleCount > 0) ++SurfaceTravelTilesWithSkirts; if (Mesh->groups.skirtTriangleCount > 0) ++SurfaceTravelTilesWithSkirts;
} }
const FIntVector SurfaceCollisionCenter(
FMath::FloorToInt(SurfaceCameraVoxels.x / CHUNK_SIZE),
FMath::FloorToInt(SurfaceCameraVoxels.y / CHUNK_SIZE),
FMath::FloorToInt(SurfaceCameraVoxels.z / CHUNK_SIZE));
for (int32 Z = -CollisionRadius; Z <= CollisionRadius; ++Z)
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
{
const FVoxelTileKey Key(SurfaceCollisionCenter + FIntVector(X, Y, Z), 0);
const FVoxelForgeCoreTileHashRecord* Record = ExpectedSurfaceTravelHashes.Find(Key);
if (Record == nullptr)
{
OutError = FString::Printf(
TEXT("Standalone native-surface reference omitted expected collision tile (%d,%d,%d)."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z);
return false;
}
ExpectedSurfaceCollisionSet.Add(Key);
if (Record->bEmpty) ++ExpectedSurfaceCollisionNoBodyCount;
else ++ExpectedSurfaceCollisionBodyCount;
}
SurfaceTravelAggregates = StandaloneCoreAggregateHashes(SurfaceTravelRows); SurfaceTravelAggregates = StandaloneCoreAggregateHashes(SurfaceTravelRows);
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] standalone_surface_reference=1 location=(%.3f,%.3f,%.3f) tiles=%d fine_compiled=%llu regions=%llu local_tiles=%d checked_faces=%d lod_transition_faces=%d skirt_tiles=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"), TEXT("[VoxelForgeCoreWorldTest] standalone_surface_reference=1 location=(%.3f,%.3f,%.3f) tiles=%d fine_compiled=%llu regions=%llu local_tiles=%d checked_faces=%d lod_transition_faces=%d skirt_tiles=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"),
@@ -13719,6 +13893,107 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
CarveTargetTile.Coord.Z, CarveTargetTile.Level); CarveTargetTile.Coord.Z, CarveTargetTile.Level);
} }
// Compare native and compatibility bridge on the same remote surface keys in this process.
// Comparer le champ natif et le pont de compatibilite sur les memes cles de surface.
if (MatchedTimingInitialDesired.empty() || MatchedTimingSurfaceDesired.empty()
|| MatchedTimingSurfaceMissingFine.empty())
{
OutError = TEXT("Matched native/bridge timing did not retain its reference tile keys.");
return false;
}
FString BridgeJson = Json;
TSharedPtr<FJsonObject> BridgeRoot;
const TSharedRef<TJsonReader<>> BridgeReader = TJsonReaderFactory<>::Create(BridgeJson);
if (!FJsonSerializer::Deserialize(BridgeReader, BridgeRoot) || !BridgeRoot.IsValid())
{
OutError = TEXT("Matched legacy-bridge timing could not parse the Core snapshot JSON.");
return false;
}
BridgeRoot->RemoveField(TEXT("world_assembly"));
BridgeRoot->RemoveField(TEXT("world_assembly_content_hash"));
BridgeJson.Reset();
const TSharedRef<FExploreJsonWriter> BridgeWriter = TJsonWriterFactory<TCHAR,
TCondensedJsonPrintPolicy<TCHAR>>::Create(&BridgeJson);
if (!FJsonSerializer::Serialize(BridgeRoot.ToSharedRef(), BridgeWriter))
{
OutError = TEXT("Matched legacy-bridge timing could not serialize the bridge snapshot.");
return false;
}
BridgeWriter->Close();
FTCHARToUTF8 BridgeUtf8(*BridgeJson);
std::string BridgeError;
std::optional<VoxelForgeCore::ResolvedWorldSnapshot> BridgeSnapshot =
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
std::string_view(BridgeUtf8.Get(), static_cast<std::size_t>(BridgeUtf8.Length())),
BridgeError);
if (!BridgeSnapshot || BridgeSnapshot->worldAssembly() != nullptr)
{
OutError = FString::Printf(
TEXT("Matched legacy-bridge snapshot failed validation or retained a native plan: %s"),
UTF8_TO_TCHAR(BridgeError.c_str()));
return false;
}
VoxelForgeCore::World BridgeWorld(std::move(*BridgeSnapshot));
FStandaloneCoreAdapterScheduler BridgeScheduler;
if (!BridgeWorld.compile(MatchedTimingInitialRequest, BridgeScheduler, BridgeError))
{
OutError = FString::Printf(TEXT("Matched legacy-bridge initial compile failed: %s"),
UTF8_TO_TCHAR(BridgeError.c_str()));
return false;
}
BridgeWorld.retainTileRegions(MatchedTimingInitialDesired);
const double BridgeRegionCompileStart = FPlatformTime::Seconds();
BridgeWorld.retainTileRegions(MatchedTimingSurfaceDesired);
VoxelForgeCore::TileRegionCompileStats BridgeRegionStats;
if (!BridgeWorld.compileTileRegion(MatchedTimingSurfaceMissingFine, 16, BridgeScheduler,
BridgeError, nullptr, &BridgeRegionStats))
{
OutError = FString::Printf(TEXT("Matched legacy-bridge surface compile failed: %s"),
UTF8_TO_TCHAR(BridgeError.c_str()));
return false;
}
const double BridgeSurfaceRegionCompileSeconds = FPlatformTime::Seconds()
- BridgeRegionCompileStart;
VoxelForgeCore::TileGenerationOptions BridgeTileOptions;
BridgeTileOptions.hashDensity = true;
double BridgeSurfaceTileGenerationSeconds = 0.0;
std::uint64_t BridgeSurfaceTriangleCount = 0;
std::size_t BridgeSurfaceTilesGenerated = 0;
for (const VoxelForgeCore::TileKey& Key : MatchedTimingSurfaceDesired)
{
const double TileGenerationStart = FPlatformTime::Seconds();
const std::optional<VoxelForgeCore::TileMesh> Mesh =
BridgeWorld.generateTile(Key, BridgeTileOptions, BridgeError);
BridgeSurfaceTileGenerationSeconds += FPlatformTime::Seconds() - TileGenerationStart;
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
|| !Mesh->hasPayloadHash || !Mesh->hasTopologyHash)
{
OutError = FString::Printf(TEXT("Matched legacy-bridge tile generation failed: %s"),
UTF8_TO_TCHAR(BridgeError.c_str()));
return false;
}
BridgeSurfaceTriangleCount += Mesh->indices.size() / 3;
++BridgeSurfaceTilesGenerated;
}
if (BridgeSurfaceTilesGenerated != MatchedTimingSurfaceDesired.size())
{
OutError = TEXT("Matched legacy-bridge timing did not generate the complete surface key set.");
return false;
}
const double NativeTotal = NativeSurfaceRegionCompileSeconds
+ NativeSurfaceTileGenerationSeconds;
const double BridgeTotal = BridgeSurfaceRegionCompileSeconds
+ BridgeSurfaceTileGenerationSeconds;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] matched_surface_tile_timing=1 native_backend=heightfield2d bridge_backend=legacy_surface_bridge same_keys=1 tiles=%llu hashes=on native_region_compile_s=%.6f bridge_region_compile_s=%.6f native_tile_generation_s=%.6f bridge_tile_generation_s=%.6f native_compile_plus_tiles_s=%.6f bridge_compile_plus_tiles_s=%.6f speedup=%.3fx native_triangles=%llu bridge_triangles=%llu"),
static_cast<unsigned long long>(MatchedTimingSurfaceDesired.size()),
NativeSurfaceRegionCompileSeconds, BridgeSurfaceRegionCompileSeconds,
NativeSurfaceTileGenerationSeconds, BridgeSurfaceTileGenerationSeconds,
NativeTotal, BridgeTotal, BridgeTotal / FMath::Max(NativeTotal, 1.0e-9),
static_cast<unsigned long long>(NativeSurfaceTriangleCount),
static_cast<unsigned long long>(BridgeSurfaceTriangleCount));
const UWorld::InitializationValues WorldInitialization = UWorld::InitializationValues() const UWorld::InitializationValues WorldInitialization = UWorld::InitializationValues()
.AllowAudioPlayback(false) .AllowAudioPlayback(false)
.RequiresHitProxies(false) .RequiresHitProxies(false)
@@ -13901,7 +14176,6 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
} }
} }
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
TSet<FVoxelTileKey> ExpectedCollisionSet; TSet<FVoxelTileKey> ExpectedCollisionSet;
int32 ExpectedBodyCount = 0; int32 ExpectedBodyCount = 0;
int32 ExpectedNoBodyCount = 0; int32 ExpectedNoBodyCount = 0;
@@ -14178,6 +14452,21 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
break; break;
} }
} }
if (bVisualTilesReady)
{
for (const FVoxelTileKey& Key : ExpectedSurfaceCollisionSet)
{
const FVoxelForgeCoreTileHashRecord* Expected = ExpectedSurfaceTravelHashes.Find(Key);
const bool bExpectedBody = Expected != nullptr && !Expected->bEmpty;
if (!Actor->CollisionDesiredTiles.Contains(Key)
|| (bExpectedBody && !Actor->CollisionReadyTiles.Contains(Key))
|| (!bExpectedBody && !Actor->CollisionNotRequiredTiles.Contains(Key)))
{
bVisualTilesReady = false;
break;
}
}
}
if (bVisualTilesReady) return true; if (bVisualTilesReady) return true;
} }
return false; return false;
@@ -14201,6 +14490,39 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
return false; return false;
} }
int32 ActualSurfaceCollisionReady = 0;
int32 ActualSurfaceCollisionNoBody = 0;
for (const FVoxelTileKey& Key : ExpectedSurfaceCollisionSet)
{
const FVoxelForgeCoreTileHashRecord& Expected = ExpectedSurfaceTravelHashes.FindChecked(Key);
if (!Actor->CollisionDesiredTiles.Contains(Key))
{
OutError = FString::Printf(TEXT("Native-surface collision tile (%d,%d,%d) was not requested."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z);
return false;
}
if (Expected.bEmpty)
{
if (!Actor->CollisionNotRequiredTiles.Contains(Key))
{
OutError = FString::Printf(TEXT("Native-surface empty tile (%d,%d,%d) has unexpected collision state."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z);
return false;
}
++ActualSurfaceCollisionNoBody;
}
else
{
if (!Actor->CollisionReadyTiles.Contains(Key))
{
OutError = FString::Printf(TEXT("Native-surface solid tile (%d,%d,%d) has no cooked collision body."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z);
return false;
}
++ActualSurfaceCollisionReady;
}
}
TSet<FVoxelTileKey> ActualSurfaceVisualDesiredSet; TSet<FVoxelTileKey> ActualSurfaceVisualDesiredSet;
std::vector<VoxelForgeCore::TileKey> ActorSurfaceTravelDesired; std::vector<VoxelForgeCore::TileKey> ActorSurfaceTravelDesired;
ActorSurfaceTravelDesired.reserve(static_cast<std::size_t>(Actor->DesiredSorted.Num())); ActorSurfaceTravelDesired.reserve(static_cast<std::size_t>(Actor->DesiredSorted.Num()));
@@ -14231,14 +14553,31 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key); const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key);
const FVoxelForgeCoreTileHashRecord* Expected = ExpectedSurfaceTravelHashes.Find(Key); const FVoxelForgeCoreTileHashRecord* Expected = ExpectedSurfaceTravelHashes.Find(Key);
if (Actual == nullptr || Expected == nullptr || !Actual->bHasHashes if (Actual == nullptr || Expected == nullptr || !Actual->bHasHashes
|| (!Expected->bEmpty && !Actor->LoadedTiles.Contains(Key)) || (!Expected->bEmpty && !Actor->LoadedTiles.Contains(Key)))
|| Actual->Density != Expected->Density || Actual->Mesh != Expected->Mesh {
OutError = FString::Printf(
TEXT("Core actor surface-strate tile state is incomplete at (%d,%d,%d) lod=%d (actual=%d expected=%d hashed=%d loaded=%d)."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level,
Actual ? 1 : 0, Expected ? 1 : 0, Actual && Actual->bHasHashes ? 1 : 0,
Actor->LoadedTiles.Contains(Key) ? 1 : 0);
return false;
}
if (Actual->Density != Expected->Density || Actual->Mesh != Expected->Mesh
|| Actual->Payload != Expected->Payload || Actual->Topology != Expected->Topology || Actual->Payload != Expected->Payload || Actual->Topology != Expected->Topology
|| Actual->Triangles != Expected->Triangles || Actual->bEmpty != Expected->bEmpty) || Actual->Triangles != Expected->Triangles || Actual->bEmpty != Expected->bEmpty)
{ {
OutError = FString::Printf( OutError = FString::Printf(
TEXT("Core actor surface-strate tile differs from standalone Core at (%d,%d,%d) lod=%d."), TEXT("Core actor surface-strate hash mismatch at (%d,%d,%d) lod=%d; density=%016llx/%016llx mesh=%016llx/%016llx payload=%016llx/%016llx topology=%016llx/%016llx triangles=%d/%d empty=%d/%d."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level); Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level,
static_cast<unsigned long long>(Actual->Density),
static_cast<unsigned long long>(Expected->Density),
static_cast<unsigned long long>(Actual->Mesh),
static_cast<unsigned long long>(Expected->Mesh),
static_cast<unsigned long long>(Actual->Payload),
static_cast<unsigned long long>(Expected->Payload),
static_cast<unsigned long long>(Actual->Topology),
static_cast<unsigned long long>(Expected->Topology),
Actual->Triangles, Expected->Triangles, Actual->bEmpty ? 1 : 0, Expected->bEmpty ? 1 : 0);
return false; return false;
} }
} }
@@ -14255,20 +14594,20 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
*SurfaceCoverageError); *SurfaceCoverageError);
return false; return false;
} }
const FVoxelBiomeQuery SurfaceBiome = Actor->GetBiomeAtWorldLocation(SurfaceWorldPosition); if (!bNativeBiomeFieldValidated || NativeBiomeIdsSampled < 2)
if (!SurfaceBiome.bHasBiomes || SurfaceBiome.DominantContextIndex < 0)
{ {
OutError = TEXT("Core-mode biome query returned no dominant biome at the authored surface-strate fixture."); OutError = TEXT("Core native SurfaceWorld biome validation did not complete.");
return false; return false;
} }
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] pass phase=surface_strate_travel center=(%.3f,%.3f,%.3f) desired=%d loaded=%d fine_compiled=%llu regions=%llu local_tiles=%d checked_faces=%d lod_transition_faces=%d biome_index=%d biome=%s density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"), TEXT("[VoxelForgeCoreWorldTest] pass phase=surface_strate_travel center=(%.3f,%.3f,%.3f) desired=%d loaded=%d fine_compiled=%llu regions=%llu local_tiles=%d checked_faces=%d lod_transition_faces=%d collision_desired=%d collision_ready=%d collision_no_body=%d native_surface_biome_id=%u native_biomes_sampled=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"),
SurfaceCameraVoxels.x, SurfaceCameraVoxels.y, SurfaceCameraVoxels.z, SurfaceCameraVoxels.x, SurfaceCameraVoxels.y, SurfaceCameraVoxels.z,
ActualSurfaceVisualDesiredSet.Num(), Actor->LoadedTiles.Num(), ActualSurfaceVisualDesiredSet.Num(), Actor->LoadedTiles.Num(),
static_cast<unsigned long long>(SurfaceTravelFineTilesCompiled), static_cast<unsigned long long>(SurfaceTravelFineTilesCompiled),
static_cast<unsigned long long>(SurfaceTravelRegionsBuilt), ActorSurfaceLocalTiles, static_cast<unsigned long long>(SurfaceTravelRegionsBuilt), ActorSurfaceLocalTiles,
ActorSurfaceCheckedFaces, ActorSurfaceTransitionFaces, ActorSurfaceCheckedFaces, ActorSurfaceTransitionFaces, ExpectedSurfaceCollisionSet.Num(),
SurfaceBiome.DominantContextIndex, *SurfaceBiome.DominantName.ToString(), ActualSurfaceCollisionReady, ActualSurfaceCollisionNoBody,
NativeSurfaceBiomeId, NativeBiomeIdsSampled,
static_cast<unsigned long long>(SurfaceTravelAggregates[0]), static_cast<unsigned long long>(SurfaceTravelAggregates[0]),
static_cast<unsigned long long>(SurfaceTravelAggregates[1]), static_cast<unsigned long long>(SurfaceTravelAggregates[1]),
static_cast<unsigned long long>(SurfaceTravelAggregates[2]), static_cast<unsigned long long>(SurfaceTravelAggregates[2]),
+119 -7
View File
@@ -508,12 +508,10 @@ profiles are in `round4/final/logs/mesh-hash-final-w{1,8,16}.stdout.txt` and
- **Round 4:** Complete. The route and cap are in the research manifest; field sweep, negative - **Round 4:** Complete. The route and cap are in the research manifest; field sweep, negative
controls, bounds, determinism, headless query and matched captures are recorded above. Physics controls, bounds, determinism, headless query and matched captures are recorded above. Physics
collision cooking and Unreal light-leak behavior remain owner-build checks. collision cooking and Unreal light-leak behavior remain owner-build checks.
- **Round 5 — before Unreal delivery:** Integrate the reviewed manifest into the season asset/content - **Round 5:** Complete for plugin-side delivery. The manifest/content-hash path, native Core
hash path and connect Core behind `bUseVoxelForgeCore`; prove that flag-off behavior remains surface, BuildHost tile/collision coverage, source-level switch-off check, matched timing and
unchanged. The owner builds and reviews the in-game two-biome surface, enclosed cap lighting and handoff are recorded below. The owner's project build, in-game walk, and cap-lighting review are
capsule-walkable route. Run the matched 23.085 s workload, report full-view and local query/ the remaining owner checks.
collision readiness separately, and assess the approximately 1.57 s aim. No BuildHost or owner
project build was done here.
### Run log ### Run log
@@ -538,4 +536,118 @@ profiles are in `round4/final/logs/mesh-hash-final-w{1,8,16}.stdout.txt` and
`Saved/BuildHost/HostProject/Saved/Crashes/` folder contains one older `UECC-Windows-*` directory `Saved/BuildHost/HostProject/Saved/Crashes/` folder contains one older `UECC-Windows-*` directory
from 2026-09-17; there was no new crash folder from this round. from 2026-09-17; there was no new crash folder from this round.
No Unreal route or asset was changed in this round, so there is no owner rebuild handoff yet. No Unreal route or asset was changed in round 4, so there was no owner rebuild handoff then. Round 5
supersedes that status.
## Round 5 — native SurfaceWorld in Unreal
The reviewed round-4 manifest now enters the opt-in Unreal Core route through `UVoxelSettings`.
The editor-only `SourceWorldAssemblyManifestJson` file picker plus `ImportWorldAssemblyManifestJson`
button reads the reviewed JSON into the settings asset as canonical serialized JSON and stores its
Core content hash. Import validates the recipe seed against the effective settings seed. The Core
adapter revalidates the manifest and requires a unique exact match for every planned stratum index,
top chunk and bottom chunk against resolved `SurfaceWorld` slots; invalid or incomplete plans fail
Core world initialization rather than silently falling back. With no manifest, existing Core cave
layouts retain their prior schema and route. With `bUseVoxelForgeCore` off, the legacy route and its
season identity hash are unchanged; the manifest only contributes to that hash while Core is active.
### Headless BuildHost evidence
`Tools/TestVoxelForgeCoreWorldHost.ps1` staged the reviewed JSON only under BuildHost and passed it
transiently to `-run=VoxelForgeExplore -coreworldtest=1 -coreworldassembly=...`; no data asset was
edited by the test. Manifest hash `8933693d9560ba9f`, recipe hash `c37de78b759c0a4c`, and plan hash
`1ca84619c8600ee5` were read back in Core. The field probe sampled 4,096 points over 3,200 voxels,
found three native biome IDs (including surface biome ID 4099), and generated native `Heightfield2D`
surface tiles. The actor's W1/W8/W16 initial hashes remained identical.
The final surface-strate travel phase passed at Core-local `(37,165,-2011)` voxels. All 2,553
expected visual keys matched independently generated standalone Core tile hashes; 573 local tiles,
3,438 neighbour faces and 432 LOD transition faces were checked. Of 125 collision-desired tiles,
36 had cooked bodies and 89 correctly had no body because their expected geometry was empty. The
gate checked each non-empty collision expectation individually. The log records the native surface
aggregate hashes as density `56ccc76575aa5f5f`, mesh `f3aeb31de1f967c1`, payload `95cb4781a1be4b73`,
topology `26ac366d430599bd`. Initial cave streaming, anchor release, and sequenced carve also passed.
Unreal's BuildHost exercised the RealtimeMesh/Chaos collision-body path; standalone does not cook
physics bodies. Full final output is in [CoreWorldValidation.stdout.log](../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation.stdout.log)
and [CoreWorldValidation.log](../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation.log).
Failed-attempt archives use the `CoreWorldValidation-backup-2026.10.09-*` names in the same log
directory.
### Switch-off check
The chosen narrow check is a source-level route and identity audit, not a runtime flag-off actor
comparison. `bUseVoxelForgeCore` remains default-false; `UpdateChunksAroundPosition` branches to the
existing legacy path when false; manifest import/adapter initialization and the new collision-support
deferral are Core-only; and `GetCurrentSeasonContentHash()` returns the pre-existing season hash
before inspecting the manifest when Core is off. This verifies the affected route boundaries and
content identity, but does not claim bit-for-bit runtime output from a second legacy actor session.
### Matched timing and the 1.57 s aim
In one BuildHost session, native `Heightfield2D` and the legacy-surface bridge generated the same
2,553 surface tile keys with hashes enabled. Region compilation took 0.015903 s native and 0.012752 s
bridge. Summed serial tile generation took 96.800984 s native versus 197.333401 s bridge; including
region compilation, 96.816887 s versus 197.346153 s (2.038× native speedup). This is a headless
same-key tile-generation comparison, not game-thread apply time or a full rendered-view timing.
The approximately 1.57 s aim is still unmet. The last matched standalone full 529-tile LOD8 stream
took 4.446 s wall, about 2.83× the aim. This round did not run a viewer/full-view benchmark; the
2,553-key BuildHost timing has a different workload and should not be compared directly to the aim.
### Ridge review and owner handoff
I inspected only the existing 17.6 KB downscaled `cliff-final4-overlook-native.jpg` and its 20.5 KB
owner comparison. The pointed ridge peaks are attached to continuous highland terrain and have no
detached shard or broad fanned-plate trail. They read as sharp native mountain peaks, not a mesher
artifact, so no terrain change was made. The frame remains an owner-review item for taste.
For the owner's project build and walk:
1. In the Content Browser, open the existing `DA_Settings` (`UVoxelSettings`) and confirm its
effective seed is `0` (the manifest seed). Set `SourceWorldAssemblyManifestJson` to
`Standalone/Research/WorldAssemblerM1/round4/world_assembly.json` in this plugin checkout, click
**Import World Assembly Manifest JSON**, confirm hash `8933693d9560ba9f`, then save the settings
asset. Assign that settings asset to the project's `AVoxelWorld` if it is not already assigned.
2. Select the `AVoxelWorld`, enable **Voxel World → Core → Use VoxelForge Core**. Keep the shipped
Core clip settings `CoreClipRadius=3` and `CoreMaxClipLevel=8` for the first review. Do not change
the legacy clip settings for this check. Coordinates below are actor-local Unreal centimetres;
Core scale is 25 cm per voxel.
3. To inspect the surface, place the gameplay/free camera near `(0,-3500,-48794)` cm and look toward
ground target `(0,0,-50394)` cm. The coast view is near camera `(33600,-6700,-47776)` cm,
targeting `(33600,-3200,-49376)` cm. The biome-border view is near camera
`(12800,-24300,-49291)` cm, targeting `(12800,-20800,-50891)` cm. The attached cliff/overhang is
near camera `(-16800,7000,-44571)` cm, targeting `(-16800,10500,-46171)` cm. To inspect the cap,
use the surface view and pitch upward/outward about 25°; the cap is hundreds of metres above the
surface floor, so look for the enclosing roof and distant horizon, not a nearby ceiling.
4. Walk the native ground route beginning near `(925,4125,-50275)` cm. The route is terrain support
geometry, not a painted path. Expect two inland terrain profiles, a coastal transition, one
attached cliff shelf, a closed cap and a collision-supported path. Check lighting inside the cap
and the small ridge peaks in the game.
Likely compile touchpoints if the project reports an error are `VoxelSettings.h/.cpp` (UHT/editor
import property and exported methods), `VoxelForgeCoreAdapter.cpp` (JSON parser and exact strate
match), `VoxelWorld.cpp` (manifest content hash and collision admission order), and
`VoxelForgeExploreCommandlet.cpp` (editor-only commandlet path). The plugin BuildHost compile passed;
the owner's project has not been built or opened here. No `.uasset` was modified.
### Round-5 run ledger
| Run | Result |
|---|---|
| First BuildHost UBT compile | Failed at Editor link because the new `UVoxelSettings` getter/setter definitions were not exported. Added `VOXELFORGE_API`; retry passed UBT and the Core link-safety audit. |
| Surface actor test attempt 1 | Failed tile-hash parity while moving to the surface, at support tile `(1,5,-63)`. |
| Surface actor test attempt 2 | Retried after route-fixture correction; the same support tile still differed. Root cause was a non-desired collision support request generating before Core region admission. |
| Surface actor test attempt 3 | Failed the old `GetBiomeAtWorldLocation` assertion: that API is the legacy biome/decorator query and is empty for this native Core surface. Replaced it with direct native `Heightfield2D` sampling from the resolved snapshot and added a native biome-field assertion. |
| BuildHost compile and actor test after collision fix | Passed after collision support was deferred until the tile enters desired-set region compilation. UBT and Core symbol audit passed; the complete actor test passed the gates above. |
| Final BuildHost rerun after switch-off hash gate | UBT/link-safety passed. Actor test passed in 9m20s with peak sampled private memory 3,880,357,888 bytes (3.61 GiB), including matched native/bridge timing and every surface coverage/collision gate. |
All actor attempts stayed below 15 minutes and below 8 GiB per process. No `0xC0000142` retry
occurred, and there was no new UECC crash folder; the only BuildHost crash directory is the older
2026-09-17 `UECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000`. No owner project/editor was opened,
no owner build or `.uasset` edit occurred, and no commit, push or stash was made.
New source files: none. Modified source files:
`Source/VoxelForge/Public/VoxelSettings.h`, `Source/VoxelForge/Private/VoxelSettings.cpp`,
`Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`, `Source/VoxelForge/Private/VoxelWorld.cpp`, and
`Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp`. Modified BuildHost test tool:
`Tools/TestVoxelForgeCoreWorldHost.ps1`.
@@ -778,3 +778,56 @@ reported separately. See `Standalone/ASSEMBLER_M1_REPORT.md` for the full measur
run ledger. run ledger.
— Luna — Luna
## Luna, M1 round 5 results
Hi Claude,
The reviewed `WorldAssemblyManifest` now reaches Unreal's opt-in Core route. `UVoxelSettings` imports
the canonical JSON through an editor picker/button and stores its Core content hash. The adapter
checks the recipe seed and requires each plan to match a unique resolved `SurfaceWorld` stratum by
index and top/bottom chunk bounds. With no manifest, the existing Core cave route remains unchanged.
With `bUseVoxelForgeCore` off, the legacy generator route and season identity hash remain unchanged;
the hash review is source-level, not a second flag-off actor replay.
The final BuildHost compile/link audit passed, and the final actor commandlet passed in 9m20s with
3,880,357,888 bytes peak sampled private memory (3.61 GiB). The reviewed hashes were manifest
`8933693d9560ba9f`, recipe `c37de78b759c0a4c`, plan `1ca84619c8600ee5`. The surface field sampled
4,096 points across 3,200 voxels and found three biome IDs. At `(37,165,-2011)` Core voxels, all
2,553 expected visual tile hashes matched standalone Core; 573 local tiles, 3,438 neighbour faces
and 432 LOD transitions passed. The collision gate checked 125 tiles: all 36 expected non-empty
cases had cooked bodies; 89 expected-empty cases correctly had none. Cave initial streaming, anchor
release and sequenced carving also passed. The run used BuildHost/null RHI; no viewer was launched.
In the same session, native `Heightfield2D` and the legacy bridge generated the same 2,553 keys with
hashes on. Compile plus serial tile generation was 96.816887 s native versus 197.346153 s bridge
(2.038× speedup). The approximately 1.57 s whole-view aim remains open: the last 529-tile LOD8
stream measured 4.446 s. I inspected the 17.6 KB downscaled native cliff overlook and owner pair.
The pointed peaks are attached highland terrain without a detached shard trail, so I did not smooth
the field; the owner can judge the sharp look in-game.
The failure ledger is in `Standalone/ASSEMBLER_M1_REPORT.md`: initial UBT link failed because the
new settings methods were not exported, then passed after the export fix. The first two actor attempts
found the same surface tile hash mismatch because an out-of-desired-set collision support job could
start before Core region admission. A third attempt failed the legacy biome/decorator query assertion;
it now samples the native `Heightfield2D` field directly. After deferring that support request until
desired-set admission, the actor test passed. I then gated the assembly hash on Core being active to
preserve legacy identity when the switch is off, rebuilt, and reran the complete actor test; that final
run passed too. No `0xC0000142` retry, new UECC folder, owner build/editor, `.uasset` edit, commit,
push or stash.
Owner handoff: in the existing `DA_Settings`, set its source picker to
`Standalone/Research/WorldAssemblerM1/round4/world_assembly.json`, click **Import World Assembly
Manifest JSON**, confirm the hash and save. Verify effective seed `0`, assign the settings asset to
the world, then enable `AVoxelWorld → Voxel World → Core → Use VoxelForge Core` with Core radius 3
and max clip level 8. The M1 report has actor-local centimeter camera/target coordinates for the
surface, biome border, coast and attached cliff, plus the route start and cap viewing direction. Walk
the route near `(925,4125,-50275)` cm; look upward/outward about 25° for the high enclosing cap.
Review the cap lighting and ridge peaks after the project build. Likely compile touchpoints are
`VoxelSettings.h/.cpp`, `VoxelForgeCoreAdapter.cpp`, `VoxelWorld.cpp`, and
`VoxelForgeExploreCommandlet.cpp`.
New source files: none. The six modified source/tool files and detailed BuildHost evidence are listed
in `Standalone/ASSEMBLER_M1_REPORT.md`. The owner's project has not been built or walked here.
— Luna
+54
View File
@@ -428,3 +428,57 @@ legacy route. After the owner rebuild, recheck the SurfaceWorld shoreline, hills
at the matched camera after fine tiles settle; also recheck the opening-room floor. The captures show at the matched camera after fine tiles settle; also recheck the opening-room floor. The captures show
the exact-density bridge improves the surface field, but water/presentation and the other missing the exact-density bridge improves the surface field, but water/presentation and the other missing
archetypes are still open work. archetypes are still open work.
### Round-5 native SurfaceWorld delivery
Round 4 above is historical. The native manifest route now supersedes the temporary legacy density
bridge for a reviewed `SurfaceWorld` when `AVoxelWorld::bUseVoxelForgeCore` is enabled. Existing Core
cave generation remains supported when there is no world-assembly manifest. With the switch off,
legacy generation and the existing season identity hash remain on the old path.
The editor `UVoxelSettings` asset now has a `SourceWorldAssemblyManifestJson` picker and
`ImportWorldAssemblyManifestJson` button. Import stores canonical JSON plus the verified Core content
hash in the settings asset. `GetCurrentSeasonContentHash()` combines that hash only while Core is
active. Core adapter initialization checks the manifest seed against the resolved layout and
requires exact, unique `SurfaceWorld` slot index/top/bottom matches. The test passes the reviewed
JSON to BuildHost through `-coreworldassembly`; it does not edit an asset. Manifest, recipe and plan
hashes were `8933693d9560ba9f`, `c37de78b759c0a4c` and `1ca84619c8600ee5`.
The final BuildHost commandlet passed the W1/W8/W16 matrix, cave initial streaming, anchor movement,
carving, lower-surface travel and native SurfaceWorld travel. At `(37,165,-2011)` Core voxels, all
2,553 expected visual tile hashes matched standalone Core. Coverage checked 573 local tiles, 3,438
neighbour faces and 432 LOD transitions across two regions. The collision gate checked 125 expected
near tiles: 36 non-empty tiles had cooked bodies; 89 expected-empty tiles correctly had none. Native
SurfaceWorld hashes were density `56ccc76575aa5f5f`, mesh `f3aeb31de1f967c1`, payload
`95cb4781a1be4b73`, topology `26ac366d430599bd`. The BuildHost path uses Unreal mesh upload and
Chaos collision cooking; standalone checks field and mesh output only.
The narrow switch-off check is source-level, not a second runtime actor session: Core remains
default-off; the legacy `UpdateChunksAroundPosition` branch is retained; Core-only manifest
initialization and collision admission are guarded by the switch; and the legacy hash returns before
reading optional manifest data when disabled. This does not claim a full flag-off runtime replay.
In the same BuildHost session, 2,553 identical keys were generated with hashes on. Native
`Heightfield2D` vs the legacy bridge took 0.015903 vs 0.012752 s for region compilation and
96.800984 vs 197.333401 s for summed tile generation; compile plus tiles was 96.816887 vs 197.346153
s (2.038× speedup). This is serial headless tile generation, not rendered frame time. The ~1.57 s
full-view aim remains unmet: the last full 529-tile LOD8 stream measured 4.446 s, 2.83× the aim.
No full-view viewer benchmark ran in round 5.
The existing downscaled native/owner cliff overlook pair was inspected. The sharp ridge peaks remain
attached to the highland surface with no detached shard trail, so the field/mesh was not altered;
their appearance is left for the owner to judge in-game. Handoff instructions and actor-local camera
targets for the surface, coast, cliff, route and cap are in
[`ASSEMBLER_M1_REPORT.md`](ASSEMBLER_M1_REPORT.md). Likely project compile touchpoints are
`VoxelSettings.h/.cpp`, `VoxelForgeCoreAdapter.cpp`, `VoxelWorld.cpp` and
`VoxelForgeExploreCommandlet.cpp`.
Round-5 BuildHost run ledger: the first UBT compile failed to link non-exported settings methods;
exporting them made the retry pass. Actor attempt 1 found a surface support-tile hash mismatch;
attempt 2 reproduced it and exposed collision work starting before region admission. Actor attempt 3
failed the legacy biome-query assertion because the native surface does not provide that legacy
decorator query; it was replaced with direct native heightfield sampling. After the collision-admission
fix, the actor test passed. The final rerun after gating the manifest hash on the Core switch also
passed in 9m20s with 3,880,357,888 bytes peak sampled private memory (3.61 GiB). No `0xC0000142`
retry or new UECC folder occurred. The owner's project was not opened or built and no `.uasset` was
edited.
+30 -2
View File
@@ -1,6 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900 [ValidateRange(60, 900)][int]$TimeoutSeconds = 900,
[string]$WorldAssemblyManifestPath = ''
) )
# Exercise AVoxelWorld Core streaming, collision, anchor movement, and carving in BuildHost only. # Exercise AVoxelWorld Core streaming, collision, anchor movement, and carving in BuildHost only.
@@ -13,6 +14,8 @@ $StagePlugin = Join-Path $HostRoot 'Plugins\VoxelForge'
$HostProject = Join-Path $HostRoot 'HostProject.uproject' $HostProject = Join-Path $HostRoot 'HostProject.uproject'
$OutputRoot = Join-Path $StagePlugin 'Saved\CoreWorldValidation' $OutputRoot = Join-Path $StagePlugin 'Saved\CoreWorldValidation'
$OutputDirectory = Join-Path $OutputRoot 'Output' $OutputDirectory = Join-Path $OutputRoot 'Output'
$InputDirectory = Join-Path $OutputRoot 'Input'
$StagedWorldAssembly = Join-Path $InputDirectory 'world_assembly.json'
$LogDirectory = Join-Path $OutputRoot 'Logs' $LogDirectory = Join-Path $OutputRoot 'Logs'
$LocalAppData = Join-Path $OutputRoot 'LocalAppData' $LocalAppData = Join-Path $OutputRoot 'LocalAppData'
$UserDirectory = Join-Path $OutputRoot 'User' $UserDirectory = Join-Path $OutputRoot 'User'
@@ -33,11 +36,19 @@ function Assert-UnderPath([string]$Candidate, [string]$Root) {
Assert-UnderPath $HostRoot (Join-Path $PluginRoot 'Saved\BuildHost') Assert-UnderPath $HostRoot (Join-Path $PluginRoot 'Saved\BuildHost')
Assert-UnderPath $OutputRoot (Join-Path $StagePlugin 'Saved') Assert-UnderPath $OutputRoot (Join-Path $StagePlugin 'Saved')
if ([string]::IsNullOrWhiteSpace($WorldAssemblyManifestPath)) {
$WorldAssemblyManifestPath = Join-Path $PluginRoot 'Standalone\Research\WorldAssemblerM1\round4\world_assembly.json'
}
$WorldAssemblyManifestPath = [IO.Path]::GetFullPath($WorldAssemblyManifestPath)
Assert-UnderPath $WorldAssemblyManifestPath $PluginRoot
foreach ($required in @($HostProject, $EditorCmd)) { foreach ($required in @($HostProject, $EditorCmd)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
throw "Required BuildHost commandlet input is missing: $required" throw "Required BuildHost commandlet input is missing: $required"
} }
} }
if (-not (Test-Path -LiteralPath $WorldAssemblyManifestPath -PathType Leaf)) {
throw "Reviewed world assembly manifest is missing: $WorldAssemblyManifestPath"
}
$foreignEditors = @(Get-Process -ErrorAction SilentlyContinue | $foreignEditors = @(Get-Process -ErrorAction SilentlyContinue |
Where-Object { $_.ProcessName -like 'UnrealEditor*' }) Where-Object { $_.ProcessName -like 'UnrealEditor*' })
@@ -63,10 +74,15 @@ foreach ($assetName in $ExpectedAssetHashes.Keys) {
} }
} }
foreach ($directory in @($OutputDirectory, $LogDirectory, $LocalAppData, foreach ($directory in @($OutputDirectory, $InputDirectory, $LogDirectory, $LocalAppData,
$UserDirectory, $DerivedDataCache, $ZenData)) { $UserDirectory, $DerivedDataCache, $ZenData)) {
[IO.Directory]::CreateDirectory($directory) | Out-Null [IO.Directory]::CreateDirectory($directory) | Out-Null
} }
Copy-Item -LiteralPath $WorldAssemblyManifestPath -Destination $StagedWorldAssembly -Force
$ManifestData = Get-Content -LiteralPath $StagedWorldAssembly -Raw | ConvertFrom-Json
if ($ManifestData.content_hash -ne '8933693d9560ba9f') {
throw "BuildHost world assembly input is not the reviewed round-4 manifest: $($ManifestData.content_hash)"
}
$CrashBefore = @{} $CrashBefore = @{}
foreach ($CrashRoot in $CrashRoots) { foreach ($CrashRoot in $CrashRoots) {
@@ -102,6 +118,7 @@ $BaseArguments = @(
'-run=VoxelForgeExplore', '-seed=0', '-archetype=TunnelNetwork', '-slot=0', '-run=VoxelForgeExplore', '-seed=0', '-archetype=TunnelNetwork', '-slot=0',
'-modes=export', '-gameconfig=1', '-coreworldtest=1', '-modes=export', '-gameconfig=1', '-coreworldtest=1',
'-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings', '-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings',
('-coreworldassembly="{0}"' -f $StagedWorldAssembly),
('-out="{0}"' -f $OutputDirectory) ('-out="{0}"' -f $OutputDirectory)
) )
@@ -153,6 +170,8 @@ try {
if ($CompletedLog -match '\[VoxelForgeCoreWorldTest\] pass complete=1' -and if ($CompletedLog -match '\[VoxelForgeCoreWorldTest\] pass complete=1' -and
$CompletedLog -match 'Commandlet VoxelForgeExploreCommandlet_\d+ finished execution \(result 0\)') { $CompletedLog -match 'Commandlet VoxelForgeExploreCommandlet_\d+ finished execution \(result 0\)') {
$ExitCode = 0 $ExitCode = 0
} elseif ($CompletedLog -match 'Commandlet VoxelForgeExploreCommandlet_\d+ finished execution \(result (-?\d+)\)') {
$ExitCode = [int]$Matches[1]
} }
} }
if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) { if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) {
@@ -188,6 +207,15 @@ foreach ($Phase in @('initial', 'anchor', 'edit', 'surface_travel', 'surface_str
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] worker_hash_matrix=1/8/16 stable=1') { if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] worker_hash_matrix=1/8/16 stable=1') {
throw "BuildHost Core actor log is missing the W1/W8/W16 determinism marker. See $FinalLog." throw "BuildHost Core actor log is missing the W1/W8/W16 determinism marker. See $FinalLog."
} }
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] world_assembly_hash=8933693d9560ba9f plans=1 native=1') {
throw "BuildHost Core actor log does not prove the reviewed native SurfaceWorld plan was attached. See $FinalLog."
}
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] native_biome_field=1 .*sampled_biomes=[2-9]') {
throw "BuildHost Core actor log does not prove native SurfaceWorld biome assignment. See $FinalLog."
}
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] matched_surface_tile_timing=1 .*same_keys=1 tiles=2553') {
throw "BuildHost Core actor log is missing the matched native/legacy-bridge surface timing. See $FinalLog."
}
$NewCrashes = @(Get-NewCrashFolders) $NewCrashes = @(Get-NewCrashFolders)
if ($NewCrashes.Count -gt 0) { if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during Core actor validation: $($NewCrashes -join '; ')" throw "New UECC crash folders appeared during Core actor validation: $($NewCrashes -join '; ')"