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:
1 parent
2d0cec90e4
commit
f67b519e75
11 files changed
+1002
-45
No files matched your search
+61
-13
@@ -1157,8 +1157,8 @@ fidelity fallback while a native heightfield with fixed-order noise and per-colu
|
||||
caching is developed.
|
||||
|
||||
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
|
||||
choices for the next implementation pass:
|
||||
eight generator types, five passage types, four passage styles, and three transitions. These are the
|
||||
round-4 recommendations; the SurfaceWorld row is superseded by §8.20:
|
||||
|
||||
| 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. |
|
||||
| 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;
|
||||
for low-cost discrete fields such as slabs, mazes, shafts, and islands, native support keeps the
|
||||
worker path explicit and avoids charging every sample for a second field implementation. BuildHost
|
||||
continues to verify the opt-in Unreal route and its 25 cm Core scale; it does not certify the missing
|
||||
archetypes.
|
||||
The copied evaluator remains a compatibility route when exact density is required and no reviewed
|
||||
native manifest is attached. For low-cost discrete fields such as slabs, mazes, shafts, and islands,
|
||||
native support keeps the worker path explicit and avoids charging every sample for a second field
|
||||
implementation. BuildHost verifies the opt-in Unreal route and its 25 cm Core scale; it does not
|
||||
certify the missing archetypes.
|
||||
|
||||
**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,
|
||||
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.
|
||||
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
|
||||
the owner's game.
|
||||
At the end of round 4 this was standalone evidence, not yet connected to the settings/content-hash
|
||||
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
|
||||
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
|
||||
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)
|
||||
|
||||
> **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
|
||||
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
|
||||
layout is the reviewed manifest, not a pool shuffle: peers must load the same season asset and compare
|
||||
`AVoxelWorld::GetCurrentSeasonContentHash()` before accepting play. For the legacy path the layout remains
|
||||
deterministic from the effective seed. Every peer then generates terrain locally; geometry never crosses
|
||||
the wire. The transport/RPC that performs this join handshake remains out of tree.
|
||||
layout is the reviewed season manifest; when Core is enabled with a native SurfaceWorld, the
|
||||
reviewed world-assembly digest is also part of `AVoxelWorld::GetCurrentSeasonContentHash()`. Peers
|
||||
compare that active content hash before accepting play. With Core disabled the optional assembly
|
||||
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]])
|
||||
→ it is the only thing that must sync. Since carving is a minor feature, this traffic is small.
|
||||
|
||||
|
||||
+24
-9
@@ -145,7 +145,7 @@ remains `0`, and lateral regions remain gated off.
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `../../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/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. |
|
||||
@@ -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.
|
||||
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. |
|
||||
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
|
||||
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; 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. |
|
||||
| `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. |
|
||||
@@ -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. |
|
||||
| `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. |
|
||||
| `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". |
|
||||
|
||||
> **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`). |
|
||||
| `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
|
||||
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
|
||||
@@ -852,12 +854,15 @@ affected keys under a new epoch. Existing streaming anchors participate; the hea
|
||||
covers `CollisionOnly` policy.
|
||||
|
||||
`-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.
|
||||
It also probes the finite origin landing room and Core-mode biome query. `VoxelForge.Determinism.CrossPlatformDigest`
|
||||
is the narrow switch-off baseline; it passes, but does not instantiate an actor with the switch false.
|
||||
See the report for that limitation, the changed world hashes, classification evidence, and owner-editor
|
||||
instructions. The Core route is opt-in; the legacy path remains the default.
|
||||
`-coreworldassembly=PATH` supplies a reviewed manifest transiently for this test. The native surface
|
||||
probe samples `Heightfield2D` directly from the Core snapshot; every one of its 2,553 expected visual
|
||||
tile hashes is compared with an independent standalone Core reference. The 125 collision-desired
|
||||
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
|
||||
[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
|
||||
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,
|
||||
VerticalShaftParams, and FloatingIslandParams. The shape-world evaluator still lacks their native
|
||||
terrain fields: FlatPlain, CrystalChamber, Maze, VerticalShafts, FloatingIslands, and water
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Policies/CondensedJsonPrintPolicy.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
#include "UObject/TextProperty.h"
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include "VoxelSeasonAsset.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
@@ -271,6 +274,95 @@ void WriteCoreJsonValue(FCoreJsonWriter& Writer, const TCHAR* Identifier,
|
||||
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(
|
||||
@@ -380,6 +472,10 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
Passages.Add(MakeShared<FJsonValueObject>(CorePassageJson(Passage)));
|
||||
}
|
||||
Root->SetArrayField(TEXT("passages"), MoveTemp(Passages));
|
||||
if (!AttachValidatedWorldAssembly(Settings, StrateManager, Root, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Root->SetArrayField(TEXT("density_samples"), {});
|
||||
TSharedRef<FJsonObject> ViewerStart = MakeShared<FJsonObject>();
|
||||
ViewerStart->SetBoolField(TEXT("found_open_space"), false);
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
#include "VoxelSeasonAsset.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "Misc/FileHelper.h"
|
||||
#endif
|
||||
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include <string>
|
||||
|
||||
int32 UVoxelSettings::GetEffectiveWorldSeed() const
|
||||
{
|
||||
if (!Season.IsNull())
|
||||
@@ -49,3 +56,123 @@ int32 UVoxelSettings::GetEffectiveSeasonNumber() const
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "Engine/World.h"
|
||||
#include "Misc/IQueuedWork.h"
|
||||
#include "Misc/QueuedThreadPool.h"
|
||||
#include "Misc/SecureHash.h"
|
||||
#include "RealtimeMeshCollisionLibrary.h"
|
||||
#include "Camera/PlayerCameraManager.h"
|
||||
#include "Async/Async.h"
|
||||
@@ -7259,6 +7260,15 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
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 =
|
||||
MakeShared<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe>();
|
||||
@@ -7786,6 +7796,18 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
|
||||
}
|
||||
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
|
||||
// epoch. Before the first build there is no array to promote into; LoadTile below still uses
|
||||
// the same CollisionCritical work class and priority queue.
|
||||
@@ -13222,7 +13244,47 @@ int32 AVoxelWorld::GetCurrentSeason() 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);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
|
||||
@@ -28,6 +28,14 @@ public:
|
||||
float GetEffectiveWorldRadiusVoxels() 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)
|
||||
//=========================================================================
|
||||
@@ -365,6 +373,21 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Voxel|Strates")
|
||||
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.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Strates")
|
||||
int32 CurrentSeason = 1;
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
@@ -341,6 +342,7 @@ struct FExploreArguments
|
||||
int32 Slot = 4;
|
||||
FString StrateReference;
|
||||
FString SettingsReference;
|
||||
FString CoreWorldAssemblyManifestPath;
|
||||
bool bRender = true;
|
||||
bool bWalk = 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("strateref="), OutArguments.StrateReference);
|
||||
FParse::Value(*Params, TEXT("settingsref="), OutArguments.SettingsReference);
|
||||
FParse::Value(*Params, TEXT("coreworldassembly="), OutArguments.CoreWorldAssemblyManifestPath);
|
||||
FParse::Value(*Params, TEXT("modes="), ModesText);
|
||||
// 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
|
||||
@@ -987,6 +990,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
OutError = TEXT("coreworldtest requires settingsref and must omit strateref.");
|
||||
return false;
|
||||
}
|
||||
if (!OutArguments.CoreWorldAssemblyManifestPath.IsEmpty() && !OutArguments.bCoreWorldTest)
|
||||
{
|
||||
OutError = TEXT("coreworldassembly is accepted only by coreworldtest.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (OutText.IsEmpty())
|
||||
{
|
||||
@@ -13197,6 +13205,30 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
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>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient));
|
||||
int32 ResolvedSeed = AuthoredSettings->Seed;
|
||||
@@ -13227,6 +13259,9 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
TSet<FVoxelTileKey> ExpectedInitialDesiredSet;
|
||||
TSet<FVoxelTileKey> ExpectedTravelDesiredSet;
|
||||
TSet<FVoxelTileKey> ExpectedSurfaceTravelDesiredSet;
|
||||
TSet<FVoxelTileKey> ExpectedSurfaceCollisionSet;
|
||||
int32 ExpectedSurfaceCollisionBodyCount = 0;
|
||||
int32 ExpectedSurfaceCollisionNoBodyCount = 0;
|
||||
TArray<FVoxelTileKey> ExpectedAffectedTiles;
|
||||
FVector CarveLocalPositionCm = FVector::ZeroVector;
|
||||
FVoxelTileKey CarveTargetTile;
|
||||
@@ -13234,10 +13269,18 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
constexpr double CoreVoxelSizeCm = 25.0;
|
||||
const double CarveBrushRadiusCoreVoxels =
|
||||
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
||||
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
|
||||
uint64 ExpectedEditDigest = 0;
|
||||
std::array<std::uint64_t, 4> InitialAggregates{};
|
||||
std::array<std::uint64_t, 4> TravelAggregates{};
|
||||
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 ExpectedTravelTileCount = 0;
|
||||
std::size_t TravelFineTilesCompiled = 0;
|
||||
@@ -13256,8 +13299,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
VoxelForgeCore::TileRegionCompileStats SurfaceTravelCompileStats;
|
||||
const VoxelForgeCore::Vec3d TravelCameraVoxels{-156.75164794921875, 25.093801498413086,
|
||||
-823.67364501953125};
|
||||
const VoxelForgeCore::Vec3d SurfaceCameraVoxels{-156.75164794921875, 25.093801498413086,
|
||||
-1024.0};
|
||||
const VoxelForgeCore::Vec3d SurfaceCameraVoxels{37.0, 165.0, -2011.0};
|
||||
uint32 NativeSurfaceBiomeId = 0;
|
||||
int32 NativeBiomeIdsSampled = 0;
|
||||
int32 NativeBiomeSampleCount = 0;
|
||||
bool bNativeBiomeFieldValidated = false;
|
||||
{
|
||||
FTCHARToUTF8 Utf8Json(*Json);
|
||||
std::string CoreError;
|
||||
@@ -13271,6 +13317,102 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
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));
|
||||
const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({});
|
||||
@@ -13314,6 +13456,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
Request.workerLimit = 8;
|
||||
Request.detail = VoxelForgeCore::DetailVariant::coherent;
|
||||
Request.includeFarWorld = true;
|
||||
MatchedTimingInitialDesired = Desired;
|
||||
MatchedTimingInitialRequest = Request;
|
||||
FStandaloneCoreAdapterScheduler Scheduler;
|
||||
if (!ReferenceWorld.compile(Request, Scheduler, CoreError))
|
||||
{
|
||||
@@ -13651,6 +13795,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
}
|
||||
std::vector<VoxelForgeCore::TileKey> MissingSurfaceFineTiles;
|
||||
MissingSurfaceFineTiles.reserve(SurfaceTravelDesired.size());
|
||||
MatchedTimingSurfaceDesired = SurfaceTravelDesired;
|
||||
for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired)
|
||||
{
|
||||
const FVoxelTileKey RuntimeKey(
|
||||
@@ -13659,8 +13804,10 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
if (Key.lod < ReferenceWorld.configuration().fullResolutionLodCount)
|
||||
MissingSurfaceFineTiles.push_back(Key);
|
||||
}
|
||||
MatchedTimingSurfaceMissingFine = MissingSurfaceFineTiles;
|
||||
// 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.
|
||||
const double NativeSurfaceRegionCompileStart = FPlatformTime::Seconds();
|
||||
ReferenceWorld.retainTileRegions(SurfaceTravelDesired);
|
||||
if (!ReferenceWorld.compileTileRegion(MissingSurfaceFineTiles, 16, Scheduler, CoreError,
|
||||
&SurfaceTravelFineTilesCompiled, &SurfaceTravelCompileStats))
|
||||
@@ -13669,11 +13816,14 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
NativeSurfaceRegionCompileSeconds = FPlatformTime::Seconds()
|
||||
- NativeSurfaceRegionCompileStart;
|
||||
SurfaceTravelRegionsBuilt = SurfaceTravelCompileStats.regionsBuilt;
|
||||
std::vector<FStandaloneCoreAdapterTileHashes> SurfaceTravelRows;
|
||||
SurfaceTravelRows.reserve(SurfaceTravelDesired.size());
|
||||
for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired)
|
||||
{
|
||||
const double TileGenerationStart = FPlatformTime::Seconds();
|
||||
const std::optional<VoxelForgeCore::TileMesh> Mesh =
|
||||
ReferenceWorld.generateTile(Key, TravelOptions, CoreError);
|
||||
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
|
||||
@@ -13683,6 +13833,9 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
NativeSurfaceTileGenerationSeconds += FPlatformTime::Seconds()
|
||||
- TileGenerationStart;
|
||||
NativeSurfaceTriangleCount += Mesh->indices.size() / 3;
|
||||
const FVoxelTileKey RuntimeKey(
|
||||
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
|
||||
FVoxelForgeCoreTileHashRecord Record;
|
||||
@@ -13698,6 +13851,27 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
{Record.Density, Record.Mesh, Record.Payload, Record.Topology}});
|
||||
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);
|
||||
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"),
|
||||
@@ -13719,6 +13893,107 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
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()
|
||||
.AllowAudioPlayback(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;
|
||||
int32 ExpectedBodyCount = 0;
|
||||
int32 ExpectedNoBodyCount = 0;
|
||||
@@ -14178,6 +14452,21 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
@@ -14201,6 +14490,39 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
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;
|
||||
std::vector<VoxelForgeCore::TileKey> ActorSurfaceTravelDesired;
|
||||
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* Expected = ExpectedSurfaceTravelHashes.Find(Key);
|
||||
if (Actual == nullptr || Expected == nullptr || !Actual->bHasHashes
|
||||
|| (!Expected->bEmpty && !Actor->LoadedTiles.Contains(Key))
|
||||
|| Actual->Density != Expected->Density || Actual->Mesh != Expected->Mesh
|
||||
|| (!Expected->bEmpty && !Actor->LoadedTiles.Contains(Key)))
|
||||
{
|
||||
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->Triangles != Expected->Triangles || Actual->bEmpty != Expected->bEmpty)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core actor surface-strate tile differs from standalone Core at (%d,%d,%d) lod=%d."),
|
||||
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
|
||||
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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -14255,20 +14594,20 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
*SurfaceCoverageError);
|
||||
return false;
|
||||
}
|
||||
const FVoxelBiomeQuery SurfaceBiome = Actor->GetBiomeAtWorldLocation(SurfaceWorldPosition);
|
||||
if (!SurfaceBiome.bHasBiomes || SurfaceBiome.DominantContextIndex < 0)
|
||||
if (!bNativeBiomeFieldValidated || NativeBiomeIdsSampled < 2)
|
||||
{
|
||||
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;
|
||||
}
|
||||
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,
|
||||
ActualSurfaceVisualDesiredSet.Num(), Actor->LoadedTiles.Num(),
|
||||
static_cast<unsigned long long>(SurfaceTravelFineTilesCompiled),
|
||||
static_cast<unsigned long long>(SurfaceTravelRegionsBuilt), ActorSurfaceLocalTiles,
|
||||
ActorSurfaceCheckedFaces, ActorSurfaceTransitionFaces,
|
||||
SurfaceBiome.DominantContextIndex, *SurfaceBiome.DominantName.ToString(),
|
||||
ActorSurfaceCheckedFaces, ActorSurfaceTransitionFaces, ExpectedSurfaceCollisionSet.Num(),
|
||||
ActualSurfaceCollisionReady, ActualSurfaceCollisionNoBody,
|
||||
NativeSurfaceBiomeId, NativeBiomeIdsSampled,
|
||||
static_cast<unsigned long long>(SurfaceTravelAggregates[0]),
|
||||
static_cast<unsigned long long>(SurfaceTravelAggregates[1]),
|
||||
static_cast<unsigned long long>(SurfaceTravelAggregates[2]),
|
||||
|
||||
@@ -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
|
||||
controls, bounds, determinism, headless query and matched captures are recorded above. Physics
|
||||
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
|
||||
hash path and connect Core behind `bUseVoxelForgeCore`; prove that flag-off behavior remains
|
||||
unchanged. The owner builds and reviews the in-game two-biome surface, enclosed cap lighting and
|
||||
capsule-walkable route. Run the matched 23.085 s workload, report full-view and local query/
|
||||
collision readiness separately, and assess the approximately 1.57 s aim. No BuildHost or owner
|
||||
project build was done here.
|
||||
- **Round 5:** Complete for plugin-side delivery. The manifest/content-hash path, native Core
|
||||
surface, BuildHost tile/collision coverage, source-level switch-off check, matched timing and
|
||||
handoff are recorded below. The owner's project build, in-game walk, and cap-lighting review are
|
||||
the remaining owner checks.
|
||||
|
||||
### 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
|
||||
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.
|
||||
|
||||
— 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
|
||||
@@ -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
|
||||
the exact-density bridge improves the surface field, but water/presentation and the other missing
|
||||
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.
|
||||
@@ -1,6 +1,7 @@
|
||||
[CmdletBinding()]
|
||||
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.
|
||||
@@ -13,6 +14,8 @@ $StagePlugin = Join-Path $HostRoot 'Plugins\VoxelForge'
|
||||
$HostProject = Join-Path $HostRoot 'HostProject.uproject'
|
||||
$OutputRoot = Join-Path $StagePlugin 'Saved\CoreWorldValidation'
|
||||
$OutputDirectory = Join-Path $OutputRoot 'Output'
|
||||
$InputDirectory = Join-Path $OutputRoot 'Input'
|
||||
$StagedWorldAssembly = Join-Path $InputDirectory 'world_assembly.json'
|
||||
$LogDirectory = Join-Path $OutputRoot 'Logs'
|
||||
$LocalAppData = Join-Path $OutputRoot 'LocalAppData'
|
||||
$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 $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)) {
|
||||
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
|
||||
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 |
|
||||
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)) {
|
||||
[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 = @{}
|
||||
foreach ($CrashRoot in $CrashRoots) {
|
||||
@@ -102,6 +118,7 @@ $BaseArguments = @(
|
||||
'-run=VoxelForgeExplore', '-seed=0', '-archetype=TunnelNetwork', '-slot=0',
|
||||
'-modes=export', '-gameconfig=1', '-coreworldtest=1',
|
||||
'-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings',
|
||||
('-coreworldassembly="{0}"' -f $StagedWorldAssembly),
|
||||
('-out="{0}"' -f $OutputDirectory)
|
||||
)
|
||||
|
||||
@@ -153,6 +170,8 @@ try {
|
||||
if ($CompletedLog -match '\[VoxelForgeCoreWorldTest\] pass complete=1' -and
|
||||
$CompletedLog -match 'Commandlet VoxelForgeExploreCommandlet_\d+ finished execution \(result 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) {
|
||||
@@ -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') {
|
||||
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)
|
||||
if ($NewCrashes.Count -gt 0) {
|
||||
throw "New UECC crash folders appeared during Core actor validation: $($NewCrashes -join '; ')"
|
||||
|
||||
Reference in new issue
Block a user