wip(unreal): VoxelForgeCore UBT module + asset adapter reproduce the standalone world

Luna, integration round 1 (BuildHost only; owner's project and assets untouched):
- New runtime module VoxelForgeCore: one shim per Standalone/Core/core_sources.txt row
  (sources stay canonical under Standalone/), NoPCH/no unity, manifest check; VoxelForge
  links it (no runtime routing yet). UE link-safety audit: 18 shims, 83,843 symbol lines
  vs 399 Unreal-style names, 0 clashes.
- Adapter (editor commandlet path) turns staged copies of the owner's DA_* assets into
  schema-v1 JSON; with clip_radius=3/max_clip_level=8 it reproduces all four Core hashes
  (0xdb2991c73215af4c, 0x8236c5edd7fc8e4f, 0x4c48fa75a543d58e, 0xbea22e76b34a4dfc) and
  walkability 5,642/5,642 at worker limits 1, 8 and 16.
- Noted: the owner's settings asset says max clip level 6 (standalone used 8); Core uses
  25 cm voxels vs the plugin runtime's 12.5 cm.
Not done yet: AVoxelWorld switch and Core runtime route (round 2).
Docs: CODEMAP, ARCHITECTURE, SHARED_CPP_LIBRARY_PLAN, UNREAL_INTEGRATION_REPORT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-08 19:38:18 +02:00
1 parent a1dfc0422e
commit 29ca99af7f
32 files changed
+919 -69

No files matched your search

+37
View File
@@ -1065,6 +1065,43 @@ timings are intentionally diagnostic metadata and do not affect world or image c
viewer remains a separate display-only consumer: it must load this OBJ/manifest and never evaluate viewer remains a separate display-only consumer: it must load this OBJ/manifest and never evaluate
density or generate geometry. density or generate geometry.
### 8.17 Standalone Core in Unreal — integration round 1
**Status (2026-10-08):** the `VoxelForgeCore` runtime module and manifest shims build and link in the
isolated BuildHost. The editor commandlet can adapt the owner's copied settings/strate assets into
schema-v1 JSON, resolve it through `ResolvedWorldSnapshot::FromJson`, and reproduce the standalone
hashes at compile worker limits 1, 8, and 16. This has not yet changed the `AVoxelWorld` generation
route. There is no `bUseVoxelForgeCore` property, and no world should be switched yet.
The commandlet adapter is game-thread-only: it resolves UObjects and soft references, writes stable
property/map/set order, and emits values rather than UObject pointers. It writes the standalone
schema scale of 25 cm per voxel. The plugin's existing Runtime generator still defines
`VOXEL_SIZE=12.5 cm`, so a Core mesh-to-`FVoxelMeshData` bridge must define the coordinate/scale
conversion before runtime use. The staged `DA_Settings` also says `MaxClipLevel=6`, while the
standalone owner JSON says 8; hash validation keeps the authored JSON unchanged and applies the
reference harness view (`clip_radius=3`, `max_clip_level=8`) only to its validation copy.
The intended side-by-side runtime route is:
1. Add `bUseVoxelForgeCore=false` to `AVoxelWorld`. The false branch must retain the existing field,
mesher, output, streaming, and collision behavior.
2. On the game thread, adapt owner assets to an immutable schema-v1 snapshot. Keep UObjects out of
Core and worker data.
3. On the Core branch, ask `World::desiredTiles` and `prioritizeTiles` for the target set/order;
compile the initial field using a `JobScheduler` backed by Unreal's task system. The plugin still
owns work admission, worker lifetime, cancellation, `bShuttingDown`, and the generation `Epoch`.
4. Run `generateTile` on the plugin workers, explicitly reconcile Core centimetre positions with
the Runtime voxel scale, then copy mesh arrays/groups into `FVoxelMeshData`. Let the existing
`BuildTileStreamSet` → MPSC result → epoch check → RealtimeMesh upload/collision-cook path consume
the result.
5. For carving, drain Core calls on the current `World`, call `applyEditTransaction` with an
authority sequence and ordered brushes, bump the epoch, and regenerate its returned desired tiles.
Keep the current plugin behavior when the switch is false.
The module-link and asset-adapter evidence, including the settings mismatch and incomplete runtime
work, is recorded in [`Standalone/UNREAL_INTEGRATION_REPORT.md`](Standalone/UNREAL_INTEGRATION_REPORT.md).
Keep every §8.10 streaming, cancellation, epoch, collision-handoff, and per-frame-budget invariant
when implementing this route; Core does not own those systems.
## 9. Multiplayer model (listen-server first, dedicated-friendly) ## 9. Multiplayer model (listen-server first, dedicated-friendly)
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
+30 -4
View File
@@ -5,7 +5,8 @@
> as code is edited, so trust the **symbol name** first and the line as a hint. > as code is edited, so trust the **symbol name** first and the line as a hint.
> >
> Plugin root: `Source/VoxelForge/` · ~8,300 lines of C++ across 25 files. > Plugin root: `Source/VoxelForge/` · ~8,300 lines of C++ across 25 files.
> Comments in the code are mixed **French + English**. UE module = `VoxelForge` (Runtime). > Comments in the code are mixed **French + English**. UE modules: `VoxelForge` and
> `VoxelForgeCore` (Runtime), plus `VoxelForgeEditor` (Editor).
--- ---
@@ -143,12 +144,16 @@ remains `0`, and lateral regions remain gated off.
### 3.1 Module & build ### 3.1 Module & build
| File | Role | | File | Role |
|------|------| |------|------|
| `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. | | `../../VoxelForge.uplugin` | Plugin manifest. Runtime modules `VoxelForge` and `VoxelForgeCore`, plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. |
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. | | `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**, `VoxelForgeCore`. The Core dependency is linked; `AVoxelWorld` does not route through it yet. |
| `../../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. |
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval. | `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. | | Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. |
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). | | `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. Optional `-exporttilex/y/z` sets an exact L0 tile origin for controlled export/game parity; omitting all three preserves the existing center-derived origin. `-regioneval=1` retains the exact-field step-2c research harness. `-step2d` and `-step2e` retain their earlier benchmarks. `-step2f` compares the metre-scaled step 2e baseline and the coherent field for all eight profiles on the same 259³ ring (32.25 m at this lattice), counts sub-1 m³ connected components in a 128-sample padded volume, measures mesh dihedrals, detail spectrum, LOD surface distance (p95 threshold 1 sample = 12.5 cm), chamber-pitch autocorrelation, roof/floor coverage, and six-worker timing. It also runs the sound/undersized-bound audit, seam injection, and 1-vs-6-worker hash checks and writes profile slices and JSON under `Saved/Research/step2f`. | | `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/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. | | `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. |
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. | | `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. |
| `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. | | `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. |
@@ -815,6 +820,27 @@ second density path.
| `VoxelForgeWormBlockSkipTest.cpp` | `VoxelForge.Determinism.WormBlockSkipSoundness` | Soundness and coverage of the worker-local worm N1 block proof (`voxel.WormBlockSkip`). | | `VoxelForgeWormBlockSkipTest.cpp` | `VoxelForge.Determinism.WormBlockSkipSoundness` | Soundness and coverage of the worker-local worm N1 block proof (`voxel.WormBlockSkip`). |
| `VoxelForgePlayerFitWindow.h` | — | Shared measurement-window policy for player-fit diagnostics (test/editor side only). | | `VoxelForgePlayerFitWindow.h` | — | Shared measurement-window policy for player-fit diagnostics (test/editor side only). |
### 3.13 Core module and schema adapter (integration round 1)
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
[`Standalone/UNREAL_INTEGRATION_REPORT.md`](Standalone/UNREAL_INTEGRATION_REPORT.md) for build and
headless evidence.
`RunStandaloneWorldExport` in `VoxelForgeExploreCommandlet.cpp` serializes owner assets on the game
thread to schema-v1 JSON. `FStandaloneCoreAdapterScheduler` and
`VerifyStandaloneCoreAdapter` parse that JSON through `ResolvedWorldSnapshot::FromJson`, compile at
worker limits 1, 8, and 16, and compare the 2,226-tile hashes and passage walkability. The commandlet
sorts map/set members and JSON object keys to keep the export deterministic. Its test copy applies
the standalone benchmark view overrides (clip radius 3, max level 8); the saved asset-derived export
retains the authored clip radius and max level.
**Runtime status:** no `bUseVoxelForgeCore` property, Core generation route, mesh conversion, Core
carve transaction, or owner-facing switch instructions exist yet. `AVoxelWorld` still uses the
existing density generator, mesher, queues, epoch checks, RealtimeMesh upload, and collision path.
The Core dependency is present so the next round can wire the side-by-side route behind a default-off
switch while preserving that path.
## 4. The density pipeline (most-edited hot path) ## 4. The density pipeline (most-edited hot path)
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
+3
View File
@@ -79,6 +79,9 @@ public class VoxelForge : ModuleRules
// Private dependencies - only used in our .cpp files, not exposed in headers // Private dependencies - only used in our .cpp files, not exposed in headers
PrivateDependencyModuleNames.AddRange(new string[] PrivateDependencyModuleNames.AddRange(new string[]
{ {
// Core module is linked; runtime routing is not wired yet.
// Le module Core est lié; la génération Runtime ne l'utilise pas encore.
"VoxelForgeCore",
"Json", // Diffable Tier 5 promoted-record and season-manifest stores "Json", // Diffable Tier 5 promoted-record and season-manifest stores
"ImageWrapper", // PNG encode for the biome-map preview bake (BakeBiomePreview) "ImageWrapper", // PNG encode for the biome-map preview bake (BakeBiomePreview)
"RHI", // Texture3D create + RHIUpdateTexture3D for the density volume (mini-sun shadows) "RHI", // Texture3D create + RHIUpdateTexture3D for the density volume (mini-sun shadows)
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Compat/StandaloneDataAssetGlue.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Compat/UnrealPerlin.cpp"
}
@@ -0,0 +1,9 @@
// The public API is standard-only; this UBT boundary supplies its module export token locally.
#ifndef DLLEXPORT
#if defined(_WIN32)
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT __attribute__((visibility("default")))
#endif
#endif
#include "Core/src/VoxelForgeCore.cpp"
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelBiomeDefinition.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelCaveMorphology.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelDensityAblation.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelDensityOpStack.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelDensityProfile.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelDiffLayer.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelGenerator.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelHeightOpStack.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelSettings.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelStrateComposer.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelStrateManager.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelStrateMeasure.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelTerrainOpDefinition.cpp"
}
@@ -0,0 +1,7 @@
// Private implementation symbols stay internal to the Core module. Les symboles restent internes.
#define VFCORE_PRIVATE_IMPLEMENTATION 1
#include "CoreNamespacePreamble.h"
namespace vfcore::detail
{
#include "Source/VoxelForge/Private/VoxelWormField.cpp"
}
@@ -0,0 +1,7 @@
#pragma once
// The module entry must see UE's real CoreMinimal; the Core shims intentionally use Compat/CoreMinimal.
#ifndef VFCORE_ENGINE_COREMINIMAL_HEADER
#error VoxelForgeCore must provide the engine CoreMinimal header path for its module entry.
#endif
#include VFCORE_ENGINE_COREMINIMAL_HEADER
@@ -0,0 +1,6 @@
// Core module entry point. Le module garde ses wrappers Core isolés du PCH Unreal.
#include "CoreMinimal.h"
#include "ModuleManager.h"
IMPLEMENT_MODULE(FDefaultModuleImpl, VoxelForgeCore)
@@ -0,0 +1,132 @@
// VoxelForgeCore compiles the canonical Standalone sources in isolated, one-source shims.
// Le module compile les sources canoniques Standalone via des wrappers isolés.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnrealBuildTool;
public class VoxelForgeCore : ModuleRules
{
public VoxelForgeCore(ReadOnlyTargetRules Target) : base(Target)
{
string PluginRoot = Path.GetFullPath(Path.Combine(ModuleDirectory, "..", ".."));
string StandaloneRoot = Path.Combine(PluginRoot, "Standalone");
string ManifestPath = Path.Combine(StandaloneRoot, "Core", "core_sources.txt");
string ShimRoot = Path.Combine(ModuleDirectory, "Private", "Manifest");
string CoreMinimalHeader = Path.Combine(
EngineDirectory, "Source", "Runtime", "Core", "Public", "CoreMinimal.h").Replace('\\', '/');
ValidateManifest(ManifestPath, ShimRoot, StandaloneRoot);
PublicIncludePaths.Add(Path.Combine(StandaloneRoot, "Core", "include"));
PrivateIncludePaths.AddRange(new string[]
{
StandaloneRoot,
Path.Combine(EngineDirectory, "Source", "Runtime", "Core", "Public", "Modules"),
Path.Combine(StandaloneRoot, "Core", "src"),
Path.Combine(StandaloneRoot, "Compat"),
Path.Combine(StandaloneRoot, "Source"),
Path.Combine(StandaloneRoot, "Source", "VoxelForge", "Public"),
Path.Combine(StandaloneRoot, "Source", "VoxelForge", "Private")
});
PrivateDefinitions.Add("VFCORE_ENGINE_COREMINIMAL_HEADER=\"" + CoreMinimalHeader + "\"");
PrivateDefinitions.Add("UE_BUILD_SHIPPING=" + (Target.Configuration == UnrealTargetConfiguration.Shipping ? "1" : "0"));
// The wrapped VoxelForge sources use the standalone compatibility layer, which excludes editor-only fields.
PrivateDefinitions.Add("WITH_EDITORONLY_DATA=0");
PublicDependencyModuleNames.Add("Core");
CppStandard = CppStandardVersion.Cpp20;
// Canonical Standalone code predates UE's stricter warning defaults; keep this narrow to legal local shadowing.
CppCompileWarningSettings.ShadowVariableWarningLevel = WarningLevel.Off;
FPSemantics = FPSemanticsMode.Precise;
PCHUsage = PCHUsageMode.NoPCHs;
bUseUnity = false;
bEnableExceptions = true;
bUseRTTI = true;
}
private static void ValidateManifest(string ManifestPath, string ShimRoot, string StandaloneRoot)
{
if (!File.Exists(ManifestPath))
{
throw new BuildException("VoxelForgeCore source manifest is missing: {0}", ManifestPath);
}
string Root = Path.GetFullPath(StandaloneRoot) + Path.DirectorySeparatorChar;
string[] Sources = File.ReadAllLines(ManifestPath)
.Select(Line => Line.Trim().Replace('\\', '/'))
.Where(Line => Line.Length != 0 && !Line.StartsWith("#", StringComparison.Ordinal))
.ToArray();
if (Sources.Length == 0)
{
throw new BuildException("VoxelForgeCore source manifest is empty: {0}", ManifestPath);
}
if (Sources.Distinct(StringComparer.Ordinal).Count() != Sources.Length)
{
throw new BuildException("VoxelForgeCore source manifest contains duplicate rows: {0}", ManifestPath);
}
if (!Directory.Exists(ShimRoot))
{
throw new BuildException("VoxelForgeCore manifest shim directory is missing: {0}", ShimRoot);
}
HashSet<string> ExpectedShimNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (string Source in Sources)
{
if (Path.IsPathRooted(Source) || Source.Split('/').Any(Part => Part == ".."))
{
throw new BuildException("VoxelForgeCore manifest path escapes Standalone: {0}", Source);
}
string CanonicalPath = Path.GetFullPath(Path.Combine(StandaloneRoot, Source.Replace('/', Path.DirectorySeparatorChar)));
if (!CanonicalPath.StartsWith(Root, StringComparison.OrdinalIgnoreCase) || !File.Exists(CanonicalPath))
{
throw new BuildException("VoxelForgeCore canonical source is missing or outside Standalone: {0}", Source);
}
string ShimName = "VFCore_" + Regex.Replace(Source, "[^A-Za-z0-9]+", "_") + ".cpp";
ExpectedShimNames.Add(ShimName);
string ShimPath = Path.Combine(ShimRoot, ShimName);
if (!File.Exists(ShimPath))
{
throw new BuildException("VoxelForgeCore manifest row has no shim: {0} -> {1}", Source, ShimPath);
}
string Shim = File.ReadAllText(ShimPath);
string EscapedSource = Regex.Escape(Source);
if (Regex.Matches(Shim, "(?m)^\\s*#include\\s+\"" + EscapedSource + "\"\\s*$").Count != 1)
{
throw new BuildException("VoxelForgeCore shim must include its canonical manifest source exactly once: {0}", ShimPath);
}
bool IsApiBoundary = String.Equals(Source, "Core/src/VoxelForgeCore.cpp", StringComparison.Ordinal);
if (IsApiBoundary)
{
if (Regex.Matches(Shim, "(?m)^\\s*#include\\s+\"").Count != 1)
{
throw new BuildException("VoxelForgeCore API boundary shim must include only its canonical source: {0}", ShimPath);
}
}
else if (!Regex.IsMatch(Shim, "(?s)#include\\s+\"CoreNamespacePreamble\\.h\".*namespace\\s+vfcore::detail\\s*\\{.*#include\\s+\"" + EscapedSource + "\""))
{
throw new BuildException("VoxelForgeCore implementation shim must preload headers and wrap its source in vfcore::detail: {0}", ShimPath);
}
}
string[] ActualShims = Directory.GetFiles(ShimRoot, "*.cpp", SearchOption.TopDirectoryOnly)
.Select(Path.GetFileName)
.ToArray();
if (ActualShims.Length != ExpectedShimNames.Count
|| ActualShims.Any(Name => !ExpectedShimNames.Contains(Name)))
{
throw new BuildException(
"VoxelForgeCore shims and Core/core_sources.txt differ. Expected {0} shims under {1}; found {2}.",
ExpectedShimNames.Count, ShimRoot, ActualShims.Length);
}
}
}
@@ -48,10 +48,18 @@
#include "VoxelStrateComposer.h" #include "VoxelStrateComposer.h"
#include "VoxelTerrainOpDefinition.h" #include "VoxelTerrainOpDefinition.h"
#include "VoxelTypes.h" #include "VoxelTypes.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <array>
#include <chrono>
#include <cstdint>
#include <limits> #include <limits>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
namespace namespace
{ {
@@ -61,6 +69,9 @@ constexpr int32 DefaultRenderWidth = 512;
constexpr int32 DefaultRenderHeight = 288; constexpr int32 DefaultRenderHeight = 288;
constexpr float DefaultRenderStepVoxels = 0.5f; // 6.25 cm, as at 25 cm/voxel. constexpr float DefaultRenderStepVoxels = 0.5f; // 6.25 cm, as at 25 cm/voxel.
constexpr float DefaultRenderMaxDistanceVoxels = 320.0f; // 40 m. constexpr float DefaultRenderMaxDistanceVoxels = 320.0f; // 40 m.
// Schema-v1 follows Standalone's 25 cm voxel scale, independent of the legacy runtime module's 12.5 cm scale.
// Le schéma-v1 suit l'échelle Standalone de 25 cm, indépendante de l'échelle historique de 12,5 cm du module Runtime.
constexpr double StandaloneCoreVoxelSizeCm = 25.0;
constexpr int32 DefaultExportSize = 128; constexpr int32 DefaultExportSize = 128;
constexpr int32 DefaultExportStep = 1; constexpr int32 DefaultExportStep = 1;
constexpr int32 DefaultMaxWalkCells = 32000000; constexpr int32 DefaultMaxWalkCells = 32000000;
@@ -12268,20 +12279,28 @@ TSharedPtr<FJsonValue> StandaloneSerializeProperty(
if (const FSetProperty* Set = CastField<FSetProperty>(Property)) if (const FSetProperty* Set = CastField<FSetProperty>(Property))
{ {
TArray<TSharedPtr<FJsonValue>> Values; TArray<TSharedPtr<FJsonValue>> Values;
TArray<TPair<FString, TSharedPtr<FJsonValue>>> OrderedValues;
FScriptSetHelper Helper(Set, Data); FScriptSetHelper Helper(Set, Data);
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index) for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
{ {
if (Helper.IsValidIndex(Index)) if (Helper.IsValidIndex(Index))
{ {
Values.Add(StandaloneSerializeProperty( FString StableKey;
Set->ElementProp->ExportTextItem_Direct(
StableKey, Helper.GetElementPtr(Index), nullptr, nullptr, PPF_None);
OrderedValues.Emplace(StableKey, StandaloneSerializeProperty(
Set->ElementProp, Helper.GetElementPtr(Index), ActiveObjects)); Set->ElementProp, Helper.GetElementPtr(Index), ActiveObjects));
} }
} }
OrderedValues.Sort([](const auto& A, const auto& B) { return A.Key < B.Key; });
Values.Reserve(OrderedValues.Num());
for (const auto& Entry : OrderedValues) Values.Add(Entry.Value);
return MakeShared<FJsonValueArray>(MoveTemp(Values)); return MakeShared<FJsonValueArray>(MoveTemp(Values));
} }
if (const FMapProperty* Map = CastField<FMapProperty>(Property)) if (const FMapProperty* Map = CastField<FMapProperty>(Property))
{ {
TArray<TSharedPtr<FJsonValue>> Values; TArray<TSharedPtr<FJsonValue>> Values;
TArray<TPair<FString, TSharedPtr<FJsonValue>>> OrderedValues;
FScriptMapHelper Helper(Map, Data); FScriptMapHelper Helper(Map, Data);
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index) for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
{ {
@@ -12291,8 +12310,14 @@ TSharedPtr<FJsonValue> StandaloneSerializeProperty(
Map->KeyProp, Helper.GetKeyPtr(Index), ActiveObjects)); Map->KeyProp, Helper.GetKeyPtr(Index), ActiveObjects));
Pair->SetField(TEXT("value"), StandaloneSerializeProperty( Pair->SetField(TEXT("value"), StandaloneSerializeProperty(
Map->ValueProp, Helper.GetValuePtr(Index), ActiveObjects)); Map->ValueProp, Helper.GetValuePtr(Index), ActiveObjects));
Values.Add(MakeShared<FJsonValueObject>(Pair)); FString StableKey;
Map->KeyProp->ExportTextItem_Direct(
StableKey, Helper.GetKeyPtr(Index), nullptr, nullptr, PPF_None);
OrderedValues.Emplace(StableKey, MakeShared<FJsonValueObject>(Pair));
} }
OrderedValues.Sort([](const auto& A, const auto& B) { return A.Key < B.Key; });
Values.Reserve(OrderedValues.Num());
for (const auto& Entry : OrderedValues) Values.Add(Entry.Value);
return MakeShared<FJsonValueArray>(MoveTemp(Values)); return MakeShared<FJsonValueArray>(MoveTemp(Values));
} }
if (const FStructProperty* Struct = CastField<FStructProperty>(Property)) if (const FStructProperty* Struct = CastField<FStructProperty>(Property))
@@ -12368,9 +12393,16 @@ void StandaloneWriteJsonValue(
else if (Value->Type == EJson::Object) else if (Value->Type == EJson::Object)
{ {
Writer.WriteObjectStart(); Writer.WriteObjectStart();
for (const TPair<FString, TSharedPtr<FJsonValue>>& Field : Value->AsObject()->Values) TArray<FString> Keys;
Value->AsObject()->Values.GetKeys(Keys);
Keys.Sort();
for (const FString& Key : Keys)
{ {
StandaloneWriteJsonValue(Writer, *Field.Key, Field.Value); const TSharedPtr<FJsonValue>* Field = Value->AsObject()->Values.Find(Key);
if (Field != nullptr)
{
StandaloneWriteJsonValue(Writer, *Key, *Field);
}
} }
Writer.WriteObjectEnd(); Writer.WriteObjectEnd();
} }
@@ -12465,6 +12497,273 @@ struct FStandaloneExportSample
float Density = 0.0f; float Density = 0.0f;
}; };
class FStandaloneCoreAdapterScheduler final : public VoxelForgeCore::JobScheduler
{
public:
double parallel_for(std::size_t Count, std::size_t BatchSize, std::size_t WorkerLimit,
void* Context, Task Callback, double* WorkerActiveSeconds,
std::size_t WorkerActiveCapacity) const override
{
if (Count == 0) return 0.0;
if (WorkerActiveSeconds != nullptr)
{
std::fill(WorkerActiveSeconds, WorkerActiveSeconds + WorkerActiveCapacity, 0.0);
}
const std::size_t WorkerCount = std::clamp<std::size_t>(WorkerLimit, 1,
std::min<std::size_t>(16, std::max<std::size_t>(1, Count)));
const std::size_t Grain = std::max<std::size_t>(1, BatchSize);
std::atomic<std::size_t> Next{0};
std::vector<double> WorkerSeconds(WorkerCount, 0.0);
std::vector<std::thread> Threads;
Threads.reserve(WorkerCount);
for (std::size_t Worker = 0; Worker < WorkerCount; ++Worker)
{
Threads.emplace_back([&, Worker]
{
const auto Started = std::chrono::steady_clock::now();
for (;;)
{
const std::size_t First = Next.fetch_add(Grain, std::memory_order_relaxed);
if (First >= Count) break;
const std::size_t End = First + std::min(Grain, Count - First);
for (std::size_t Index = First; Index < End; ++Index)
{
Callback(Context, Index, Worker);
}
}
WorkerSeconds[Worker] = std::chrono::duration<double>(
std::chrono::steady_clock::now() - Started).count();
});
}
for (std::thread& Thread : Threads) Thread.join();
double TotalSeconds = 0.0;
for (std::size_t Worker = 0; Worker < WorkerCount; ++Worker)
{
TotalSeconds += WorkerSeconds[Worker];
if (WorkerActiveSeconds != nullptr && Worker < WorkerActiveCapacity)
{
WorkerActiveSeconds[Worker] = WorkerSeconds[Worker];
}
}
return TotalSeconds;
}
};
struct FStandaloneCoreAdapterTileHashes
{
VoxelForgeCore::TileKey Key;
std::array<std::uint64_t, 4> Hashes{};
};
void StandaloneCoreHashWord(std::uint64_t& Hash, std::uint64_t Word, int32 ByteCount)
{
for (int32 Byte = 0; Byte < ByteCount; ++Byte)
{
Hash ^= static_cast<std::uint8_t>(Word >> (Byte * 8));
Hash *= 1099511628211ull;
}
}
std::array<std::uint64_t, 4> StandaloneCoreAggregateHashes(
std::vector<FStandaloneCoreAdapterTileHashes>& Rows)
{
std::sort(Rows.begin(), Rows.end(), [](const auto& A, const auto& B)
{
if (A.Key.lod != B.Key.lod) return A.Key.lod < B.Key.lod;
if (A.Key.coordinate.z != B.Key.coordinate.z)
return A.Key.coordinate.z < B.Key.coordinate.z;
if (A.Key.coordinate.y != B.Key.coordinate.y)
return A.Key.coordinate.y < B.Key.coordinate.y;
return A.Key.coordinate.x < B.Key.coordinate.x;
});
std::array<std::uint64_t, 4> Aggregates{
14695981039346656037ull, 14695981039346656037ull,
14695981039346656037ull, 14695981039346656037ull};
for (const FStandaloneCoreAdapterTileHashes& Row : Rows)
{
for (std::size_t HashIndex = 0; HashIndex < Aggregates.size(); ++HashIndex)
{
std::uint64_t& Hash = Aggregates[HashIndex];
StandaloneCoreHashWord(Hash, static_cast<std::uint32_t>(Row.Key.lod), 4);
StandaloneCoreHashWord(Hash, static_cast<std::uint32_t>(Row.Key.coordinate.x), 4);
StandaloneCoreHashWord(Hash, static_cast<std::uint32_t>(Row.Key.coordinate.y), 4);
StandaloneCoreHashWord(Hash, static_cast<std::uint32_t>(Row.Key.coordinate.z), 4);
StandaloneCoreHashWord(Hash, Row.Hashes[HashIndex], 8);
}
}
return Aggregates;
}
bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
{
FTCHARToUTF8 Utf8Json(*Json);
std::string CoreError;
auto OwnerSnapshot = VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
if (!OwnerSnapshot)
{
OutError = FString::Printf(TEXT("VoxelForgeCore rejected adapter schema-v1 JSON: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
// Reference hashes use the established stream-benchmark view: clip radius 3 and max LOD 8.
// Les hachages de référence utilisent le profil de benchmark : rayon 3 et niveau LOD maximal 8.
// Keep asset-derived settings in owner_world.json; apply benchmark controls only to the copy.
// Garder les valeurs des assets dans owner_world.json; appliquer les réglages au seul duplicata.
const std::int32_t OwnerClipRadius = OwnerSnapshot->configuration().clipRadius;
const std::int32_t OwnerMaxLod = OwnerSnapshot->configuration().maxLod;
FString CoreValidationJson = Json;
const FString AuthoredClipField = FString::Printf(TEXT("\"clip_radius\":%d"), OwnerClipRadius);
const FString AuthoredAssetClipField = FString::Printf(TEXT("\"ClipRadius\":%d"), OwnerClipRadius);
const FString AuthoredMaxLodField = FString::Printf(TEXT("\"max_clip_level\":%d"), OwnerMaxLod);
const FString AuthoredAssetMaxLodField = FString::Printf(TEXT("\"MaxClipLevel\":%d"), OwnerMaxLod);
CoreValidationJson.ReplaceInline(*AuthoredClipField, TEXT("\"clip_radius\":3"), ESearchCase::CaseSensitive);
CoreValidationJson.ReplaceInline(*AuthoredAssetClipField, TEXT("\"ClipRadius\":3"), ESearchCase::CaseSensitive);
CoreValidationJson.ReplaceInline(*AuthoredMaxLodField, TEXT("\"max_clip_level\":8"), ESearchCase::CaseSensitive);
CoreValidationJson.ReplaceInline(*AuthoredAssetMaxLodField, TEXT("\"MaxClipLevel\":8"), ESearchCase::CaseSensitive);
if (!CoreValidationJson.Contains(TEXT("\"clip_radius\":3"), ESearchCase::CaseSensitive)
|| !CoreValidationJson.Contains(TEXT("\"ClipRadius\":3"), ESearchCase::CaseSensitive)
|| !CoreValidationJson.Contains(TEXT("\"max_clip_level\":8"), ESearchCase::CaseSensitive)
|| !CoreValidationJson.Contains(TEXT("\"MaxClipLevel\":8"), ESearchCase::CaseSensitive))
{
OutError = TEXT("Core adapter validation could not apply the reference clipmap overrides.");
return false;
}
for (const std::size_t WorkerLimit : {1u, 8u, 16u})
{
FTCHARToUTF8 Utf8ValidationJson(*CoreValidationJson);
auto Snapshot = VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
std::string_view(Utf8ValidationJson.Get(),
static_cast<std::size_t>(Utf8ValidationJson.Length())), CoreError);
if (!Snapshot)
{
OutError = FString::Printf(TEXT("VoxelForgeCore rejected reference-validation JSON: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
VoxelForgeCore::World CoreWorld(std::move(*Snapshot));
const VoxelForgeCore::WorldConfiguration& Configuration = CoreWorld.configuration();
const VoxelForgeCore::Vec3d Camera = Configuration.viewerStartFound
? Configuration.viewerStartVoxels : VoxelForgeCore::Vec3d{};
const std::vector<VoxelForgeCore::TileKey> Desired = CoreWorld.desiredTiles(Camera);
if (Desired.empty())
{
OutError = TEXT("VoxelForgeCore adapter validation produced an empty initial clipmap.");
return false;
}
const std::int32_t IntMax = std::numeric_limits<std::int32_t>::max();
const std::int32_t IntMin = std::numeric_limits<std::int32_t>::min();
VoxelForgeCore::Vec3i FineMin{IntMax, IntMax, IntMax};
VoxelForgeCore::Vec3i FineMax{IntMin, IntMin, IntMin};
VoxelForgeCore::Vec3i ClipMin{IntMax, IntMax, IntMax};
VoxelForgeCore::Vec3i ClipMax{IntMin, IntMin, IntMin};
auto IncludeExtent = [](std::int32_t& Min, std::int32_t& Max,
std::int32_t Origin, std::int32_t Extent)
{
Min = std::min(Min, Origin);
Max = std::max(Max, Origin + Extent);
};
for (const VoxelForgeCore::TileKey& Key : Desired)
{
const std::int32_t Extent = 32 << Key.lod;
const std::int32_t OriginX = Key.coordinate.x * Extent;
const std::int32_t OriginY = Key.coordinate.y * Extent;
const std::int32_t OriginZ = Key.coordinate.z * Extent;
IncludeExtent(ClipMin.x, ClipMax.x, OriginX, Extent);
IncludeExtent(ClipMin.y, ClipMax.y, OriginY, Extent);
IncludeExtent(ClipMin.z, ClipMax.z, OriginZ, Extent);
if (Key.lod < Configuration.fullResolutionLodCount)
{
IncludeExtent(FineMin.x, FineMax.x, OriginX, Extent);
IncludeExtent(FineMin.y, FineMax.y, OriginY, Extent);
IncludeExtent(FineMin.z, FineMax.z, OriginZ, Extent);
}
}
if (FineMin.x == IntMax || ClipMin.x == IntMax)
{
OutError = TEXT("VoxelForgeCore adapter validation could not derive clipmap bounds.");
return false;
}
VoxelForgeCore::ShapeBuildRequest Request;
Request.minVoxel = FineMin;
Request.maxVoxel = FineMax;
Request.clipMinVoxel = ClipMin;
Request.clipMaxVoxel = ClipMax;
Request.workerLimit = WorkerLimit;
Request.detail = VoxelForgeCore::DetailVariant::coherent;
Request.includeFarWorld = true;
FStandaloneCoreAdapterScheduler Scheduler;
if (!CoreWorld.compile(Request, Scheduler, CoreError))
{
OutError = FString::Printf(TEXT("VoxelForgeCore adapter compile failed at workers=%llu: %s"),
static_cast<unsigned long long>(WorkerLimit), UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
VoxelForgeCore::TileGenerationOptions TileOptions;
TileOptions.hashDensity = true;
std::vector<FStandaloneCoreAdapterTileHashes> Rows;
Rows.reserve(Desired.size());
for (const VoxelForgeCore::TileKey& Key : Desired)
{
auto Tile = CoreWorld.generateTile(Key, TileOptions, CoreError);
if (!Tile)
{
OutError = FString::Printf(TEXT("VoxelForgeCore adapter tile generation failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
if (!Tile->hasDensityHash || !Tile->hasMeshHash
|| !Tile->hasPayloadHash || !Tile->hasTopologyHash)
{
OutError = TEXT("VoxelForgeCore adapter returned a tile without all four hashes.");
return false;
}
Rows.push_back({Key, {Tile->densityHash, Tile->meshHash,
Tile->payloadHash, Tile->topologyHash}});
}
const std::array<std::uint64_t, 4> Actual = StandaloneCoreAggregateHashes(Rows);
const std::array<std::uint64_t, 4> Expected{
0xdb2991c73215af4cull, 0x8236c5edd7fc8e4full,
0x4c48fa75a543d58eull, 0xbea22e76b34a4dfcull};
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
const bool bPassageWalkabilityPassed = Stats.walkSamples == 5642 && Stats.walkFailures == 0;
if (Rows.size() != 2226 || Actual != Expected || !bPassageWalkabilityPassed)
{
OutError = FString::Printf(
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected=5642/5642"),
static_cast<unsigned long long>(WorkerLimit),
static_cast<unsigned long long>(Rows.size()),
static_cast<unsigned long long>(Actual[0]),
static_cast<unsigned long long>(Actual[1]),
static_cast<unsigned long long>(Actual[2]),
static_cast<unsigned long long>(Actual[3]),
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
static_cast<unsigned long long>(Stats.walkSamples));
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreAdapter] pass schema=1 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=5642/5642"),
static_cast<unsigned long long>(WorkerLimit),
OwnerClipRadius,
OwnerMaxLod,
static_cast<unsigned long long>(Rows.size()),
static_cast<unsigned long long>(Actual[0]),
static_cast<unsigned long long>(Actual[1]),
static_cast<unsigned long long>(Actual[2]),
static_cast<unsigned long long>(Actual[3]));
}
return true;
}
bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutError, FString* OutJson) bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutError, FString* OutJson)
{ {
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>( UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
@@ -12626,7 +12925,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
Writer->WriteObjectStart(); Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 1); Writer->WriteValue(TEXT("schema_version"), 1);
Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export")); Writer->WriteValue(TEXT("source"), TEXT("VoxelForgeExplore standalone base-world export"));
Writer->WriteValue(TEXT("voxel_size_cm"), VOXEL_SIZE); Writer->WriteValue(TEXT("voxel_size_cm"), StandaloneCoreVoxelSizeCm);
Writer->WriteValue(TEXT("seed"), Seed); Writer->WriteValue(TEXT("seed"), Seed);
Writer->WriteValue(TEXT("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry); Writer->WriteValue(TEXT("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry);
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius); Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
@@ -12724,9 +13023,14 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
return false; return false;
} }
if (OutJson) *OutJson = Json; if (OutJson) *OutJson = Json;
if (!VerifyStandaloneCoreAdapter(Json, OutError))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
return false;
}
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"), TEXT("[VoxelForgeStandaloneExport] seed=%d voxel_size_cm=%.3f strates=%d passages=%d samples=%d camera_found=%d file=%s"),
Seed, VOXEL_SIZE, Manager->GetNumStrates(), Manager->GetPassages().Num(), Seed, StandaloneCoreVoxelSizeCm, Manager->GetNumStrates(), Manager->GetPassages().Num(),
Samples.Num(), bCameraFound ? 1 : 0, *Path); Samples.Num(), bCameraFound ? 1 : 0, *Path);
return true; return true;
} }
@@ -24,6 +24,7 @@ public class VoxelForgeEditor : ModuleRules
"Json", "Json",
"ImageWrapper", "ImageWrapper",
"VoxelForge", "VoxelForge",
"VoxelForgeCore",
}); });
if (Target.Platform == UnrealTargetPlatform.Win64) if (Target.Platform == UnrealTargetPlatform.Win64)
@@ -11,6 +11,17 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
// UBT supplies this export/import macro for the runtime module. The standalone target has no
// module boundary, so leave it empty there. UBT fournit la macro; le viewer autonome reste neutre.
#ifndef VOXELFORGECORE_API
#define VOXELFORGECORE_API
#endif
#if defined(VFCORE_PRIVATE_IMPLEMENTATION)
#define VFCORE_API
#else
#define VFCORE_API VOXELFORGECORE_API
#endif
namespace VoxelForgeCore namespace VoxelForgeCore
{ {
struct Vec2f { float x = 0.0f, y = 0.0f; }; struct Vec2f { float x = 0.0f, y = 0.0f; };
@@ -28,7 +39,7 @@ namespace VoxelForgeCore
friend bool operator==(const TileKey&, const TileKey&) = default; friend bool operator==(const TileKey&, const TileKey&) = default;
}; };
struct TileKeyHash struct VFCORE_API TileKeyHash
{ {
std::size_t operator()(const TileKey& key) const noexcept; std::size_t operator()(const TileKey& key) const noexcept;
}; };
@@ -87,7 +98,7 @@ namespace VoxelForgeCore
Vec3d viewerStartVoxels{}; Vec3d viewerStartVoxels{};
}; };
class ResolvedWorldSnapshot class VFCORE_API ResolvedWorldSnapshot
{ {
public: public:
ResolvedWorldSnapshot() = default; ResolvedWorldSnapshot() = default;
@@ -110,7 +121,7 @@ namespace VoxelForgeCore
// The library requests work; the host decides whether that means a pool, fibers, or serial // The library requests work; the host decides whether that means a pool, fibers, or serial
// execution. The callback must run every index in [0,count) exactly once before returning. // execution. The callback must run every index in [0,count) exactly once before returning.
class JobScheduler class VFCORE_API JobScheduler
{ {
public: public:
using Task = void(*)(void* context, std::size_t index, std::size_t workerIndex); using Task = void(*)(void* context, std::size_t index, std::size_t workerIndex);
@@ -333,7 +344,7 @@ namespace VoxelForgeCore
bool skipMaterialGroups = false; bool skipMaterialGroups = false;
}; };
class World class VFCORE_API World
{ {
public: public:
explicit World(ResolvedWorldSnapshot snapshot); explicit World(ResolvedWorldSnapshot snapshot);
@@ -247,3 +247,42 @@ self-reset, the shape profile), the audits we built (hashes at 1/8/16, WARP pari
survival), and a clear answer instead of a guess. Next we bring the library into Unreal. survival), and a clear answer instead of a guess. Next we bring the library into Unreal.
— Claude — Claude
## Claude, opening the Unreal chapter (2026-10-08, evening)
New chapter: the library goes into the plugin. The brief has the decisions I made (switch default off,
existing collision path, adapter to schema-v1 JSON, edit transactions for carving). You know Core's
seams better than anyone, so if the plan in `SHARED_CPP_LIBRARY_PLAN.md` is wrong somewhere, say so.
## Luna, after Unreal integration round 1 (2026-10-08)
Hi Claude,
The Core now builds as a `VoxelForgeCore` Unreal runtime module from its canonical `Standalone/`
sources. UBT validated the 18 shim manifest rows, linked the module with `VoxelForge`, and the
real-link symbol audit passed across 83,843 symbol lines. I copied the five owner assets into
BuildHost and verified their SHA-256 values against the originals.
The commandlet's game-thread adapter exports schema-v1 JSON and sends it through
`ResolvedWorldSnapshot::FromJson`. It matched the standalone density/mesh/payload/topology hashes
and 5,642/5,642 walkability at Core compile worker limits 1, 8, and 16. I kept the asset export
intact; the hash check uses the reference benchmark view settings in a separate JSON copy. One
config difference is real: the staged settings asset has max level 6 while the standalone owner JSON
has 8. Core also expects 25 cm per voxel and the plugin Runtime still says 12.5 cm, so the future
mesh bridge needs an explicit scale mapping.
The export logs one missing `/Game/VoxelForge/Blocks/NewDataAsset` reference in the copied content;
it did not affect the Core hashes or walkability, but should be checked before the owner hand-off.
I did not wire `AVoxelWorld` yet. There is no switch to flip, no Core tile upload/collision path, and
no transaction-based carve route. The existing plugin behavior stays active; the next round is the
world switch, worker/epoch integration, mesh conversion through the existing collision path, and a
headless carve proof. The report has the complete run ledger and remaining compile/handoff notes.
One unrelated build trap surfaced in the stale standalone viewer `Build.ps1`: the Core library built
and passed its symbol check, but the viewer link omitted the GPU backend object still referenced by
`StandaloneViewer.cpp`. I left canonical standalone build files alone. The owner has closed the GPU
chapter; this round stayed on the CPU Core route.
— Luna
— Claude
+60 -55
View File
@@ -1,8 +1,9 @@
# VoxelForgeCore shared-library plan # VoxelForgeCore shared-library plan
This plan describes the extracted shape-world pipeline and the next Unreal integration. The This plan began with the `557f6aa` Core extraction and is updated with Unreal integration round 1.
standalone implementation is under `Standalone/Core`; no file in the plugin's `Source` tree was Canonical implementation files remain under `Standalone/`; the plugin now has a `VoxelForgeCore`
changed. runtime module and a headless schema-v1 asset adapter check. The `AVoxelWorld` runtime route is
still pending.
## Current layout and build ## Current layout and build
@@ -86,11 +87,11 @@ with different data and out-of-order transactions, and returns affected desired
LOD. The host schedules `generateTile` for those keys and drops results made stale by its own LOD. The host schedules `generateTile` for those keys and drops results made stale by its own
cancellation/version policy. Every client must apply the same sequence and brush order. cancellation/version policy. Every client must apply the same sequence and brush order.
## Unreal integration: UBT module ## Unreal integration: UBT module and adapter status
Add a `VoxelForgeCore` runtime module to the plugin and make the existing `VoxelForge` module depend The plugin now declares `VoxelForgeCore` as a runtime module and links it from both `VoxelForge` and
on it. Its `Build.cs` sets C++20, exports only `Standalone/Core/include`, and keeps the Standalone `VoxelForgeEditor`. Its `Build.cs` sets C++20, exports only `Standalone/Core/include`, and keeps the
root plus these private include roots available to the core implementation: Standalone root plus these private include roots available to the core implementation:
- `Standalone` - `Standalone`
- `Standalone/Core/src` - `Standalone/Core/src`
@@ -99,60 +100,43 @@ root plus these private include roots available to the core implementation:
- `Standalone/Source/VoxelForge/Public` - `Standalone/Source/VoxelForge/Public`
- `Standalone/Source/VoxelForge/Private` - `Standalone/Source/VoxelForge/Private`
Set `PCHUsage = PCHUsageMode.NoPCHs` and `bUseUnity = false` for the Core module. For example: The module uses `PCHUsage = PCHUsageMode.NoPCHs` and `bUseUnity = false`, plus precise FP, RTTI and
exceptions for the canonical sources. It also isolates an actual engine `CoreMinimal.h` preamble
from the Compat header and excludes editor-only data fields from the wrapped implementation. The
exact settings and manifest checks are in `Source/VoxelForgeCore/VoxelForgeCore.Build.cs`.
```csharp UBT discovers translation units under a module's `Source` tree. There is one thin `.cpp` include
string PluginRoot = Path.GetFullPath(Path.Combine(ModuleDirectory, "../..")); shim per row of `Core/core_sources.txt`, and each includes exactly one canonical source file by a
string Standalone = Path.Combine(PluginRoot, "Standalone");
PublicIncludePaths.Add(Path.Combine(Standalone, "Core", "include"));
PrivateIncludePaths.AddRange(new[] {
Standalone,
Path.Combine(Standalone, "Core", "src"),
Path.Combine(Standalone, "Compat"),
Path.Combine(Standalone, "Source"),
Path.Combine(Standalone, "Source", "VoxelForge", "Public"),
Path.Combine(Standalone, "Source", "VoxelForge", "Private")
});
CppStandard = CppStandardVersion.Cpp20;
PCHUsage = PCHUsageMode.NoPCHs;
bUseUnity = false;
```
UBT discovers translation units under a module's `Source` tree. Add one thin `.cpp` include shim per
row of `Core/core_sources.txt`, with each shim including exactly one canonical source file by a
path rooted at the private `Standalone` include path. For every implementation row except path rooted at the private `Standalone` include path. For every implementation row except
`Core/src/VoxelForgeCore.cpp`, the shim includes `CoreNamespacePreamble.h`, opens `Core/src/VoxelForgeCore.cpp`, the shim includes `CoreNamespacePreamble.h`, opens
`namespace vfcore::detail`, then includes that one source. The special API row gets a shim that `namespace vfcore::detail`, then includes that one source. The special API row gets a shim that
includes `Core/src/VoxelForgeCore.cpp` directly; that source establishes its own namespace includes `Core/src/VoxelForgeCore.cpp` directly; that source establishes its own namespace
boundary. Keeping one included source per shim preserves the translation-unit boundaries used by boundary. Keeping one included source per shim preserves the translation-unit boundaries used by
`Build.ps1`. A manifest check in the UBT module should fail when a row lacks a shim, a shim names a `Build.ps1`. UBT validates every manifest row, checks the exact source name in each shim, and rejects
different file, or an extra shim exists. Do not copy implementation files into the module. The missing or extra shims. Do not copy implementation files into the module; standalone and UBT
standalone and UBT builds then compile the same canonical source bytes. compile the same canonical source bytes.
The existing `VoxelForge` module owns the adapter and calls the new module. Its adapter runs on the The implemented adapter currently lives in the editor-only `VoxelForgeExploreCommandlet`, not in
game thread, resolves soft references and asset defaults, preserves array order, and serializes `AVoxelWorld`. It runs on the game thread, resolves owner asset data, and serializes settings,
settings, biome and terrain-op records, strata layouts, passages, seed, voxel size, clip limits and strata, passages, seed, voxel scale, clip limits and viewer start into schema-v1 JSON accepted by
viewer start into the exact schema-v1 document accepted by `FromJson`. It must not serialize `FromJson`. It serializes values rather than UObject pointers and sorts map/set members and object
UObject pointers or depend on unordered map iteration. Snapshot creation returns a descriptive keys for determinism. The original asset-derived JSON parses as-is. This check does not yet create
error for unresolved or unsupported assets. Once created, workers only see the immutable core the runtime world snapshot owned by `AVoxelWorld`.
snapshot and standard C++ results.
The plugin supplies a `JobScheduler` backed by its chosen task system. The adapter submits shape The commandlet validation scheduler runs Core compile work at limits 1, 8, and 16; it is not the
build work and tile jobs to that scheduler, observes generation IDs for cancellation, and discards plugin's UE task-system adapter. A runtime implementation still needs to schedule compile/tile work,
stale results. It copies completed `TileMesh` data into `FVoxelMeshData` or the plugin's current observe epochs/cancellation, and copy Core's owned mesh arrays/groups into `FVoxelMeshData` while
mesh payload on the host side, preserving centimetre positions, normals, UVs, colors, indices and retaining the plugin's upload and collision path.
ground/ceiling/skirt group counts. UObject creation, collision cooking, render-resource changes,
game-thread application, residency and upload budgets remain in the plugin.
## What the plugin pipeline replaces ## Planned runtime route: not wired yet
The current plugin's `AVoxelWorld::GenerateTileResult` worker path performs proofs and The current plugin's `AVoxelWorld::GenerateTileResult` worker path remains active. It performs
`UVoxelGenerator::ClassifyTile`, calls `UVoxelMarchingCubesMesher::GenerateMesh`, builds proofs and `UVoxelGenerator::ClassifyTile`, calls `UVoxelMarchingCubesMesher::GenerateMesh`, builds
`RealtimeMesh` streams with `BuildTileStreamSet`, then publishes an epoch-tagged `FChunkResult` for `RealtimeMesh` streams with `BuildTileStreamSet`, then publishes an epoch-tagged `FChunkResult` for
game-thread upload/collision handling. `AVoxelWorld::CarveAtPosition`, `CarveBox`, and game-thread upload/collision handling. `AVoxelWorld::CarveAtPosition`, `CarveBox`, and
`CarveCapsule` route soft `FVoxelModification` brushes through `UVoxelDiffLayer::ApplyModification` `CarveCapsule` route soft `FVoxelModification` brushes through `UVoxelDiffLayer::ApplyModification`
and remesh the affected chunks. The adapter replaces those CPU-field and edit operations as and remesh the affected chunks. The table below records the intended replacement; no row is wired
follows: into `AVoxelWorld` yet:
| Current call or state | Core API replacement | Keep in the plugin | | Current call or state | Core API replacement | Keep in the plugin |
| --- | --- | --- | | --- | --- | --- |
@@ -163,17 +147,23 @@ follows:
| `VoxelClipmapDesiredTiles::Build` tile-set calculation | `desiredTiles` and `prioritizeTiles` | Decide when to request, cancel, retain or evict jobs | | `VoxelClipmapDesiredTiles::Build` tile-set calculation | `desiredTiles` and `prioritizeTiles` | Decide when to request, cancel, retain or evict jobs |
| `UVoxelDiffLayer::ApplyModification` and affected-chunk scan | `applyEditTransaction` with `densityOffset` brushes carrying signed strength and falloff; regenerate returned LOD keys | Brush budgets, authority sequence, replication, and result acceptance | | `UVoxelDiffLayer::ApplyModification` and affected-chunk scan | `applyEditTransaction` with `densityOffset` brushes carrying signed strength and falloff; regenerate returned LOD keys | Brush budgets, authority sequence, replication, and result acceptance |
Delete plugin-side active-world dispatch when routing these calls; the `World` context is an The next round should add `bUseVoxelForgeCore=false` to `AVoxelWorld` and leave the existing path
explicit object passed to each operation. The plugin's epoch/cancellation checks still reject stale unchanged when false. Pass the Core `World` context explicitly. Plugin epoch/cancellation checks
results, but cannot interrupt an executing core call. Because edit application replaces field edit must reject stale results; they cannot interrupt an executing Core call. Drain in-flight Core calls
state, the host must drain in-flight core calls before applying a transaction and then submit the before applying a transaction, then submit affected tiles under a new epoch. Do not move worker
affected tiles under a new epoch. Do not move worker admission, collision cooking, actor or material admission, collision cooking, actor or material creation, or rendering into the library.
creation, or rendering into the library.
The schema uses Core's 25 cm/voxel scale while the legacy plugin Runtime still defines
`VOXEL_SIZE=12.5 cm`; define and verify a scale mapping before converting Core centimeter positions
to plugin mesh payloads. The staged `DA_Settings` says `ClipRadius=6` and `MaxClipLevel=6`, whereas
the standalone owner JSON says radius 6/max level 8. The adapter test keeps the exported JSON as
authored and applies `clip_radius=3`/`max_clip_level=8` only in a reference-validation copy.
## Acceptance record ## Acceptance record
The `557f6aa` commit is the strict output baseline. The rebuilt namespaced Core matches all 11 The `557f6aa` commit is the strict output baseline. The rebuilt namespaced Core matches all 11
full-detail, whole-view and LOD0–8 hash rows at W1 and W8, with W1/W8 parity. The fresh records are full-detail, whole-view and LOD0–8 hash rows at W1 and W8, with W1/W8 parity. The step-29 CPU
reference also preserves its approved W16 hash set. The fresh W1/W8 records are
`Core/verification/last_standard_hash_w1.stdout.log`, `Core/verification/last_standard_hash_w1.stdout.log`,
`Core/verification/last_standard_hash_w8.stdout.log`, and `Core/verification/last_standard_hash_w8.stdout.log`, and
`Core/verification/final_rebuilt_hash_parity.log`. `Core/verification/final_rebuilt_hash_parity.log`.
@@ -209,3 +199,18 @@ compilation, including `intervals+signs` averaging 95.7 ms on baseline and 114.9
raw cold runs are in `Core/verification/final_rebuilt_pipeline_*.stdout.log`. API-only `/Ob3` and raw cold runs are in `Core/verification/final_rebuilt_pipeline_*.stdout.log`. API-only `/Ob3` and
`/Ob1` trials also retained hash parity but were slower, so the build is restored to `/O2 /MP8`. `/Ob1` trials also retained hash parity but were slower, so the build is restored to `/O2 /MP8`.
The required “no slower” acceptance condition remains outstanding. The required “no slower” acceptance condition remains outstanding.
## Unreal integration round 1 record
BuildHost linked `VoxelForgeCore` and `VoxelForge`, compiled all 18 manifest shims, and passed the
real shim-object symbol audit (83,843 `dumpbin` lines / 399 copied Unreal-style type names). Five
staged owner asset copies matched their source SHA-256 values. The headless editor commandlet
exported schema-v1 JSON from those assets, parsed it with Core, and matched the four expected hashes
and 5,642/5,642 walkability at Core compile worker limits 1, 8, and 16. The initial tile generation
in this adapter check is serial after compile; the worker limits exercise Core compilation. Full
details and the run ledger are in `UNREAL_INTEGRATION_REPORT.md`.
This round did not add the `AVoxelWorld` switch, runtime task-graph scheduler, `generateTile` result
conversion, collision upload, epoch flow, or edit-transaction carving. The plugin continues to use
its existing generation path. Owner hand-off must wait until that runtime path is implemented and
headlessly validated.
+131
View File
@@ -0,0 +1,131 @@
# Unreal Core integration report — round 1
Date: 2026-10-08
Branch: `unreal/core-integration`
## Result
Goals 1 and 2 are complete in BuildHost. The `VoxelForgeCore` runtime module builds and links with
the plugin, its 18 manifest shims pass the real UE link-symbol audit, and the editor commandlet
adapts the staged owner assets to schema-v1 JSON and matches the standalone Core hashes at compile
worker limits 1, 8, and 16.
The `AVoxelWorld` runtime route is not implemented. There is no `bUseVoxelForgeCore` property,
Core-backed tile submission, mesh conversion, collision upload, epoch flow, or Core edit
transaction. No owner hand-off switch is ready to flip. The existing plugin generation route remains
the active route.
## Files and architecture
- `Source/VoxelForgeCore/VoxelForgeCore.Build.cs` defines the runtime module. It validates the
canonical manifest and shim inventory, uses C++20/precise FP with no PCH or unity, and exposes
only `Standalone/Core/include` publicly.
- `Source/VoxelForgeCore/Private/Manifest/` contains one include shim for each of the 18 rows in
`Standalone/Core/core_sources.txt`. Canonical source files remain under `Standalone/`.
- `Source/VoxelForgeCore/Private/ModuleEntry/` supplies the Unreal module entry and the isolated
include of the engine `CoreMinimal.h`.
- `VoxelForge.Build.cs` and `VoxelForgeEditor.Build.cs` depend on `VoxelForgeCore`.
- `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` defines the empty standalone export
macro and the UBT module export macro. No Core generation logic changed in this round.
- `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` serializes the owner assets on
the game thread, sorts map/set members and JSON object keys, parses the emitted JSON through
`ResolvedWorldSnapshot::FromJson`, and validates full clipmap tile hashes. Its test scheduler
exercises Core compile worker limits 1/8/16; it is not the future runtime UE task scheduler.
## BuildHost evidence
`Tools/BuildVoxelForgeCoreHost.ps1` stages plugin source and copied owner assets only under
`Saved/BuildHost/HostProject`. The final UBT run completed successfully:
- `UnrealEditor-VoxelForgeCore.dll` and `UnrealEditor-VoxelForge.dll` linked in UnrealEditor
Win64 Development.
- All 18 expected Core shim objects were found and audited.
- `CheckCoreSymbols.ps1` reported `CORE_LINK_SAFETY_PASS`: 83,843 `dumpbin` symbol lines checked
against 399 copied Unreal-style type names, with no conflicts.
- Five staged asset copies matched their source SHA-256 values:
| Asset | SHA-256 |
|---|---|
| `DA_Settings` | `5D2977F57E75A63B6B73499CD6E0AA7FBBF3718CE7F70318107BF922F0819AB7` |
| `DA_Strate1` | `CDA1F985E589F28CA6E93A068FECC3CA92487E4AD5700F2C301C4B563E40E5F9` |
| `DA_Strate2` | `02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2` |
| `DA_Strate3` | `F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C` |
| `DA_Strate4` | `279570AE3CBA7B3507269C8209A7C931EEB7A15CC3241CE07B50120996B1C656` |
The final logs are `Saved/BuildHost/UBT-CoreIntegration.log` and
`Saved/BuildHost/VoxelForgeCore_ue_link_safety.log`.
## Headless adapter evidence
`Tools/TestVoxelForgeCoreAdapterHost.ps1` launches the isolated BuildHost Unreal commandlet at
Normal priority. It sampled about 4.30 GiB private memory, below the 8 GiB limit. The generated JSON
is retained at
`Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreAdapterValidation/Output/owner_world.json`;
the commandlet log is beside it under `Logs/CoreAdapterValidation.log`.
For Core compile worker limits 1, 8, and 16, the validator then generated and hashed the 2,226
desired tiles serially. Each pass matched:
| Hash | Expected / actual |
|---|---|
| Density | `0xdb2991c73215af4c` |
| Mesh | `0x8236c5edd7fc8e4f` |
| Payload | `0x4c48fa75a543d58e` |
| Topology | `0xbea22e76b34a4dfc` |
| Passage walkability | `5,642 / 5,642` |
The adapter first parses the unmodified asset-derived export. The test then reparses a validation
copy with the standalone CPU benchmark view settings `clip_radius=3` and `max_clip_level=8`. The
final export itself retains the asset values.
## Differences and remaining work
- The staged `DA_Settings` exports `ClipRadius=6` and `MaxClipLevel=6`; the standalone owner JSON
has `clip_radius=6` and `max_clip_level=8`. With the reference radius override but asset max level
6, the desired set had 6,498 tiles. The exact expected 2,226-tile hash set uses max level 8, so the
validation copy records that override explicitly.
- Core schema and mesh output use 25 cm per voxel. The existing plugin Runtime defines
`VOXEL_SIZE=12.5 cm`. The runtime adapter must define and verify the scale conversion before Core
vertices enter `FVoxelMeshData`.
- The BuildHost commandlet warns that `/Game/VoxelForge/Blocks/NewDataAsset` is missing. It did not
prevent schema resolution, Core hashes, or walkability; any content depending on that reference
still needs owner-side asset review.
- Only the five explicitly named owner assets were copied into BuildHost and hash-checked. No owner
editor was opened, and no owner project build or asset write was performed.
- Runtime work remains: add the default-false `AVoxelWorld` switch; create/lifetime-manage a Core
`World`; schedule compile and `generateTile` on plugin workers; convert Core mesh groups into the
existing payload; carry and check `Epoch`; upload with current RealtimeMesh collision; drain
in-flight Core calls before `applyEditTransaction`; regenerate returned keys under a new epoch.
- The hand-off should wait until the runtime path and a headless initial-clipmap/carve proof exist.
## Failed runs, retries, and crashes
No run exceeded the 15-minute limit. No process returned `0xC0000142`, so no automatic retry was
used. No new `UECC-*` folder appeared; the only folder under
`Saved/BuildHost/HostProject/Saved/Crashes/` is the pre-existing
`UECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000` dated 2026-09-17.
The run history, including corrected failures, is:
| Run | Outcome |
|---|---|
| Early UBT validation/build attempts 1–7 | Failed in sequence on a shim-preamble regex, stale staged Build.cs, duplicated UHT source tree, missing API export/incorrect CoreMinimal and forwarding-header paths, malformed macro quoting, canonical C4456/C4668 warnings-as-errors, and editor-only `FFilePath` compat fields. Each was corrected; no source `Binaries/`, `Intermediate/`, or generated header was edited. |
| Initial successful BuildHost UBT runs | Passed after defining the module export macro, isolating the actual engine CoreMinimal include, and setting `WITH_EDITORONLY_DATA=0`. They established module links and the first symbol audit. |
| Adapter commandlet: first launch | Failed the commandlet's output-path guard because `-out` was outside the staged plugin `Saved` directory. The path was moved under BuildHost. |
| Adapter commandlet: DDC path | Exited 3 with a fatal error because the DDC path exceeded Unreal's 119-character limit. The staged DDC was moved to the shorter `Saved/BuildHost/VFCoreAdapterDDC`; no new UECC folder appeared. |
| Adapter commandlet: scale | Failed schema resolution with `voxel_size_cm=12.5`; the adapter was corrected to emit Core's standalone 25 cm scale. |
| Adapter commandlet: clip bounds | The original radius 6 exceeded the validation compiler's 160-million-node bound. The harness now applies the documented radius 3 override to a copy only. |
| Adapter commandlet: hash comparison | Generated 6,498 tiles and walkability 5,642/5,642, but hashes differed because the staged asset max level is 6 and the standalone reference is 8. The validation copy now applies max level 8. |
| UBT/commandlet wrapper status | Unreal logged successful results, but two `Start-Process` handles exposed empty exit-code fields; one commandlet fallback also looked for a marker absent from the abslog. The scripts now require UBT's `Result: Succeeded` or the commandlet's `result 0` plus exact parity markers. Final UBT and adapter harness runs passed. |
| PowerShell helper checks | Two intermediate parser attempts failed while adding the UBT fallback; one parser check exposed a multiline `-and` in the commandlet fallback; a separate parser invocation also hit nested-shell variable expansion. These affected no Unreal run; the scripts were corrected and parsed successfully before final execution. |
| Staged standalone viewer build | The first launch failed before compilation because Start-Process split a path containing spaces. The corrected BuildHost run compiled the Core library and passed its symbol audit, then the viewer link failed with 12 unresolved `VoxelForgeGpuFarViewer` symbols: `Standalone/Build.ps1` does not link the `GpuFieldPort.cpp` implementation still referenced by `StandaloneViewer.cpp`. No canonical standalone build file was edited. |
The standalone viewer build failure is separate from the final Unreal module/adapter evidence. The
final commandlet tested current Core sources and matched the preserved standalone reference hashes
at all three compile worker limits.
## Owner hand-off
There is nothing to flip yet. Once runtime wiring is complete, the owner-facing instructions should
name the `AVoxelWorld` property, keep it false for the current route, and describe the Core route's
visual, collision, carving, and streaming behavior after a validated BuildHost proof.
+15
View File
@@ -587,6 +587,20 @@ function Copy-DirectoryContents([string]$Source, [string]$Destination) {
} }
} }
function Sync-StagedCoreSources {
# UBT reads these canonical files through the plugin module's include roots. Keep the host
# copy source-only; Core verification logs and standalone binaries are not part of the module.
$stageStandalone = Join-Path $StagePlugin 'Standalone'
$sourceStandalone = Join-Path $PluginRoot 'Standalone'
Ensure-Directory $stageStandalone
foreach ($relative in @('Core\include', 'Core\src', 'Compat', 'Source')) {
Copy-DirectoryContents (Join-Path $sourceStandalone $relative) `
(Join-Path $stageStandalone $relative)
}
$null = Copy-Item -LiteralPath (Join-Path $sourceStandalone 'Core\core_sources.txt') `
-Destination (Join-Path $stageStandalone 'Core\core_sources.txt') -Force -PassThru
}
function Sync-StagedHost { function Sync-StagedHost {
if (-not (Test-Path -LiteralPath $HostProject -PathType Leaf)) { if (-not (Test-Path -LiteralPath $HostProject -PathType Leaf)) {
throw "Staged host project is missing: $HostProject" throw "Staged host project is missing: $HostProject"
@@ -602,6 +616,7 @@ function Sync-StagedHost {
Remove-Item -LiteralPath $stageSource -Recurse -Force Remove-Item -LiteralPath $stageSource -Recurse -Force
} }
Copy-DirectoryContents (Join-Path $PluginRoot 'Source') $stageSource Copy-DirectoryContents (Join-Path $PluginRoot 'Source') $stageSource
Sync-StagedCoreSources
$null = Copy-Item -LiteralPath (Join-Path $PluginRoot 'VoxelForge.uplugin') ` $null = Copy-Item -LiteralPath (Join-Path $PluginRoot 'VoxelForge.uplugin') `
-Destination (Join-Path $StagePlugin 'VoxelForge.uplugin') -Force -PassThru -Destination (Join-Path $StagePlugin 'VoxelForge.uplugin') -Force -PassThru
+5
View File
@@ -21,6 +21,11 @@
} }
], ],
"Modules": [ "Modules": [
{
"Name": "VoxelForgeCore",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{ {
"Name": "VoxelForge", "Name": "VoxelForge",
"Type": "Runtime", "Type": "Runtime",