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:
1 parent
29ca99af7f
commit
075e58187e
7 files changed
+2068
-117
No files matched your search
+30
-33
@@ -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
|
viewer remains a separate display-only consumer: it must load this OBJ/manifest and never evaluate
|
||||||
density or generate geometry.
|
density or generate geometry.
|
||||||
|
|
||||||
### 8.17 Standalone Core in Unreal — integration round 1
|
### 8.17 Standalone Core in Unreal — integration round 2
|
||||||
**Status (2026-10-08):** the `VoxelForgeCore` runtime module and manifest shims build and link in the
|
**Status (2026-10-08):** `AVoxelWorld::bUseVoxelForgeCore` now selects a Core runtime route and is
|
||||||
isolated BuildHost. The editor commandlet can adapt the owner's copied settings/strate assets into
|
false by default. Core mode has independent `CoreClipRadius=3` and `CoreMaxClipLevel=8` properties;
|
||||||
schema-v1 JSON, resolve it through `ResolvedWorldSnapshot::FromJson`, and reproduce the standalone
|
it does not read or write the older clip properties or owner assets. BuildHost UBT and the Core link
|
||||||
hashes at compile worker limits 1, 8, and 16. This has not yet changed the `AVoxelWorld` generation
|
symbol audit pass. A headless BuildHost actor proof matches all initial tile hashes against standalone
|
||||||
route. There is no `bUseVoxelForgeCore` property, and no world should be switched yet.
|
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
|
The game-thread asset adapter emits the immutable schema-v1 input and resolves it through
|
||||||
property/map/set order, and emits values rather than UObject pointers. It writes the standalone
|
`ResolvedWorldSnapshot::FromJson`; Core workers receive no UObjects. The Core route asks
|
||||||
schema scale of 25 cm per voxel. The plugin's existing Runtime generator still defines
|
`World::desiredTiles` and `prioritizeTiles`, compiles and generates through Unreal tasks, and carries
|
||||||
`VOXEL_SIZE=12.5 cm`, so a Core mesh-to-`FVoxelMeshData` bridge must define the coordinate/scale
|
the generation `Epoch` through MPSC results. The shared payload builder, RealtimeMesh upload, and
|
||||||
conversion before runtime use. The staged `DA_Settings` also says `MaxClipLevel=6`, while the
|
collision cook remain the final path. Workers observe `bShuttingDown`, and `EndPlay` drains active
|
||||||
standalone owner JSON says 8; hash validation keeps the authored JSON unchanged and applies the
|
tasks. Preserve §8.10 streaming, cancellation, epoch, collision-handoff, and per-frame-budget rules.
|
||||||
reference harness view (`clip_radius=3`, `max_clip_level=8`) only to its validation copy.
|
|
||||||
|
|
||||||
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,
|
Carving drains in-flight Core calls, applies an increasing-sequence `applyEditTransaction`, advances
|
||||||
mesher, output, streaming, and collision behavior.
|
the epoch, and regenerates the returned affected keys. This leaves a transaction boundary for later
|
||||||
2. On the game thread, adapt owner assets to an immutable schema-v1 snapshot. Keep UObjects out of
|
replication; no transport is implemented. Existing streaming anchors are included in Core desired
|
||||||
Core and worker data.
|
tiles; the BuildHost shift check covers `CollisionOnly`. `FullVisual` is wired through the same
|
||||||
3. On the Core branch, ask `World::desiredTiles` and `prioritizeTiles` for the target set/order;
|
anchor reconciliation but has not had an independent headless assertion. The owner-editor visual
|
||||||
compile the initial field using a `JobScheduler` backed by Unreal's task system. The plugin still
|
and physical play check remains the hand-off step. See
|
||||||
owns work admission, worker lifetime, cancellation, `bShuttingDown`, and the generation `Epoch`.
|
[`Standalone/UNREAL_INTEGRATION_REPORT.md`](Standalone/UNREAL_INTEGRATION_REPORT.md) for evidence,
|
||||||
4. Run `generateTile` on the plugin workers, explicitly reconcile Core centimetre positions with
|
known limits, and instructions.
|
||||||
the Runtime voxel scale, then copy mesh arrays/groups into `FVoxelMeshData`. Let the existing
|
|
||||||
`BuildTileStreamSet` → MPSC result → epoch check → RealtimeMesh upload/collision-cook path consume
|
|
||||||
the result.
|
|
||||||
5. For carving, drain Core calls on the current `World`, call `applyEditTransaction` with an
|
|
||||||
authority sequence and ordered brushes, bump the epoch, and regenerate its returned desired tiles.
|
|
||||||
Keep the current plugin behavior when the switch is false.
|
|
||||||
|
|
||||||
The module-link and asset-adapter evidence, including the settings mismatch and incomplete runtime
|
|
||||||
work, is recorded in [`Standalone/UNREAL_INTEGRATION_REPORT.md`](Standalone/UNREAL_INTEGRATION_REPORT.md).
|
|
||||||
Keep every §8.10 streaming, cancellation, epoch, collision-handoff, and per-frame-budget invariant
|
|
||||||
when implementing this route; Core does not own those systems.
|
|
||||||
|
|
||||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||||
|
|
||||||
|
|||||||
+20
-13
@@ -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`). |
|
| `VoxelForgeWormBlockSkipTest.cpp` | `VoxelForge.Determinism.WormBlockSkipSoundness` | Soundness and coverage of the worker-local worm N1 block proof (`voxel.WormBlockSkip`). |
|
||||||
| `VoxelForgePlayerFitWindow.h` | — | Shared measurement-window policy for player-fit diagnostics (test/editor side only). |
|
| `VoxelForgePlayerFitWindow.h` | — | Shared measurement-window policy for player-fit diagnostics (test/editor side only). |
|
||||||
|
|
||||||
### 3.13 Core module and schema adapter (integration round 1)
|
### 3.13 Core runtime integration (round 2)
|
||||||
The public Core API is linked as the `VoxelForgeCore` runtime module, with canonical implementation
|
The public Core API is linked as the `VoxelForgeCore` runtime module, with canonical implementation
|
||||||
sources still under `Standalone/`. UBT validates the 18-row manifest and its shims; BuildHost linked
|
sources still under `Standalone/`. UBT validates the 18-row manifest and its shims; BuildHost linked
|
||||||
the module and passed `CheckCoreSymbols.ps1` against the actual shim objects. See
|
the module and passed `CheckCoreSymbols.ps1` against the actual shim objects. See
|
||||||
[`Standalone/UNREAL_INTEGRATION_REPORT.md`](Standalone/UNREAL_INTEGRATION_REPORT.md) for build and
|
[`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
|
`RunStandaloneWorldExport` in `VoxelForgeExploreCommandlet.cpp` serializes owner assets on the game
|
||||||
thread to schema-v1 JSON. `FStandaloneCoreAdapterScheduler` and
|
thread to schema-v1 JSON. The adapter parses it through `ResolvedWorldSnapshot::FromJson` and keeps
|
||||||
`VerifyStandaloneCoreAdapter` parse that JSON through `ResolvedWorldSnapshot::FromJson`, compile at
|
UObjects off Core workers. The runtime switch is `AVoxelWorld::bUseVoxelForgeCore` (default false),
|
||||||
worker limits 1, 8, and 16, and compare the 2,226-tile hashes and passage walkability. The commandlet
|
with independent `CoreClipRadius=3` and `CoreMaxClipLevel=8`; it does not read or mutate the old clip
|
||||||
sorts map/set members and JSON object keys to keep the export deterministic. Its test copy applies
|
settings or assets.
|
||||||
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
|
When enabled, `AVoxelWorld` uses Core desired keys and priorities, compiles/generates through Unreal
|
||||||
carve transaction, or owner-facing switch instructions exist yet. `AVoxelWorld` still uses the
|
tasks, carries `Epoch` through the existing MPSC result queues, and uploads converted payloads through
|
||||||
existing density generator, mesher, queues, epoch checks, RealtimeMesh upload, and collision path.
|
RealtimeMesh and its collision cook. Core voxel coordinates use 25 cm per voxel; Core tile keys are
|
||||||
The Core dependency is present so the next round can wire the side-by-side route behind a default-off
|
retained, and Core mesh vertices are already in centimetres. High-level legacy brush dimensions are
|
||||||
switch while preserving that path.
|
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)
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -34,6 +34,18 @@ class UMaterialInstanceDynamic;
|
|||||||
class FScopedGenerationPause;
|
class FScopedGenerationPause;
|
||||||
class ACameraActor;
|
class ACameraActor;
|
||||||
namespace RealtimeMesh { struct FRealtimeMeshStreamSet; } // T1.f — worker-built geometry buffers
|
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
|
/** 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. */
|
* 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).
|
// section for a non-empty polygroup — configuring a missing one is invalid).
|
||||||
bool bHasGroundTris = false;
|
bool bHasGroundTris = false;
|
||||||
bool bHasCeilingTris = 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
|
// 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
|
// 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).
|
// sit in a strate whose content was cut out of the mesh entirely).
|
||||||
@@ -248,6 +265,25 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World")
|
||||||
bool bManageAtmosphere = true;
|
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
|
// CHUNK STORAGE
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -388,7 +424,8 @@ public:
|
|||||||
* Applies a spherical brush that subtracts density.
|
* Applies a spherical brush that subtracts density.
|
||||||
*
|
*
|
||||||
* @param Position - World-space center of the carve brush
|
* @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)
|
* @param Strength - How aggressively to carve (default 10, higher = deeper)
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
@@ -399,7 +436,8 @@ public:
|
|||||||
* Applies a spherical brush that adds density.
|
* Applies a spherical brush that adds density.
|
||||||
*
|
*
|
||||||
* @param Position - World-space center of the fill brush
|
* @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)
|
* @param Strength - How aggressively to fill (default 10, higher = more solid)
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
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 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")
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
void CarveBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
|
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);
|
void FillBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capsule brush carve/fill between two world-space points (Unreal units), with a
|
* Capsule brush carve/fill between two world-space points (Unreal units), with a tube radius
|
||||||
* tube radius in voxels. Great for boring tunnels or laying solid pillars/walls.
|
* 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")
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
void CarveCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 6.0f, float Strength = 12.0f);
|
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);
|
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
|
* Apply a fully-specified modification (any shape). Centers/endpoints and brush dimensions
|
||||||
* in VOXEL coordinates here (this is the low-level entry the helpers build on).
|
* 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.
|
* Returns nothing; re-meshes affected chunks.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||||
@@ -523,6 +564,11 @@ public:
|
|||||||
UFUNCTION(BlueprintPure, Category = "Voxel World|Space")
|
UFUNCTION(BlueprintPure, Category = "Voxel World|Space")
|
||||||
FVector LocalVoxelToWorld(FVector VoxelPos) const;
|
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")
|
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
|
||||||
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
||||||
|
|
||||||
@@ -601,6 +647,20 @@ private:
|
|||||||
/** Put all terrain MIDs into the material's neutral, volume-disabled state. */
|
/** Put all terrain MIDs into the material's neutral, volume-disabled state. */
|
||||||
void SetDensityVolumeMaterialParamsDisabled();
|
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;
|
/** 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
|
* 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. */
|
* -nullrhi request a graceful EndPlay so the clean latency summary is flushed. */
|
||||||
@@ -652,6 +712,12 @@ private:
|
|||||||
// UpdateOrbLightMPC, which deliberately rewrites every frame).
|
// UpdateOrbLightMPC, which deliberately rewrites every frame).
|
||||||
TWeakObjectPtr<UVolumeTexture> LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated
|
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:
|
public:
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -717,7 +783,7 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush")
|
||||||
FVector EditorBrushCenter = FVector::ZeroVector;
|
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"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.5"))
|
||||||
float EditorBrushRadius = 1.5f * 100.0f / VOXEL_SIZE;
|
float EditorBrushRadius = 1.5f * 100.0f / VOXEL_SIZE;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,15 @@
|
|||||||
#include "Async/ParallelFor.h"
|
#include "Async/ParallelFor.h"
|
||||||
#include "Commandlets/Commandlet.h"
|
#include "Commandlets/Commandlet.h"
|
||||||
#include "Containers/StringConv.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/FileManager.h"
|
||||||
|
#include "HAL/PlatformProcess.h"
|
||||||
#include "HAL/PlatformTime.h"
|
#include "HAL/PlatformTime.h"
|
||||||
#include "HAL/PlatformTLS.h"
|
#include "HAL/PlatformTLS.h"
|
||||||
#include "IImageWrapper.h"
|
#include "IImageWrapper.h"
|
||||||
@@ -48,6 +56,8 @@
|
|||||||
#include "VoxelStrateComposer.h"
|
#include "VoxelStrateComposer.h"
|
||||||
#include "VoxelTerrainOpDefinition.h"
|
#include "VoxelTerrainOpDefinition.h"
|
||||||
#include "VoxelTypes.h"
|
#include "VoxelTypes.h"
|
||||||
|
#include "VoxelWorld.h"
|
||||||
|
#include "VoxelForgeCoreAssetAdapter.h"
|
||||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -395,6 +405,7 @@ struct FExploreArguments
|
|||||||
bool bPassageFloorWritesOverride = false;
|
bool bPassageFloorWritesOverride = false;
|
||||||
int32 PassageFloorWrites = 1;
|
int32 PassageFloorWrites = 1;
|
||||||
bool bStandaloneWorldExport = false;
|
bool bStandaloneWorldExport = false;
|
||||||
|
bool bCoreWorldTest = false;
|
||||||
bool bRenderCameraOverride = false;
|
bool bRenderCameraOverride = false;
|
||||||
FVector RenderCamera = FVector::ZeroVector;
|
FVector RenderCamera = FVector::ZeroVector;
|
||||||
bool bRenderTargetOverride = false;
|
bool bRenderTargetOverride = false;
|
||||||
@@ -437,6 +448,9 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
int32 StandaloneWorldExport = 0;
|
int32 StandaloneWorldExport = 0;
|
||||||
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
||||||
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
|
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("archetype="), ArchetypeText);
|
||||||
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
|
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
|
||||||
FParse::Value(*Params, TEXT("strateref="), OutArguments.StrateReference);
|
FParse::Value(*Params, TEXT("strateref="), OutArguments.StrateReference);
|
||||||
@@ -965,6 +979,12 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
|
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
|
||||||
return false;
|
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())
|
if (OutText.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -13035,6 +13055,561 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
|||||||
return true;
|
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
|
} // namespace
|
||||||
|
|
||||||
UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet()
|
UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet()
|
||||||
@@ -13078,6 +13653,17 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
|
|||||||
return 1;
|
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 (Arguments.bStandaloneWorldExport)
|
||||||
{
|
{
|
||||||
if (!RunStandaloneWorldExport(Arguments, Error, OutJson))
|
if (!RunStandaloneWorldExport(Arguments, Error, OutJson))
|
||||||
|
|||||||
@@ -286,3 +286,49 @@ chapter; this round stayed on the CPU Core route.
|
|||||||
— Luna
|
— Luna
|
||||||
|
|
||||||
— Claude
|
— 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
|
||||||
@@ -1,19 +1,62 @@
|
|||||||
# Unreal Core integration report — round 1
|
# Unreal Core integration report — round 2
|
||||||
|
|
||||||
Date: 2026-10-08
|
Date: 2026-10-08
|
||||||
Branch: `unreal/core-integration`
|
Branch: `unreal/core-integration`
|
||||||
|
|
||||||
## Result
|
## Result
|
||||||
|
|
||||||
Goals 1 and 2 are complete in BuildHost. The `VoxelForgeCore` runtime module builds and links with
|
Goals 1 and 2 now pass in BuildHost: an `AVoxelWorld` with Core mode enabled matches standalone Core
|
||||||
the plugin, its 18 manifest shims pass the real UE link-symbol audit, and the editor commandlet
|
tile-by-tile, checks expected collision bodies, streams an entering/leaving anchor tile, and applies a
|
||||||
adapts the staged owner assets to schema-v1 JSON and matches the standalone Core hashes at compile
|
carve transaction whose affected tile hashes match standalone Core. Goal 3 has a narrow legacy
|
||||||
worker limits 1, 8, and 16.
|
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 remains opt-in (`bUseVoxelForgeCore=false` by default). Core mode owns separate clip settings
|
||||||
Core-backed tile submission, mesh conversion, collision upload, epoch flow, or Core edit
|
(radius 3, max level 8), uses 25 cm Core voxels and Core tile keys, and routes mesh payloads through
|
||||||
transaction. No owner hand-off switch is ready to flip. The existing plugin generation route remains
|
the existing RealtimeMesh/collision path. No Core canonical source or owner asset was edited.
|
||||||
the active route.
|
|
||||||
|
## 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
|
## Files and architecture
|
||||||
|
|
||||||
@@ -27,10 +70,14 @@ the active route.
|
|||||||
- `VoxelForge.Build.cs` and `VoxelForgeEditor.Build.cs` depend on `VoxelForgeCore`.
|
- `VoxelForge.Build.cs` and `VoxelForgeEditor.Build.cs` depend on `VoxelForgeCore`.
|
||||||
- `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` defines the empty standalone export
|
- `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.
|
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
|
- `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` and
|
||||||
the game thread, sorts map/set members and JSON object keys, parses the emitted JSON through
|
`Source/VoxelForge/Public/VoxelForgeCoreAssetAdapter.h` serialize resolved assets into schema-v1
|
||||||
`ResolvedWorldSnapshot::FromJson`, and validates full clipmap tile hashes. Its test scheduler
|
values without passing UObjects to Core workers.
|
||||||
exercises Core compile worker limits 1/8/16; it is not the future runtime UE task scheduler.
|
- `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
|
## 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
|
copy with the standalone CPU benchmark view settings `clip_radius=3` and `max_clip_level=8`. The
|
||||||
final export itself retains the asset values.
|
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
|
- 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
|
has `clip_radius=6` and `max_clip_level=8`. Runtime Core mode intentionally does not read the old
|
||||||
6, the desired set had 6,498 tiles. The exact expected 2,226-tile hash set uses max level 8, so the
|
asset clip values; its own defaults are radius 3 and max level 8. The adapter parity test preserves
|
||||||
validation copy records that override explicitly.
|
the source export and applies those reference view values only to a separate validation copy.
|
||||||
- Core schema and mesh output use 25 cm per voxel. The existing plugin Runtime defines
|
- In the owner's Unreal editor, select the `AVoxelWorld` actor and enable **Voxel World | Core | Use
|
||||||
`VOXEL_SIZE=12.5 cm`. The runtime adapter must define and verify the scale conversion before Core
|
VoxelForge Core** in Details. Leave **Core Clip Radius = 3** and **Core Max Clip Level = 8** for the
|
||||||
vertices enter `FVoxelMeshData`.
|
first check. Save the level only if the owner wants to keep the actor setting. With the world
|
||||||
- The BuildHost commandlet warns that `/Game/VoxelForge/Blocks/NewDataAsset` is missing. It did not
|
running, expect Core terrain to stream around the player, solid LOD0 terrain to block movement,
|
||||||
prevent schema resolution, Core hashes, or walkability; any content depending on that reference
|
and the existing carve entry points to create a deterministic hole. Disable the switch to return
|
||||||
still needs owner-side asset review.
|
to the legacy route.
|
||||||
- Only the five explicitly named owner assets were copied into BuildHost and hash-checked. No owner
|
- BuildHost UBT compiled the current runtime and editor modules. If the owner's editor reports a
|
||||||
editor was opened, and no owner project build or asset write was performed.
|
compile error, the likely integration seams to inspect first are the new reflected properties in
|
||||||
- Runtime work remains: add the default-false `AVoxelWorld` switch; create/lifetime-manage a Core
|
`VoxelWorld.h` (UHT), the Core module dependency in `VoxelForge.Build.cs`, the Core/RealtimeMesh
|
||||||
`World`; schedule compile and `generateTile` on plugin workers; convert Core mesh groups into the
|
bridge in `VoxelWorld.cpp`, and the editor-only commandlet code. No owner-editor build was run.
|
||||||
existing payload; carry and check `Epoch`; upload with current RealtimeMesh collision; drain
|
- The commandlet warns that `/Game/VoxelForge/Blocks/NewDataAsset` is missing. `/Game` is project
|
||||||
in-flight Core calls before `applyEditTransaction`; regenerate returned keys under a new epoch.
|
content, and this asset is not part of the plugin; the staged copied content also lacks that
|
||||||
- The hand-off should wait until the runtime path and a headless initial-clipmap/carve proof exist.
|
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
|
## Failed runs, retries, and crashes
|
||||||
|
|
||||||
No run exceeded the 15-minute limit. No process returned `0xC0000142`, so no automatic retry was
|
No build or test exceeded the 15-minute limit. No process returned `0xC0000142`, so no automatic
|
||||||
used. No new `UECC-*` folder appeared; the only folder under
|
retry was used. One new `UECC-Windows-7B23387E4235BCFEDE42BC9D637E41CB_0000` was written under
|
||||||
`Saved/BuildHost/HostProject/Saved/Crashes/` is the pre-existing
|
`Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/User/Saved/Crashes/`;
|
||||||
`UECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000` dated 2026-09-17.
|
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:
|
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. |
|
| 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. |
|
| 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. |
|
| 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
|
The standalone viewer build failure is separate from Unreal integration. The final adapter and actor
|
||||||
final commandlet tested current Core sources and matched the preserved standalone reference hashes
|
passes match preserved standalone reference hashes; the Core worker-limit 1/8/16 adapter proof also
|
||||||
at all three compile worker limits.
|
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
|
The owner can now try the switch described above. BuildHost proves Core tile parity, expected
|
||||||
name the `AVoxelWorld` property, keep it false for the current route, and describe the Core route's
|
collision-body bookkeeping, anchor entering/leaving, and transaction-based carving. The remaining
|
||||||
visual, collision, carving, and streaming behavior after a validated BuildHost proof.
|
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.
|
||||||
Reference in new issue
Block a user