wip(unreal): AVoxelWorld Core mode -- streaming, collision and carving through VoxelForgeCore

Luna, integration round 2 (BuildHost only): AVoxelWorld::bUseVoxelForgeCore (default
false) routes desired tiles, generation, upload/collision and carving through Core.
Core-only CoreClipRadius=3 / CoreMaxClipLevel=8 (owner's asset untouched); 25 cm Core
voxels in every cm<->voxel conversion; legacy brush sizes scaled to keep physical size;
UE-task JobScheduler, epoch-checked Mpsc results, existing RMC upload + collision cook;
carving via sequenced applyEditTransaction. Switch off: BuildDesiredTiles takes the old
path unchanged.
BuildHost proof (actor commandlet, staged copies of the owner's assets): all 2,226
initial tiles hash-match standalone Core; collision bodies as expected; anchor move
requests 1 / releases 1 tile; carved tile hash matches standalone; peak ~2.68 GiB.
Legacy CrossPlatformDigest automation: 115,000 samples pass (wrapper flagged an
analytics write outside isolation).

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 20:33:46 +02:00
1 parent 29ca99af7f
commit 075e58187e
7 files changed
+2069 -118

No files matched your search

+30 -33
View File
@@ -1065,42 +1065,39 @@ 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.
### 8.17 Standalone Core in Unreal — integration round 2
**Status (2026-10-08):** `AVoxelWorld::bUseVoxelForgeCore` now selects a Core runtime route and is
false by default. Core mode has independent `CoreClipRadius=3` and `CoreMaxClipLevel=8` properties;
it does not read or write the older clip properties or owner assets. BuildHost UBT and the Core link
symbol audit pass. A headless BuildHost actor proof matches all initial tile hashes against standalone
Core, checks collision readiness against the expected non-empty level-0 tiles, moves a streaming
anchor, and applies a transaction edit. The current-plugin switch-off baseline is the narrow
`VoxelForge.Determinism.CrossPlatformDigest`; it passes but does not spawn an actor with the switch
false, so it is a legacy-generator baseline rather than an actor-level A/B proof.
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 game-thread asset adapter emits the immutable schema-v1 input and resolves it through
`ResolvedWorldSnapshot::FromJson`; Core workers receive no UObjects. The Core route asks
`World::desiredTiles` and `prioritizeTiles`, compiles and generates through Unreal tasks, and carries
the generation `Epoch` through MPSC results. The shared payload builder, RealtimeMesh upload, and
collision cook remain the final path. Workers observe `bShuttingDown`, and `EndPlay` drains active
tasks. Preserve §8.10 streaming, cancellation, epoch, collision-handoff, and per-frame-budget rules.
The intended side-by-side runtime route is:
Core `Vec3d` positions and tile keys are in voxel space, one voxel is 25 cm, and Core mesh vertex
positions are already in centimetres. The bridge copies those mesh positions directly; player,
anchor, query, and edit inputs convert world centimetres using the Core scale. High-level brushes
that historically express dimensions in the plugin's 12.5 cm voxels scale radius, extent, and falloff
by 12.5/25 to preserve physical dimensions. The low-level `ApplyModification` path expects active
voxel units. Tile keys remain Core keys; only legacy material lookup maps Core tile spans into the
existing strate chunk layout.
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.
Carving drains in-flight Core calls, applies an increasing-sequence `applyEditTransaction`, advances
the epoch, and regenerates the returned affected keys. This leaves a transaction boundary for later
replication; no transport is implemented. Existing streaming anchors are included in Core desired
tiles; the BuildHost shift check covers `CollisionOnly`. `FullVisual` is wired through the same
anchor reconciliation but has not had an independent headless assertion. The owner-editor visual
and physical play check remains the hand-off step. See
[`Standalone/UNREAL_INTEGRATION_REPORT.md`](Standalone/UNREAL_INTEGRATION_REPORT.md) for evidence,
known limits, and instructions.
## 9. Multiplayer model (listen-server first, dedicated-friendly)
+20 -13
View File
@@ -820,26 +820,33 @@ 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)
### 3.13 Core runtime integration (round 2)
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.
headless evidence and the run ledger.
`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.
thread to schema-v1 JSON. The adapter parses it through `ResolvedWorldSnapshot::FromJson` and keeps
UObjects off Core workers. The runtime switch is `AVoxelWorld::bUseVoxelForgeCore` (default false),
with independent `CoreClipRadius=3` and `CoreMaxClipLevel=8`; it does not read or mutate the old clip
settings or assets.
**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.
When enabled, `AVoxelWorld` uses Core desired keys and priorities, compiles/generates through Unreal
tasks, carries `Epoch` through the existing MPSC result queues, and uploads converted payloads through
RealtimeMesh and its collision cook. Core voxel coordinates use 25 cm per voxel; Core tile keys are
retained, and Core mesh vertices are already in centimetres. High-level legacy brush dimensions are
scaled from 12.5 cm voxel units to Core units so their physical size stays the same. Edits use
sequenced `applyEditTransaction` calls after in-flight Core work drains, then regenerate Core's
affected keys under a new epoch. Existing streaming anchors participate; the headless move proof
covers `CollisionOnly` policy.
`-coreworldtest=1` in the BuildHost editor commandlet validates the spawned actor's 2,226 initial
tiles against standalone Core, expected collision bodies/empty tiles, an anchor move, and a carve
transaction. `VoxelForge.Determinism.CrossPlatformDigest` is the narrow switch-off baseline; it passes,
but does not instantiate an actor with the switch false. See the report for that limitation and for
owner-editor instructions.
## 4. The density pipeline (most-edited hot path)
File diff suppressed because it is too large. Load diff
+74 -8
View File
@@ -34,6 +34,18 @@ class UMaterialInstanceDynamic;
class FScopedGenerationPause;
class ACameraActor;
namespace RealtimeMesh { struct FRealtimeMeshStreamSet; } // T1.f — worker-built geometry buffers
class FVoxelForgeCoreRuntimeState;
struct FVoxelForgeCoreTileHashRecord
{
uint64 Density = 0;
uint64 Mesh = 0;
uint64 Payload = 0;
uint64 Topology = 0;
int32 Triangles = 0;
bool bEmpty = true;
bool bHasHashes = false;
};
/** Per-request cancellation state. The game thread flips the flag when a pending tile leaves the
* desired set; workers only read it and return an aborted result through the normal MPSC queue. */
@@ -141,6 +153,11 @@ struct FChunkResult
// section for a non-empty polygroup — configuring a missing one is invalid).
bool bHasGroundTris = false;
bool bHasCeilingTris = false;
uint64 CoreDensityHash = 0;
uint64 CoreMeshHash = 0;
uint64 CorePayloadHash = 0;
uint64 CoreTopologyHash = 0;
bool bHasCoreHashes = false;
// STRATE CONTENT CUT — the chunk-Z band this tile was MESHED with (MIN/MAX = uncut). The
// apply path clamps its strate/material lookups into it (a coarse tile's raw min corner can
// sit in a strate whose content was cut out of the mesh entirely).
@@ -248,6 +265,25 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World")
bool bManageAtmosphere = true;
/** Route terrain generation, streaming and edits through Standalone Core (opt-in). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (DisplayName = "Use VoxelForge Core"))
bool bUseVoxelForgeCore = false;
/** Core-only clipmap radius, in level-0 Core tiles. Does not read Settings->ClipRadius. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (ClampMin = "1", ClampMax = "64"))
int32 CoreClipRadius = 3;
/** Core-only maximum clip level. Does not read Settings->MaxClipLevel. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (ClampMin = "0", ClampMax = "8"))
int32 CoreMaxClipLevel = 8;
/** Commandlet-only identity capture; normal Core streaming leaves Core's hash path disabled. */
bool bCaptureVoxelForgeCoreHashesForValidation = false;
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> VoxelForgeCoreTileHashes;
uint64 LastVoxelForgeCoreEditDigest = 0;
uint64 LastVoxelForgeCoreEditSequence = 0;
TArray<FVoxelTileKey> LastVoxelForgeCoreAffectedTiles;
//=========================================================================
// CHUNK STORAGE
//=========================================================================
@@ -388,7 +424,8 @@ public:
* Applies a spherical brush that subtracts density.
*
* @param Position - World-space center of the carve brush
* @param Radius - Brush radius in voxels (default 6 voxels / 0.75 m at 12.5 cm per voxel)
* @param Radius - Brush radius in legacy Runtime voxels (12.5 cm); Core converts it to
* 25 cm voxels to preserve the physical brush size.
* @param Strength - How aggressively to carve (default 10, higher = deeper)
*/
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
@@ -399,7 +436,8 @@ public:
* Applies a spherical brush that adds density.
*
* @param Position - World-space center of the fill brush
* @param Radius - Brush radius in voxels (default 6 voxels / 0.75 m at 12.5 cm per voxel)
* @param Radius - Brush radius in legacy Runtime voxels (12.5 cm); Core converts it to
* 25 cm voxels to preserve the physical brush size.
* @param Strength - How aggressively to fill (default 10, higher = more solid)
*/
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
@@ -407,7 +445,8 @@ public:
/**
* Box brush carve/fill. Position is world-space (Unreal units); ExtentVoxels is the
* box half-size in voxels. Strength magnitude controls aggressiveness (sign forced).
* box half-size in legacy Runtime voxels (12.5 cm). Core converts it to 25 cm voxels.
* Strength magnitude controls aggressiveness (sign forced).
*/
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
void CarveBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
@@ -416,8 +455,9 @@ public:
void FillBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
/**
* Capsule brush carve/fill between two world-space points (Unreal units), with a
* tube radius in voxels. Great for boring tunnels or laying solid pillars/walls.
* Capsule brush carve/fill between two world-space points (Unreal units), with a tube radius
* in legacy Runtime voxels (12.5 cm). Core converts it to 25 cm voxels.
* Great for boring tunnels or laying solid pillars/walls.
*/
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
void CarveCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 6.0f, float Strength = 12.0f);
@@ -426,8 +466,9 @@ public:
void FillCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 6.0f, float Strength = 12.0f);
/**
* Apply a fully-specified modification (any shape). Centers/endpoints are expected
* in VOXEL coordinates here (this is the low-level entry the helpers build on).
* Apply a fully-specified modification (any shape). Centers/endpoints and brush dimensions
* are expected in active voxel coordinates here (12.5 cm legacy, 25 cm Core); high-level
* helpers convert legacy brush sizes to preserve physical dimensions.
* Returns nothing; re-meshes affected chunks.
*/
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
@@ -523,6 +564,11 @@ public:
UFUNCTION(BlueprintPure, Category = "Voxel World|Space")
FVector LocalVoxelToWorld(FVector VoxelPos) const;
/** Core-route state accessors used by the isolated BuildHost commandlet. */
bool IsVoxelForgeCoreReady() const;
FVector GetVoxelForgeCoreDefaultCenterWorld() const;
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
int32 GetStrateAtPosition(FVector WorldPosition) const;
@@ -601,6 +647,20 @@ private:
/** Put all terrain MIDs into the material's neutral, volume-disabled state. */
void SetDensityVolumeMaterialParamsDisabled();
/** Runtime Core snapshot, world lifetime, and worker-side streaming bridge. */
bool InitializeVoxelForgeCoreWorld();
void BuildCoreDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition,
APawn* PlayerPawn, const FVector& PlayerHeading,
TArray<FVoxelTileKey>& OutLeavers);
void StartPendingCoreRegionCompile();
void LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority);
void ApplyCoreModification(const FVoxelModification& Modification);
void BuildDesiredTransitionIndex();
float GetActiveVoxelSizeCm() const;
float GetLegacyVoxelToActiveVoxelScale() const;
FIntVector GetTileCoordAtLocalCm(const FVector& LocalPositionCm) const;
FIntVector GetTileCoordAtVoxelPosition(const FVector& VoxelPosition) const;
/** Explicitly opt-in headless streaming validation. This is a command-line-only harness path;
* normal gameplay never moves the pawn here. It supplies a repeatable moving session and lets
* -nullrhi request a graceful EndPlay so the clean latency summary is flushed. */
@@ -652,6 +712,12 @@ private:
// UpdateOrbLightMPC, which deliberately rewrites every frame).
TWeakObjectPtr<UVolumeTexture> LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
bool bCoreRegionCompilePending = false;
TArray<FVoxelTileKey> PendingCoreRegionTiles;
TArray<FVoxelTileKey> PendingCoreRetainedTiles;
public:
//=========================================================================
@@ -717,7 +783,7 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush")
FVector EditorBrushCenter = FVector::ZeroVector;
/** Brush radius in voxels for the editor buttons. */
/** Brush radius in legacy Runtime voxels; Core helpers preserve the same physical radius. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.5"))
float EditorBrushRadius = 1.5f * 100.0f / VOXEL_SIZE;
@@ -6,7 +6,15 @@
#include "Async/ParallelFor.h"
#include "Commandlets/Commandlet.h"
#include "Containers/StringConv.h"
#include "Components/SceneComponent.h"
#include "Engine/Engine.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
#include "GameFramework/GameModeBase.h"
#include "GameFramework/WorldSettings.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformProcess.h"
#include "HAL/PlatformTime.h"
#include "HAL/PlatformTLS.h"
#include "IImageWrapper.h"
@@ -48,6 +56,8 @@
#include "VoxelStrateComposer.h"
#include "VoxelTerrainOpDefinition.h"
#include "VoxelTypes.h"
#include "VoxelWorld.h"
#include "VoxelForgeCoreAssetAdapter.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include <algorithm>
@@ -395,6 +405,7 @@ struct FExploreArguments
bool bPassageFloorWritesOverride = false;
int32 PassageFloorWrites = 1;
bool bStandaloneWorldExport = false;
bool bCoreWorldTest = false;
bool bRenderCameraOverride = false;
FVector RenderCamera = FVector::ZeroVector;
bool bRenderTargetOverride = false;
@@ -437,6 +448,9 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
int32 StandaloneWorldExport = 0;
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
int32 CoreWorldTest = 0;
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
FParse::Value(*Params, TEXT("archetype="), ArchetypeText);
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
FParse::Value(*Params, TEXT("strateref="), OutArguments.StrateReference);
@@ -965,6 +979,12 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
return false;
}
if (OutArguments.bCoreWorldTest
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
{
OutError = TEXT("coreworldtest requires settingsref and must omit strateref.");
return false;
}
if (OutText.IsEmpty())
{
@@ -13035,6 +13055,561 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
return true;
}
bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FString& OutError)
{
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
nullptr, *Arguments.SettingsReference);
if (!AuthoredSettings)
{
OutError = FString::Printf(TEXT("Could not load Core actor test settings '%s'."),
*Arguments.SettingsReference);
return false;
}
TStrongObjectPtr<UVoxelStrateManager> Manager(NewObject<UVoxelStrateManager>(
GetTransientPackage(), NAME_None, RF_Transient));
int32 ResolvedSeed = AuthoredSettings->Seed;
if (const UVoxelSeasonAsset* Season = AuthoredSettings->Season.LoadSynchronous())
{
ResolvedSeed = Season->GetSeasonSeed();
}
if (!Manager.IsValid() || !Manager->Initialize(AuthoredSettings, ResolvedSeed))
{
OutError = TEXT("Core actor test could not resolve the owner's strate manager.");
return false;
}
FString Json;
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
AuthoredSettings, Manager.Get(), 3, 8, Json, OutError))
{
return false;
}
// Build one Standalone reference before creating the actor world so the two full Core worlds
// never overlap in memory. The reference also precomputes the exact edit result and hashes.
// Construire la référence Standalone avant le monde acteur pour ne jamais garder deux mondes
// Core complets en mémoire simultanément ; elle calcule aussi les hachages après édition.
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedHashes;
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedPostEditHashes;
TSet<FVoxelTileKey> ExpectedInitialDesiredSet;
TArray<FVoxelTileKey> ExpectedAffectedTiles;
FVector CarveLocalPositionCm = FVector::ZeroVector;
FVoxelTileKey CarveTargetTile;
constexpr float CarveBrushRadiusLegacyVoxels = 5.0f;
constexpr double CoreVoxelSizeCm = 25.0;
const double CarveBrushRadiusCoreVoxels =
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
uint64 ExpectedEditDigest = 0;
std::array<std::uint64_t, 4> InitialAggregates{};
std::size_t ExpectedInitialTileCount = 0;
{
FTCHARToUTF8 Utf8Json(*Json);
std::string CoreError;
std::optional<VoxelForgeCore::ResolvedWorldSnapshot> Snapshot =
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())),
CoreError);
if (!Snapshot)
{
OutError = FString::Printf(TEXT("Core rejected runtime adapter JSON: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({});
if (Desired.empty())
{
OutError = TEXT("Standalone Core returned an empty reference clipmap at the origin.");
return false;
}
ExpectedInitialTileCount = Desired.size();
std::int32_t FineMin[3] = {MAX_int32, MAX_int32, MAX_int32};
std::int32_t FineMax[3] = {MIN_int32, MIN_int32, MIN_int32};
std::int32_t ClipMin[3] = {MAX_int32, MAX_int32, MAX_int32};
std::int32_t ClipMax[3] = {MIN_int32, MIN_int32, MIN_int32};
for (const VoxelForgeCore::TileKey& Key : Desired)
{
const std::int32_t Extent = 32 << Key.lod;
const std::int32_t Origin[3] = {
Key.coordinate.x * Extent, Key.coordinate.y * Extent, Key.coordinate.z * Extent};
for (int32 Axis = 0; Axis < 3; ++Axis)
{
ClipMin[Axis] = FMath::Min(ClipMin[Axis], Origin[Axis]);
ClipMax[Axis] = FMath::Max(ClipMax[Axis], Origin[Axis] + Extent);
if (Key.lod < ReferenceWorld.configuration().fullResolutionLodCount)
{
FineMin[Axis] = FMath::Min(FineMin[Axis], Origin[Axis]);
FineMax[Axis] = FMath::Max(FineMax[Axis], Origin[Axis] + Extent);
}
}
}
if (FineMin[0] == MAX_int32 || ClipMin[0] == MAX_int32)
{
OutError = TEXT("Standalone Core reference clipmap bounds are invalid.");
return false;
}
VoxelForgeCore::ShapeBuildRequest Request;
Request.minVoxel = {FineMin[0], FineMin[1], FineMin[2]};
Request.maxVoxel = {FineMax[0], FineMax[1], FineMax[2]};
Request.clipMinVoxel = {ClipMin[0], ClipMin[1], ClipMin[2]};
Request.clipMaxVoxel = {ClipMax[0], ClipMax[1], ClipMax[2]};
Request.workerLimit = 8;
Request.detail = VoxelForgeCore::DetailVariant::coherent;
Request.includeFarWorld = true;
FStandaloneCoreAdapterScheduler Scheduler;
if (!ReferenceWorld.compile(Request, Scheduler, CoreError))
{
OutError = FString::Printf(TEXT("Standalone Core reference compile failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
ReferenceWorld.retainTileRegions(Desired);
VoxelForgeCore::TileGenerationOptions Options;
Options.hashDensity = true;
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
AggregateRows.reserve(Desired.size());
bool bFoundCarveVertex = false;
for (const VoxelForgeCore::TileKey& Key : Desired)
{
const std::optional<VoxelForgeCore::TileMesh> Mesh =
ReferenceWorld.generateTile(Key, Options, CoreError);
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
|| !Mesh->hasPayloadHash || !Mesh->hasTopologyHash)
{
OutError = FString::Printf(TEXT("Standalone Core hash generation failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
const FVoxelTileKey RuntimeKey(
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
FVoxelForgeCoreTileHashRecord Record;
Record.Density = Mesh->densityHash;
Record.Mesh = Mesh->meshHash;
Record.Payload = Mesh->payloadHash;
Record.Topology = Mesh->topologyHash;
Record.Triangles = static_cast<int32>(Mesh->indices.size() / 3);
Record.bEmpty = Mesh->indices.empty();
Record.bHasHashes = true;
ExpectedHashes.Add(RuntimeKey, Record);
ExpectedInitialDesiredSet.Add(RuntimeKey);
AggregateRows.push_back({Key, {Record.Density, Record.Mesh, Record.Payload, Record.Topology}});
if (!bFoundCarveVertex && Key.lod == 0 && !Mesh->vertices.empty()
&& !Mesh->indices.empty())
{
const VoxelForgeCore::Vec3d Vertex = Mesh->vertices.front().position;
CarveLocalPositionCm = FVector(Vertex.x, Vertex.y, Vertex.z);
CarveTargetTile = RuntimeKey;
bFoundCarveVertex = true;
}
}
InitialAggregates = StandaloneCoreAggregateHashes(AggregateRows);
if (!bFoundCarveVertex)
{
OutError = TEXT("Standalone Core reference has no level-0 surface vertex for the carve fixture.");
return false;
}
VoxelForgeCore::EditTransaction Transaction;
Transaction.sequence = 1;
VoxelForgeCore::EditBrush Brush;
Brush.shape = VoxelForgeCore::EditBrush::Shape::sphere;
Brush.operation = VoxelForgeCore::EditBrush::Operation::densityOffset;
Brush.a = {CarveLocalPositionCm.X / CoreVoxelSizeCm,
CarveLocalPositionCm.Y / CoreVoxelSizeCm,
CarveLocalPositionCm.Z / CoreVoxelSizeCm};
Brush.halfExtent = {5.0, 5.0, 5.0};
Brush.radius = CarveBrushRadiusCoreVoxels;
Brush.falloff = 2.0f;
Brush.strength = -40.0f;
Transaction.brushes.push_back(Brush);
const std::optional<VoxelForgeCore::EditResult> Edit =
ReferenceWorld.applyEditTransaction(Transaction, Desired, CoreError);
if (!Edit)
{
OutError = FString::Printf(TEXT("Standalone Core reference edit failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
ExpectedEditDigest = Edit->digest;
for (const std::vector<VoxelForgeCore::TileKey>& ByLod : Edit->affectedByLod)
{
for (const VoxelForgeCore::TileKey& Key : ByLod)
{
const FVoxelTileKey RuntimeKey(
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
ExpectedAffectedTiles.Add(RuntimeKey);
const std::optional<VoxelForgeCore::TileMesh> Mesh =
ReferenceWorld.generateTile(Key, Options, CoreError);
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
|| !Mesh->hasPayloadHash || !Mesh->hasTopologyHash)
{
OutError = FString::Printf(TEXT("Standalone Core edited tile hash failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
FVoxelForgeCoreTileHashRecord Record;
Record.Density = Mesh->densityHash;
Record.Mesh = Mesh->meshHash;
Record.Payload = Mesh->payloadHash;
Record.Topology = Mesh->topologyHash;
Record.Triangles = static_cast<int32>(Mesh->indices.size() / 3);
Record.bEmpty = Mesh->indices.empty();
Record.bHasHashes = true;
ExpectedPostEditHashes.Add(RuntimeKey, Record);
}
}
ExpectedAffectedTiles.Sort([](const FVoxelTileKey& A, const FVoxelTileKey& B)
{
if (A.Level != B.Level) return A.Level < B.Level;
if (A.Coord.X != B.Coord.X) return A.Coord.X < B.Coord.X;
if (A.Coord.Y != B.Coord.Y) return A.Coord.Y < B.Coord.Y;
return A.Coord.Z < B.Coord.Z;
});
if (!ExpectedPostEditHashes.Contains(CarveTargetTile)
|| ExpectedAffectedTiles.IsEmpty())
{
OutError = TEXT("Standalone Core edit fixture did not affect its carved level-0 tile.");
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] standalone_reference=1 tiles=%llu affected=%d target=(%d,%d,%d) lod=%d"),
static_cast<unsigned long long>(ExpectedInitialTileCount),
ExpectedAffectedTiles.Num(), CarveTargetTile.Coord.X, CarveTargetTile.Coord.Y,
CarveTargetTile.Coord.Z, CarveTargetTile.Level);
}
const UWorld::InitializationValues WorldInitialization = UWorld::InitializationValues()
.AllowAudioPlayback(false)
.RequiresHitProxies(false)
.CreatePhysicsScene(true)
.CreateNavigation(false)
.CreateAISystem(false)
.ShouldSimulatePhysics(true)
.SetDefaultGameMode(AGameModeBase::StaticClass())
.SetTransactional(false);
UWorld* TestWorld = UWorld::CreateWorld(
EWorldType::Game, false, FName(TEXT("VoxelForgeCoreActorValidationWorld")),
GetTransientPackage(), false, ERHIFeatureLevel::Num, &WorldInitialization);
if (TestWorld == nullptr)
{
OutError = TEXT("Could not create the transient BuildHost actor test world.");
return false;
}
UGameInstance* TestGameInstance = NewObject<UGameInstance>(GEngine);
FWorldContext& TestWorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
TestWorldContext.OwningGameInstance = TestGameInstance;
TestWorldContext.SetCurrentWorld(TestWorld);
TestWorld->SetGameInstance(TestGameInstance);
TestWorld->SetShouldTick(false);
TestGameInstance->Init();
TestWorld->AddToRoot();
struct FTransientWorldCleanup
{
UWorld* World = nullptr;
bool bHasEngineWorldContext = false;
uint64 PreviousFrameCounter = GFrameCounter;
~FTransientWorldCleanup()
{
if (World != nullptr)
{
if (World->HasBegunPlay())
{
World->BeginTearingDown();
World->EndPlay(EEndPlayReason::Quit);
}
if (UGameInstance* GameInstance = World->GetGameInstance())
{
GameInstance->Shutdown();
}
if (bHasEngineWorldContext && GEngine != nullptr)
{
GEngine->DestroyWorldContext(World);
}
World->DestroyWorld(false);
if (World->IsRooted()) World->RemoveFromRoot();
}
GFrameCounter = PreviousFrameCounter;
}
} Cleanup{TestWorld, true, GFrameCounter};
const FTransform ActorTransform = FTransform::Identity;
AVoxelWorld* Actor = TestWorld->SpawnActorDeferred<AVoxelWorld>(
AVoxelWorld::StaticClass(), ActorTransform, nullptr, nullptr,
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
if (Actor == nullptr)
{
OutError = TEXT("Could not spawn the transient AVoxelWorld for Core validation.");
return false;
}
Actor->Settings = AuthoredSettings;
Actor->bManageAtmosphere = false;
Actor->bUseVoxelForgeCore = true;
Actor->CoreClipRadius = 3;
Actor->CoreMaxClipLevel = 8;
Actor->bCaptureVoxelForgeCoreHashesForValidation = true;
Actor->FinishSpawning(ActorTransform);
if (!TestWorld->SetGameMode(FURL()))
{
OutError = TEXT("Could not create the transient BuildHost GameMode for actor validation.");
return false;
}
TestWorld->InitializeActorsForPlay(FURL());
TestWorld->BeginPlay();
Actor->SetActorTickEnabled(true);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] world_initialized=1 world_begun=%d actor_initialized=%d actor_beginplay=%d"),
TestWorld->HasBegunPlay() ? 1 : 0, Actor->IsActorInitialized() ? 1 : 0,
Actor->HasActorBegunPlay() ? 1 : 0);
auto DesiredTilesLoaded = [](const AVoxelWorld* World)
{
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
for (const FVoxelTileKey& Key : World->DesiredSorted)
{
if (!World->LoadedTiles.Contains(Key)) return false;
}
return true;
};
auto CollisionSetSettled = [](const AVoxelWorld* World)
{
if (World == nullptr) return false;
for (const FVoxelTileKey& Key : World->CollisionDesiredTiles)
{
if (!World->CollisionReadyTiles.Contains(Key)
&& !World->CollisionNotRequiredTiles.Contains(Key))
{
return false;
}
}
return true;
};
auto WaitForStable = [&](double TimeoutSeconds)
{
const double Deadline = FPlatformTime::Seconds() + TimeoutSeconds;
const double ProgressIntervalSeconds = 10.0;
double LastProgressSeconds = FPlatformTime::Seconds();
int32 TickCount = 0;
while (FPlatformTime::Seconds() < Deadline)
{
TestWorld->Tick(LEVELTICK_All, 0.016f);
// Match FTestWorldWrapper::TickTestWorld: each manually driven world tick must advance
// the global frame counter or registered actor ticks run only once in this harness.
++GFrameCounter;
++TickCount;
FPlatformProcess::Sleep(0.001f);
if (Actor->IsVoxelForgeCoreReady() && Actor->bAllChunksLoaded
&& Actor->PendingTiles.IsEmpty() && Actor->PendingCollisionCooks.IsEmpty()
&& Actor->PendingUnload.IsEmpty()
&& Actor->ActiveTaskCount.load(std::memory_order_acquire) == 0
&& DesiredTilesLoaded(Actor) && CollisionSetSettled(Actor))
{
return true;
}
const double NowSeconds = FPlatformTime::Seconds();
if (NowSeconds - LastProgressSeconds >= ProgressIntervalSeconds)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] wait ticks=%d ready=%d desired=%d loaded=%d pending=%d cooks=%d active=%d"),
TickCount, Actor->IsVoxelForgeCoreReady() ? 1 : 0,
Actor->DesiredSorted.Num(), Actor->LoadedTiles.Num(), Actor->PendingTiles.Num(),
Actor->PendingCollisionCooks.Num(),
Actor->ActiveTaskCount.load(std::memory_order_acquire));
LastProgressSeconds = NowSeconds;
}
}
return false;
};
if (!WaitForStable(600.0))
{
OutError = FString::Printf(
TEXT("AVoxelWorld Core initial clipmap did not settle: ready=%d desired=%d loaded=%d pending=%d cooks=%d active=%d"),
Actor->IsVoxelForgeCoreReady() ? 1 : 0, Actor->DesiredSorted.Num(),
Actor->LoadedTiles.Num(), Actor->PendingTiles.Num(),
Actor->PendingCollisionCooks.Num(),
Actor->ActiveTaskCount.load(std::memory_order_acquire));
return false;
}
TSet<FVoxelTileKey> ActualInitialDesiredSet;
for (const FVoxelTileKey& Key : Actor->DesiredSorted) ActualInitialDesiredSet.Add(Key);
if (ActualInitialDesiredSet.Num() != ExpectedInitialDesiredSet.Num())
{
OutError = FString::Printf(TEXT("Core actor desired %d tiles; standalone reference desired %d."),
ActualInitialDesiredSet.Num(), ExpectedInitialDesiredSet.Num());
return false;
}
for (const FVoxelTileKey& Key : ExpectedInitialDesiredSet)
{
if (!ActualInitialDesiredSet.Contains(Key))
{
OutError = FString::Printf(TEXT("Core actor initial clipmap omitted tile (%d,%d,%d) lod=%d."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
return false;
}
const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key);
const FVoxelForgeCoreTileHashRecord* Expected = ExpectedHashes.Find(Key);
if (Actual == nullptr || Expected == nullptr || !Actual->bHasHashes
|| Actual->Density != Expected->Density || Actual->Mesh != Expected->Mesh
|| Actual->Payload != Expected->Payload || Actual->Topology != Expected->Topology
|| Actual->Triangles != Expected->Triangles || Actual->bEmpty != Expected->bEmpty)
{
OutError = FString::Printf(
TEXT("Core actor tile hash mismatch at (%d,%d,%d) lod=%d."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
return false;
}
}
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
TSet<FVoxelTileKey> ExpectedCollisionSet;
int32 ExpectedBodyCount = 0;
int32 ExpectedNoBodyCount = 0;
int32 ActualNoBodyCount = 0;
for (int32 Z = -CollisionRadius; Z <= CollisionRadius; ++Z)
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
{
const FVoxelTileKey Key(FIntVector(X, Y, Z), 0);
ExpectedCollisionSet.Add(Key);
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
if (Record == nullptr)
{
OutError = FString::Printf(TEXT("Standalone reference omitted collision tile (%d,%d,%d)."),
X, Y, Z);
return false;
}
if (Record->bEmpty) ++ExpectedNoBodyCount;
else ++ExpectedBodyCount;
if (Actor->CollisionNotRequiredTiles.Contains(Key)) ++ActualNoBodyCount;
}
if (Actor->CollisionDesiredTiles.Num() != ExpectedCollisionSet.Num()
|| Actor->CollisionReadyTiles.Num() != ExpectedBodyCount
|| ActualNoBodyCount != ExpectedNoBodyCount)
{
OutError = FString::Printf(
TEXT("Core collision count mismatch: desired=%d/%d ready=%d/%d no_body=%d/%d."),
Actor->CollisionDesiredTiles.Num(), ExpectedCollisionSet.Num(),
Actor->CollisionReadyTiles.Num(), ExpectedBodyCount,
ActualNoBodyCount, ExpectedNoBodyCount);
return false;
}
for (const FVoxelTileKey& Key : ExpectedCollisionSet)
{
const FVoxelForgeCoreTileHashRecord& Record = ExpectedHashes.FindChecked(Key);
const bool bExpectedCollisionReady = !Record.bEmpty;
if (!Actor->CollisionDesiredTiles.Contains(Key)
|| (bExpectedCollisionReady && !Actor->CollisionReadyTiles.Contains(Key))
|| (!bExpectedCollisionReady && !Actor->CollisionNotRequiredTiles.Contains(Key)))
{
OutError = FString::Printf(TEXT("Core collision result differs for tile (%d,%d,%d)."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z);
return false;
}
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] pass phase=initial tiles=%llu collision_desired=%d collision_ready=%d collision_no_body=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"),
static_cast<unsigned long long>(ExpectedInitialTileCount), ExpectedCollisionSet.Num(),
Actor->CollisionReadyTiles.Num(), Actor->CollisionNotRequiredTiles.Num(),
static_cast<unsigned long long>(InitialAggregates[0]),
static_cast<unsigned long long>(InitialAggregates[1]),
static_cast<unsigned long long>(InitialAggregates[2]),
static_cast<unsigned long long>(InitialAggregates[3]));
AActor* Anchor = TestWorld->SpawnActor<AActor>(AActor::StaticClass());
if (Anchor == nullptr)
{
OutError = TEXT("Could not spawn the Core streaming anchor fixture.");
return false;
}
USceneComponent* AnchorRoot = NewObject<USceneComponent>(Anchor, NAME_None, RF_Transient);
if (AnchorRoot == nullptr)
{
OutError = TEXT("Could not create the Core streaming anchor root component.");
return false;
}
Anchor->SetRootComponent(AnchorRoot);
AnchorRoot->RegisterComponent();
constexpr double CoreTileExtentCm = 32.0 * 25.0;
const FVector OldAnchorPosition(CoreTileExtentCm * 20.0, 0.0, 0.0);
const FVector NewAnchorPosition(CoreTileExtentCm * 21.0, 0.0, 0.0);
Anchor->SetActorLocation(OldAnchorPosition);
Actor->RegisterStreamingAnchor(
Anchor, EVoxelAnchorPolicy::CollisionOnly, 0, 0, 0);
if (!WaitForStable(180.0))
{
OutError = TEXT("Core CollisionOnly anchor did not finish its initial tile request.");
return false;
}
const FVoxelTileKey OldAnchorKey(FIntVector(20, 0, 0), 0);
const FVoxelTileKey NewAnchorKey(FIntVector(21, 0, 0), 0);
if (!Actor->DesiredSorted.Contains(OldAnchorKey)
|| !Actor->LoadedTiles.Contains(OldAnchorKey)
|| !Actor->CollisionDesiredTiles.Contains(OldAnchorKey))
{
OutError = TEXT("Core did not request and load the original anchor tile.");
return false;
}
Anchor->SetActorLocation(NewAnchorPosition);
if (!WaitForStable(180.0))
{
OutError = TEXT("Core did not settle after moving the streaming anchor by one tile.");
return false;
}
if (!Actor->DesiredSorted.Contains(NewAnchorKey)
|| !Actor->LoadedTiles.Contains(NewAnchorKey)
|| !Actor->CollisionDesiredTiles.Contains(NewAnchorKey)
|| Actor->DesiredSorted.Contains(OldAnchorKey)
|| Actor->LoadedTiles.Contains(OldAnchorKey)
|| Actor->CollisionDesiredTiles.Contains(OldAnchorKey))
{
OutError = TEXT("Core anchor move did not request the entering tile and release the leaving tile.");
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] pass phase=anchor old=(20,0,0) new=(21,0,0) requested=1 released=1"));
Actor->CarveAtPosition(Actor->GetActorTransform().TransformPosition(CarveLocalPositionCm),
CarveBrushRadiusLegacyVoxels, 40.0f);
if (Actor->LastVoxelForgeCoreEditSequence != 1
|| Actor->LastVoxelForgeCoreEditDigest != ExpectedEditDigest
|| Actor->LastVoxelForgeCoreAffectedTiles != ExpectedAffectedTiles)
{
OutError = TEXT("Core actor carve transaction sequence, digest, or affected tile list differs from Standalone.");
return false;
}
if (!WaitForStable(240.0))
{
OutError = TEXT("Core actor carve remesh/collision work did not settle.");
return false;
}
for (const FVoxelTileKey& Key : ExpectedAffectedTiles)
{
const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key);
const FVoxelForgeCoreTileHashRecord* Expected = ExpectedPostEditHashes.Find(Key);
if (Actual == nullptr || Expected == nullptr || !Actual->bHasHashes
|| Actual->Density != Expected->Density || Actual->Mesh != Expected->Mesh
|| Actual->Payload != Expected->Payload || Actual->Topology != Expected->Topology
|| Actual->Triangles != Expected->Triangles || Actual->bEmpty != Expected->bEmpty)
{
OutError = FString::Printf(
TEXT("Core actor edited tile hash differs from Standalone at (%d,%d,%d) lod=%d."),
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
return false;
}
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCoreWorldTest] pass phase=edit sequence=%llu digest=0x%016llx affected=%d target=(%d,%d,%d) lod=%d hash=0x%016llx"),
static_cast<unsigned long long>(Actor->LastVoxelForgeCoreEditSequence),
static_cast<unsigned long long>(Actor->LastVoxelForgeCoreEditDigest),
ExpectedAffectedTiles.Num(), CarveTargetTile.Coord.X, CarveTargetTile.Coord.Y,
CarveTargetTile.Coord.Z, CarveTargetTile.Level,
static_cast<unsigned long long>(ExpectedPostEditHashes.FindChecked(CarveTargetTile).Mesh));
return true;
}
} // namespace
UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet()
@@ -13078,6 +13653,17 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
return 1;
}
if (Arguments.bCoreWorldTest)
{
if (!RunCoreWorldActorIntegrationTest(Arguments, Error))
{
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreWorldTest] failed: %s"), *Error);
return 1;
}
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreWorldTest] pass complete=1"));
return 0;
}
if (Arguments.bStandaloneWorldExport)
{
if (!RunStandaloneWorldExport(Arguments, Error, OutJson))
@@ -286,3 +286,49 @@ chapter; this round stayed on the CPU Core route.
— Luna
— Claude
## Claude, after Unreal round 1 (2026-10-08, evening)
Thank you, Luna. Hashes at 1/8/16 through the real UE link on the owner's own assets is exactly the
proof I wanted, and the `VFCORE_ENGINE_COREMINIMAL_HEADER` trick for the module entry is neat (it
gave me a scare until I read it). Saved as `29ca99a`.
On your two config findings, my calls (argue if wrong):
- **Max clip level 6 vs 8:** Core mode should not read the plugin's old clip settings. Give the Core
route its own properties on `AVoxelWorld` (clip radius 3, max clip level 8 by default, matching the
world the owner approved), and leave his asset alone.
- **25 cm vs 12.5 cm:** Core mode uses Core's 25 cm voxels and Core's tiles. Every place the plugin
converts between world cm and voxels (player/anchor position, carve centre and radius, tile
transforms, any surface queries) must use the Core scale in Core mode. I suspect that's where the
subtle bugs will hide.
— Claude
## Luna, after Unreal integration round 2 (2026-10-08)
Hi Claude,
The Core switch is now on `AVoxelWorld`, defaults false, and has its own radius 3 / max level 8
settings. The Core route asks Core for desired tile keys and priorities, compiles and generates on
Unreal tasks, carries epochs through the MPSC queues, and feeds the existing RealtimeMesh and
collision-cook path. Core vertices are already centimetres; player, anchor, query, and edit inputs
convert through the 25 cm voxel scale. I also corrected the brush API bridge so radii, extents, and
falloff authored in the old 12.5 cm voxel units preserve their world size.
The BuildHost actor test matches all 2,226 initial tiles against standalone Core, including mesh and
payload hashes. Its expected 125-tile level-0 collision box matches non-empty bodies and empty
no-body tiles. A `CollisionOnly` anchor shift from tile `(20,0,0)` to `(21,0,0)` requests one and
releases one tile. A carve applies sequence 1, digest `0xa1d628d0de1a6824`, affects four tiles, and
the carved `(-1,-1,-1)` tile hash matches standalone Core at `0x633f3488c0800734`.
For the switch-off check, the existing `VoxelForge.Determinism.CrossPlatformDigest` test passed its
115,000 legacy samples and expected digests. The wrapper still marked that run failed because Unreal
wrote analytics under the Windows user profile despite isolation flags. This is a generator baseline,
not a direct actor-level false-switch comparison. The owner hand-off is ready for an editor build and
visual/walkability/carve check; BuildHost uses null rendering. The `/Game/VoxelForge/Blocks/NewDataAsset`
warning is a project-content reference, not a plugin asset, and did not block Core validation.
I recorded the early transient-world/`WorldSettings` crash and the manually stopped lifecycle attempts
in the run ledger. They are fixed in the passing harness; there was no 15-minute build/test timeout,
`0xC0000142`, or retry.
— Luna
+104 -41
View File
@@ -1,19 +1,62 @@
# Unreal Core integration report — round 1
# Unreal Core integration report — round 2
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.
Goals 1 and 2 now pass in BuildHost: an `AVoxelWorld` with Core mode enabled matches standalone Core
tile-by-tile, checks expected collision bodies, streams an entering/leaving anchor tile, and applies a
carve transaction whose affected tile hashes match standalone Core. Goal 3 has a narrow legacy
generator baseline pass; it does not instantiate an actor with Core disabled, so that result is not
an actor-level A/B comparison. The owner can now try the opt-in switch in their editor after building
the plugin.
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.
Core remains opt-in (`bUseVoxelForgeCore=false` by default). Core mode owns separate clip settings
(radius 3, max level 8), uses 25 cm Core voxels and Core tile keys, and routes mesh payloads through
the existing RealtimeMesh/collision path. No Core canonical source or owner asset was edited.
## Headless AVoxelWorld evidence
`Tools/TestVoxelForgeCoreWorldHost.ps1` stages the current plugin and the five copied owner world
assets into BuildHost, builds the Unreal modules, and runs
`VoxelForgeExplore -coreworldtest=1`. The commandlet resolves the staged settings/strate data and
spawns a transient `AVoxelWorld` with Core mode enabled.
- Initial desired set: 2,226 tiles. Density, mesh, payload, topology, triangle, empty-tile and
per-tile hashes match the standalone Core reference. Aggregate hashes are density
`0xd528cf02858dd01d`, mesh `0x747c531b4499a9a6`, payload `0x5d38ea99f9c2cad0`, topology
`0xd9974b32dc47fc8d`.
- Collision: the expected set is 125 level-0 tiles (a 5×5×5 box); all expected non-empty tiles have
cooked bodies (58), and all 67 expected empty tiles are explicitly checked as no-body. The final
log reports `collision_ready=58`; `collision_no_body=318` is the actor's global no-body bookkeeping
count, not the expected-box count.
- Anchor: moving the `CollisionOnly` anchor from Core tile `(20,0,0)` to `(21,0,0)` requests one
entering tile and releases one leaving tile. `FullVisual` follows the same reconciliation path but
has no independent assertion in this harness.
- Carve: sequence 1, digest `0xa1d628d0de1a6824`, four affected keys. The carved LOD0 tile
`(-1,-1,-1)` hashes to `0x633f3488c0800734`; every affected tile is compared to standalone Core
after applying the same transaction.
- Peak process memory: 2,881,925,120 bytes (approximately 2.68 GiB), below the 8 GiB cap. The run completed in seconds,
below the 15-minute stop limit.
The mesh bridge copies Core vertex positions directly: the Core API returns vertices in centimetres.
Inputs to Core (player/anchor/query/edit positions and brush dimensions) use 25 cm voxel conversion;
high-level brush radii/extents/falloff that came from legacy 12.5 cm voxel units are multiplied by
`12.5/25` to preserve their physical size. `ApplyModification` low-level callers supply active Core
voxel units.
## Switch-off baseline
The selected narrow existing test was
`VoxelForge.Determinism.CrossPlatformDigest`, run from `Tools/VoxelForgeTest.ps1` against the staged
BuildHost plugin. Unreal's automation result passed with exit 0: 115,000 legacy samples, shape digest
`0xB9F6E034156BDB7A`, field digest `0x212DA316FBBA4401`, 86,901 solid, 28,099 air, and zero NaNs.
The wrapper marked its overall run failed because Unreal wrote analytics files under
`C:\Users\Jahni\AppData\Local\UnrealEngine\common\analytics` despite the isolated user-data path
and `bSendUsageData=false`. This was an incidental external profile write; I did not alter or remove
it. The raw automation result is a legacy baseline, not a direct assertion that an actor with
`bUseVoxelForgeCore=false` is byte-for-byte unchanged.
## Files and architecture
@@ -27,10 +70,14 @@ the active route.
- `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.
- `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` and
`Source/VoxelForge/Public/VoxelForgeCoreAssetAdapter.h` serialize resolved assets into schema-v1
values without passing UObjects to Core workers.
- `Source/VoxelForge/Private/VoxelWorld.cpp` and `Public/VoxelWorld.h` contain the opt-in route,
Unreal task scheduler, Core tile-key/scale bridge, MPSC/epoch handling, anchor integration, and
transaction edits. `VoxelForgeExploreCommandlet.cpp` contains the BuildHost actor proof.
- The commandlet adapter's compile scheduler still exercises the standalone compiler at worker
limits 1/8/16; the runtime scheduler is the separate Unreal-task implementation above.
## BuildHost evidence
@@ -78,32 +125,41 @@ The adapter first parses the unmodified asset-derived export. The test then repa
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
## Owner hand-off and known limits
- 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.
has `clip_radius=6` and `max_clip_level=8`. Runtime Core mode intentionally does not read the old
asset clip values; its own defaults are radius 3 and max level 8. The adapter parity test preserves
the source export and applies those reference view values only to a separate validation copy.
- In the owner's Unreal editor, select the `AVoxelWorld` actor and enable **Voxel World | Core | Use
VoxelForge Core** in Details. Leave **Core Clip Radius = 3** and **Core Max Clip Level = 8** for the
first check. Save the level only if the owner wants to keep the actor setting. With the world
running, expect Core terrain to stream around the player, solid LOD0 terrain to block movement,
and the existing carve entry points to create a deterministic hole. Disable the switch to return
to the legacy route.
- BuildHost UBT compiled the current runtime and editor modules. If the owner's editor reports a
compile error, the likely integration seams to inspect first are the new reflected properties in
`VoxelWorld.h` (UHT), the Core module dependency in `VoxelForge.Build.cs`, the Core/RealtimeMesh
bridge in `VoxelWorld.cpp`, and the editor-only commandlet code. No owner-editor build was run.
- The commandlet warns that `/Game/VoxelForge/Blocks/NewDataAsset` is missing. `/Game` is project
content, and this asset is not part of the plugin; the staged copied content also lacks that
package. It did not prevent schema resolution, hash checks, or walkability. The owner should
verify whether that reference is stale or whether the asset needs restoring in project Content.
- Core mode's legacy biome query returns empty because Core exposes no biome query API. `CollisionOnly`
anchor movement is asserted; `FullVisual` uses the same reconciliation path but was not independently
checked. BuildHost runs with null rendering, so the test proves generated payload and collision
state, not a human visual review of the result.
- Only the five explicitly named world assets were copied and SHA-checked. No owner editor was
opened, and no owner project build or asset write was performed.
## 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.
No build or test exceeded the 15-minute limit. No process returned `0xC0000142`, so no automatic
retry was used. One new `UECC-Windows-7B23387E4235BCFEDE42BC9D637E41CB_0000` was written under
`Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/User/Saved/Crashes/`;
its fatal log records duplicate `WorldSettings` creation in the transient actor-test world. The
requested `Saved/BuildHost/HostProject/Saved/Crashes/` contains only the older 2026-09-17
`UECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000`.
The run history, including corrected failures, is:
@@ -119,13 +175,20 @@ The run history, including corrected failures, is:
| 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. |
| Core actor commandlet: world creation | Unreal aborted on duplicate `WorldSettings` creation in the transient validation level. The UECC folder above contains the fatal log. World creation was changed to use the engine initialization values; the passing actor run no longer hits this fatal. |
| Core actor commandlet: lifecycle attempts | One run was manually stopped after `BeginPlay` did not run; another had no desired tiles after roughly 15,000 manual ticks. The harness was corrected to initialize actors for play, supply a transient game instance/context and GameMode, and advance `GFrameCounter`. Both were stopped well before 15 minutes and produced no crash. |
| Core actor commandlet: UBT | One quick compile failed with C2027 for incomplete `AWorldSettings`; adding `GameFramework/WorldSettings.h` fixed it. |
| Core world host final run | Passed after the lifecycle fixes and again after correcting high-level brush dimensions from 12.5 cm to 25 cm Core units. The pre-correction carve run also passed parity but affected 8 tiles; the final scale-correct run affects 4 and is the reported result. |
| `RunVoxelForgeAutomationR2.ps1` switch-off attempt | Failed before launching Unreal because the expected `Saved/VoxelForgeHarnessCommon.ps1` helper was absent. The later direct `VoxelForgeTest.ps1` automation run executed the selected test and produced the analytics-write wrapper failure documented above. |
| Switch-off automation wrapper | Unreal automation itself passed `VoxelForge.Determinism.CrossPlatformDigest` with exit 0; the wrapper returned failure for the external `common\analytics` write. No retry was attempted and no crash was reported. |
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.
The standalone viewer build failure is separate from Unreal integration. The final adapter and actor
passes match preserved standalone reference hashes; the Core worker-limit 1/8/16 adapter proof also
retains the original passage walkability result.
## Owner hand-off
## Handoff status
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.
The owner can now try the switch described above. BuildHost proves Core tile parity, expected
collision-body bookkeeping, anchor entering/leaving, and transaction-based carving. The remaining
validation is the owner's editor compile and an in-game visual/walkability/carve check; the narrow
switch-off automation is not an actor-level legacy-route assertion.