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:
1 parent
a1dfc0422e
commit
29ca99af7f
32 files changed
+919
-69
No files matched your search
@@ -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
|
||||
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)
|
||||
|
||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||
|
||||
+30
-4
@@ -5,7 +5,8 @@
|
||||
> 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.
|
||||
> 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
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. |
|
||||
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. |
|
||||
| `../../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. |
|
||||
| `../../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.
|
||||
| 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`; 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. |
|
||||
| `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. |
|
||||
@@ -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`). |
|
||||
| `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.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||
|
||||
@@ -79,6 +79,9 @@ public class VoxelForge : ModuleRules
|
||||
// Private dependencies - only used in our .cpp files, not exposed in headers
|
||||
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
|
||||
"ImageWrapper", // PNG encode for the biome-map preview bake (BakeBiomePreview)
|
||||
"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"
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+7
@@ -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 "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelTypes.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -61,6 +69,9 @@ constexpr int32 DefaultRenderWidth = 512;
|
||||
constexpr int32 DefaultRenderHeight = 288;
|
||||
constexpr float DefaultRenderStepVoxels = 0.5f; // 6.25 cm, as at 25 cm/voxel.
|
||||
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 DefaultExportStep = 1;
|
||||
constexpr int32 DefaultMaxWalkCells = 32000000;
|
||||
@@ -12268,20 +12279,28 @@ TSharedPtr<FJsonValue> StandaloneSerializeProperty(
|
||||
if (const FSetProperty* Set = CastField<FSetProperty>(Property))
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
TArray<TPair<FString, TSharedPtr<FJsonValue>>> OrderedValues;
|
||||
FScriptSetHelper Helper(Set, Data);
|
||||
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++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));
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
if (const FMapProperty* Map = CastField<FMapProperty>(Property))
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> Values;
|
||||
TArray<TPair<FString, TSharedPtr<FJsonValue>>> OrderedValues;
|
||||
FScriptMapHelper Helper(Map, Data);
|
||||
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
|
||||
{
|
||||
@@ -12291,8 +12310,14 @@ TSharedPtr<FJsonValue> StandaloneSerializeProperty(
|
||||
Map->KeyProp, Helper.GetKeyPtr(Index), ActiveObjects));
|
||||
Pair->SetField(TEXT("value"), StandaloneSerializeProperty(
|
||||
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));
|
||||
}
|
||||
if (const FStructProperty* Struct = CastField<FStructProperty>(Property))
|
||||
@@ -12368,9 +12393,16 @@ void StandaloneWriteJsonValue(
|
||||
else if (Value->Type == EJson::Object)
|
||||
{
|
||||
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();
|
||||
}
|
||||
@@ -12465,6 +12497,273 @@ struct FStandaloneExportSample
|
||||
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)
|
||||
{
|
||||
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
|
||||
@@ -12626,7 +12925,7 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("schema_version"), 1);
|
||||
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("open_surface_entry"), AuthoredSettings->bOpenSurfaceEntry);
|
||||
Writer->WriteValue(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||
@@ -12724,9 +13023,14 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
return false;
|
||||
}
|
||||
if (OutJson) *OutJson = Json;
|
||||
if (!VerifyStandaloneCoreAdapter(Json, OutError))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreAdapter] %s"), *OutError);
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public class VoxelForgeEditor : ModuleRules
|
||||
"Json",
|
||||
"ImageWrapper",
|
||||
"VoxelForge",
|
||||
"VoxelForgeCore",
|
||||
});
|
||||
|
||||
if (Target.Platform == UnrealTargetPlatform.Win64)
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
#include <utility>
|
||||
#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
|
||||
{
|
||||
struct Vec2f { float x = 0.0f, y = 0.0f; };
|
||||
@@ -28,7 +39,7 @@ namespace VoxelForgeCore
|
||||
friend bool operator==(const TileKey&, const TileKey&) = default;
|
||||
};
|
||||
|
||||
struct TileKeyHash
|
||||
struct VFCORE_API TileKeyHash
|
||||
{
|
||||
std::size_t operator()(const TileKey& key) const noexcept;
|
||||
};
|
||||
@@ -87,7 +98,7 @@ namespace VoxelForgeCore
|
||||
Vec3d viewerStartVoxels{};
|
||||
};
|
||||
|
||||
class ResolvedWorldSnapshot
|
||||
class VFCORE_API ResolvedWorldSnapshot
|
||||
{
|
||||
public:
|
||||
ResolvedWorldSnapshot() = default;
|
||||
@@ -110,7 +121,7 @@ namespace VoxelForgeCore
|
||||
|
||||
// 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.
|
||||
class JobScheduler
|
||||
class VFCORE_API JobScheduler
|
||||
{
|
||||
public:
|
||||
using Task = void(*)(void* context, std::size_t index, std::size_t workerIndex);
|
||||
@@ -333,7 +344,7 @@ namespace VoxelForgeCore
|
||||
bool skipMaterialGroups = false;
|
||||
};
|
||||
|
||||
class World
|
||||
class VFCORE_API World
|
||||
{
|
||||
public:
|
||||
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.
|
||||
|
||||
— 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
|
||||
@@ -1,8 +1,9 @@
|
||||
# VoxelForgeCore shared-library plan
|
||||
|
||||
This plan describes the extracted shape-world pipeline and the next Unreal integration. The
|
||||
standalone implementation is under `Standalone/Core`; no file in the plugin's `Source` tree was
|
||||
changed.
|
||||
This plan began with the `557f6aa` Core extraction and is updated with Unreal integration round 1.
|
||||
Canonical implementation files remain under `Standalone/`; the plugin now has a `VoxelForgeCore`
|
||||
runtime module and a headless schema-v1 asset adapter check. The `AVoxelWorld` runtime route is
|
||||
still pending.
|
||||
|
||||
## 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
|
||||
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
|
||||
on it. Its `Build.cs` sets C++20, exports only `Standalone/Core/include`, and keeps the Standalone
|
||||
root plus these private include roots available to the core implementation:
|
||||
The plugin now declares `VoxelForgeCore` as a runtime module and links it from both `VoxelForge` and
|
||||
`VoxelForgeEditor`. Its `Build.cs` sets C++20, exports only `Standalone/Core/include`, and keeps the
|
||||
Standalone root plus these private include roots available to the core implementation:
|
||||
|
||||
- `Standalone`
|
||||
- `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/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
|
||||
string PluginRoot = Path.GetFullPath(Path.Combine(ModuleDirectory, "../.."));
|
||||
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
|
||||
UBT discovers translation units under a module's `Source` tree. There is one thin `.cpp` include
|
||||
shim per row of `Core/core_sources.txt`, and each includes exactly one canonical source file by a
|
||||
path rooted at the private `Standalone` include path. For every implementation row except
|
||||
`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
|
||||
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
|
||||
`Build.ps1`. A manifest check in the UBT module should fail when a row lacks a shim, a shim names a
|
||||
different file, or an extra shim exists. Do not copy implementation files into the module. The
|
||||
standalone and UBT builds then compile the same canonical source bytes.
|
||||
`Build.ps1`. UBT validates every manifest row, checks the exact source name in each shim, and rejects
|
||||
missing or extra shims. Do not copy implementation files into the module; standalone and UBT
|
||||
compile the same canonical source bytes.
|
||||
|
||||
The existing `VoxelForge` module owns the adapter and calls the new module. Its adapter runs on the
|
||||
game thread, resolves soft references and asset defaults, preserves array order, and serializes
|
||||
settings, biome and terrain-op records, strata layouts, passages, seed, voxel size, clip limits and
|
||||
viewer start into the exact schema-v1 document accepted by `FromJson`. It must not serialize
|
||||
UObject pointers or depend on unordered map iteration. Snapshot creation returns a descriptive
|
||||
error for unresolved or unsupported assets. Once created, workers only see the immutable core
|
||||
snapshot and standard C++ results.
|
||||
The implemented adapter currently lives in the editor-only `VoxelForgeExploreCommandlet`, not in
|
||||
`AVoxelWorld`. It runs on the game thread, resolves owner asset data, and serializes settings,
|
||||
strata, passages, seed, voxel scale, clip limits and viewer start into schema-v1 JSON accepted by
|
||||
`FromJson`. It serializes values rather than UObject pointers and sorts map/set members and object
|
||||
keys for determinism. The original asset-derived JSON parses as-is. This check does not yet create
|
||||
the runtime world snapshot owned by `AVoxelWorld`.
|
||||
|
||||
The plugin supplies a `JobScheduler` backed by its chosen task system. The adapter submits shape
|
||||
build work and tile jobs to that scheduler, observes generation IDs for cancellation, and discards
|
||||
stale results. It copies completed `TileMesh` data into `FVoxelMeshData` or the plugin's current
|
||||
mesh payload on the host side, preserving centimetre positions, normals, UVs, colors, indices and
|
||||
ground/ceiling/skirt group counts. UObject creation, collision cooking, render-resource changes,
|
||||
game-thread application, residency and upload budgets remain in the plugin.
|
||||
The commandlet validation scheduler runs Core compile work at limits 1, 8, and 16; it is not the
|
||||
plugin's UE task-system adapter. A runtime implementation still needs to schedule compile/tile work,
|
||||
observe epochs/cancellation, and copy Core's owned mesh arrays/groups into `FVoxelMeshData` while
|
||||
retaining the plugin's upload and collision path.
|
||||
|
||||
## What the plugin pipeline replaces
|
||||
## Planned runtime route: not wired yet
|
||||
|
||||
The current plugin's `AVoxelWorld::GenerateTileResult` worker path performs proofs and
|
||||
`UVoxelGenerator::ClassifyTile`, calls `UVoxelMarchingCubesMesher::GenerateMesh`, builds
|
||||
The current plugin's `AVoxelWorld::GenerateTileResult` worker path remains active. It performs
|
||||
proofs and `UVoxelGenerator::ClassifyTile`, calls `UVoxelMarchingCubesMesher::GenerateMesh`, builds
|
||||
`RealtimeMesh` streams with `BuildTileStreamSet`, then publishes an epoch-tagged `FChunkResult` for
|
||||
game-thread upload/collision handling. `AVoxelWorld::CarveAtPosition`, `CarveBox`, and
|
||||
`CarveCapsule` route soft `FVoxelModification` brushes through `UVoxelDiffLayer::ApplyModification`
|
||||
and remesh the affected chunks. The adapter replaces those CPU-field and edit operations as
|
||||
follows:
|
||||
and remesh the affected chunks. The table below records the intended replacement; no row is wired
|
||||
into `AVoxelWorld` yet:
|
||||
|
||||
| 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 |
|
||||
| `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
|
||||
explicit object passed to each operation. The plugin's epoch/cancellation checks still reject stale
|
||||
results, but cannot interrupt an executing core call. Because edit application replaces field edit
|
||||
state, the host must drain in-flight core calls before applying a transaction and then submit the
|
||||
affected tiles under a new epoch. Do not move worker admission, collision cooking, actor or material
|
||||
creation, or rendering into the library.
|
||||
The next round should add `bUseVoxelForgeCore=false` to `AVoxelWorld` and leave the existing path
|
||||
unchanged when false. Pass the Core `World` context explicitly. Plugin epoch/cancellation checks
|
||||
must reject stale results; they cannot interrupt an executing Core call. Drain in-flight Core calls
|
||||
before applying a transaction, then submit affected tiles under a new epoch. Do not move worker
|
||||
admission, collision cooking, actor or material 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
|
||||
|
||||
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_w8.stdout.log`, and
|
||||
`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
|
||||
`/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.
|
||||
|
||||
## 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.
|
||||
@@ -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.
|
||||
@@ -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 {
|
||||
if (-not (Test-Path -LiteralPath $HostProject -PathType Leaf)) {
|
||||
throw "Staged host project is missing: $HostProject"
|
||||
@@ -602,6 +616,7 @@ function Sync-StagedHost {
|
||||
Remove-Item -LiteralPath $stageSource -Recurse -Force
|
||||
}
|
||||
Copy-DirectoryContents (Join-Path $PluginRoot 'Source') $stageSource
|
||||
Sync-StagedCoreSources
|
||||
$null = Copy-Item -LiteralPath (Join-Path $PluginRoot 'VoxelForge.uplugin') `
|
||||
-Destination (Join-Path $StagePlugin 'VoxelForge.uplugin') -Force -PassThru
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
}
|
||||
],
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "VoxelForgeCore",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default"
|
||||
},
|
||||
{
|
||||
"Name": "VoxelForge",
|
||||
"Type": "Runtime",
|
||||
|
||||
Reference in new issue
Block a user