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
+2069
-118
No files matched your search
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user