Files
VoxelForge/Source/VoxelForge/Private/Tests/VoxelForgeLayoutOrderIndependenceTest.cpp
T
Fr0zkaandClaude Opus 5.5 fc9227f9e0 fix(unreal): U1 -- Unreal compiles again on L61; CoreGeometry asset section; versioned layout-unit upgrade
Windows MSVC catch-up after abyss L15-L61 (no Unreal there): 64-bit clipmap
shift (C4334), qualified VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, UHT
metadata and duplicate-signature fixes. Optional CoreGeometry section on
UVoxelStrateDefinition (native caves, lattice, profiled heightfields, named
stamps, rooms/tunnels; off/empty by default), resolved after passage planning.
VoxelForgeAssetVersion 2 converts strate height/transition/gap to metres on
load (exact, 0 m change on the owner's 5 assets; packages never saved).
Gates pass (SurfaceWorld, FloatingIslands, Core-off, M2, W1/W8/W16).
KNOWN REGRESSION vs L14 (2026-10-20, same command, same PC): full set 3.9 ->
5.37 s, worker-seconds 15.8 -> 30, LOD0 1.11 -> 1.69 s, ring 0.50 -> 0.80 s;
not yet attributed (Core L15-L61 vs Unreal side).
Report: Standalone/Research/WorldAssemblerM3/Unreal1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:12:08 +01:00

482 lines
19 KiB
C++

// VoxelForgeLayoutOrderIndependenceTest.cpp
// Test de déterminisme du layout et des passages quand l'ordre du pool change.
// Determinism test for the layout and passages when the pool order changes.
#if WITH_DEV_AUTOMATION_TESTS
#include "Misc/AutomationTest.h"
#include "VoxelForgeTestFixture.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgeLayoutOrderIndependenceTest,
"VoxelForge.Determinism.LayoutOrderIndependence",
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
namespace
{
constexpr int32 LayoutSeed = 20260829;
constexpr int32 NumPoolDefinitions = 5;
struct FLayoutSnapshot
{
TArray<FStrateSlot> Layout;
TArray<FVoxelPassage> Passages;
};
/**
* Small headless world whose pool has no FixedStrates entries. That is important: the test
* must exercise the shuffled pool path rather than accidentally testing only pinned slots.
*
* Petit monde sans entrée FixedStrates. C'est essentiel : le test doit exercer le chemin du
* pool mélangé, et ne pas tester par accident uniquement des slots épinglés.
*/
struct FLayoutTestWorld
{
TStrongObjectPtr<UVoxelSettings> Settings;
TStrongObjectPtr<UVoxelStrateManager> StrateManager;
TArray<TStrongObjectPtr<UVoxelStrateDefinition>> Definitions;
void Build()
{
Settings = TStrongObjectPtr<UVoxelSettings>(
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
Settings->Seed = LayoutSeed;
Settings->TotalStrates = NumPoolDefinitions;
Settings->InterStrateGapChunks = 1;
Settings->InterStrateGapMeters = Settings->StrateLayoutChunkSizeMeters;
Settings->bOpenSurfaceEntry = true;
static const ECaveGeneratorType GeneratorTypes[] = {
ECaveGeneratorType::TunnelNetwork,
ECaveGeneratorType::FlatPlain,
ECaveGeneratorType::Maze,
ECaveGeneratorType::SurfaceWorld,
ECaveGeneratorType::VerticalShafts,
};
static const EVoxelPassageStyle PassageStyles[] = {
EVoxelPassageStyle::Straight,
EVoxelPassageStyle::Worm,
EVoxelPassageStyle::Spiral,
EVoxelPassageStyle::Cascading,
EVoxelPassageStyle::Worm,
};
for (int32 Index = 0; Index < NumPoolDefinitions; ++Index)
{
UVoxelStrateDefinition* Def = NewObject<UVoxelStrateDefinition>(
GetTransientPackage(), NAME_None, RF_Transient);
Def->StrateName = FText::FromString(
FString::Printf(TEXT("Layout order test strate %d"), Index));
Def->GeneratorType = GeneratorTypes[Index];
Def->StrateHeightInChunks = 3 + Index;
Def->StrateHeightMeters = static_cast<double>(Def->StrateHeightInChunks)
* Settings->StrateLayoutChunkSizeMeters;
Def->TransitionType = EVoxelStrateTransition::Hard;
// Non-trivial, distinct passage configs exercise the connection index and the
// shape seeds while keeping every generated passage observable in the snapshot.
// Des configs distinctes rendent visibles l'index de connexion et les seeds de
// forme, tout en gardant chaque passage généré observable dans le snapshot.
FStratePassageConfig& Passage = Def->PassageConfig;
Passage.Connections = Index + 1;
Passage.Style = PassageStyles[Index];
Passage.MouthRadius = 5.0f + (float)Index;
Passage.MidRadius = 3.0f + (float)Index * 0.5f;
Passage.ReachMin = 12.0f + (float)Index * 3.0f;
Passage.ReachMax = 48.0f + (float)Index * 4.0f;
Passage.DistanceMin = 20.0f + (float)Index * 5.0f;
Passage.DistanceMax = 80.0f + (float)Index * 7.0f;
Passage.Wander = 20.0f + (float)Index;
Passage.Segments = 4 + Index;
Passage.VerticalWobble = 4.0f;
Passage.SpiralRadius = 10.0f + (float)Index;
Passage.SpiralTurns = 1.0f + (float)Index * 0.25f;
Passage.CascadeSteps = 2 + Index;
Passage.CascadeLedge = 10.0f + (float)Index;
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Def));
}
StrateManager = TStrongObjectPtr<UVoxelStrateManager>(
NewObject<UVoxelStrateManager>(GetTransientPackage(), NAME_None, RF_Transient));
}
void SetPoolOrder(const TArray<int32>& Order)
{
Settings->StratePool.Reset(Order.Num());
for (const int32 DefinitionIndex : Order)
{
Settings->StratePool.Add(
TSoftObjectPtr<UVoxelStrateDefinition>(Definitions[DefinitionIndex].Get()));
}
}
bool Rebuild(const TArray<int32>& Order)
{
if (!Settings.IsValid() || !StrateManager.IsValid())
{
return false;
}
SetPoolOrder(Order);
StrateManager->Initialize(Settings.Get(), LayoutSeed);
return true;
}
};
FLayoutSnapshot CaptureSnapshot(const UVoxelStrateManager& Manager)
{
FLayoutSnapshot Snapshot;
Snapshot.Layout = Manager.GetLayout();
Snapshot.Passages = Manager.GetPassages();
return Snapshot;
}
bool SameVectorBits(const FVector& A, const FVector& B)
{
return VoxelForgeTest::BitEqual(A.X, B.X)
&& VoxelForgeTest::BitEqual(A.Y, B.Y)
&& VoxelForgeTest::BitEqual(A.Z, B.Z);
}
bool SameLanding(const FVoxelPassageLanding& A, const FVoxelPassageLanding& B)
{
return SameVectorBits(A.StandingPoint, B.StandingPoint)
&& SameVectorBits(A.DoorPoint, B.DoorPoint)
&& SameVectorBits(A.DoorDirection, B.DoorDirection)
&& VoxelForgeTest::BitEqual(A.FloorZ, B.FloorZ)
&& VoxelForgeTest::BitEqual(A.CeilingZ, B.CeilingZ)
&& VoxelForgeTest::BitEqual(A.HalfWidth, B.HalfWidth)
&& VoxelForgeTest::BitEqual(A.FloorThickness, B.FloorThickness)
&& A.bSourcePlayerFit == B.bSourcePlayerFit;
}
bool SamePassage(const FVoxelPassage& A, const FVoxelPassage& B, FString& OutMismatch,
int32 PassageIndex)
{
if (A.UpperStrateIndex != B.UpperStrateIndex || A.LowerStrateIndex != B.LowerStrateIndex)
{
OutMismatch = FString::Printf(
TEXT("passage %d strate indices differ (%d/%d vs %d/%d)"), PassageIndex,
A.UpperStrateIndex, A.LowerStrateIndex, B.UpperStrateIndex, B.LowerStrateIndex);
return false;
}
if (!SameVectorBits(A.UpperPoint, B.UpperPoint)
|| !SameVectorBits(A.LowerPoint, B.LowerPoint))
{
OutMismatch = FString::Printf(
TEXT("passage %d endpoints differ"), PassageIndex);
return false;
}
if (!SameLanding(A.UpperLanding, B.UpperLanding)
|| !SameLanding(A.LowerLanding, B.LowerLanding))
{
OutMismatch = FString::Printf(
TEXT("passage %d landing descriptors differ"), PassageIndex);
return false;
}
if (!VoxelForgeTest::BitEqual(A.Radius, B.Radius))
{
OutMismatch = FString::Printf(
TEXT("passage %d radius differs (%.9g vs %.9g)"), PassageIndex, A.Radius, B.Radius);
return false;
}
if (A.PassageType != B.PassageType)
{
OutMismatch = FString::Printf(
TEXT("passage %d type differs (%d vs %d)"), PassageIndex,
(int32)A.PassageType, (int32)B.PassageType);
return false;
}
if (A.ControlPoints.Num() != B.ControlPoints.Num())
{
OutMismatch = FString::Printf(
TEXT("passage %d control-point count differs (%d vs %d)"), PassageIndex,
A.ControlPoints.Num(), B.ControlPoints.Num());
return false;
}
for (int32 PointIndex = 0; PointIndex < A.ControlPoints.Num(); ++PointIndex)
{
if (!SameVectorBits(A.ControlPoints[PointIndex], B.ControlPoints[PointIndex]))
{
OutMismatch = FString::Printf(
TEXT("passage %d control point %d differs"), PassageIndex, PointIndex);
return false;
}
}
if (A.ControlRadii.Num() != B.ControlRadii.Num())
{
OutMismatch = FString::Printf(
TEXT("passage %d control-radius count differs (%d vs %d)"), PassageIndex,
A.ControlRadii.Num(), B.ControlRadii.Num());
return false;
}
for (int32 RadiusIndex = 0; RadiusIndex < A.ControlRadii.Num(); ++RadiusIndex)
{
if (!VoxelForgeTest::BitEqual(A.ControlRadii[RadiusIndex], B.ControlRadii[RadiusIndex]))
{
OutMismatch = FString::Printf(
TEXT("passage %d control radius %d differs"), PassageIndex, RadiusIndex);
return false;
}
}
if (!SameVectorBits(A.BoundCenter, B.BoundCenter)
|| !VoxelForgeTest::BitEqual(A.BoundRadius, B.BoundRadius)
|| !VoxelForgeTest::BitEqual(A.BoundRadiusSq, B.BoundRadiusSq))
{
OutMismatch = FString::Printf(
TEXT("passage %d bounding sphere differs"), PassageIndex);
return false;
}
return true;
}
bool SameLayout(const TArray<FStrateSlot>& Expected, const TArray<FStrateSlot>& Actual,
FString& OutMismatch)
{
if (Expected.Num() != Actual.Num())
{
OutMismatch = FString::Printf(
TEXT("layout slot count differs (%d vs %d)"), Expected.Num(), Actual.Num());
return false;
}
for (int32 SlotIndex = 0; SlotIndex < Expected.Num(); ++SlotIndex)
{
const FStrateSlot& A = Expected[SlotIndex];
const FStrateSlot& B = Actual[SlotIndex];
if (A.Definition != B.Definition || A.StrateIndex != B.StrateIndex)
{
OutMismatch = FString::Printf(
TEXT("slot %d definition or strate index differs"), SlotIndex);
return false;
}
if (A.TopChunkZ != B.TopChunkZ || A.BottomChunkZ != B.BottomChunkZ
|| A.HeightInChunks != B.HeightInChunks)
{
OutMismatch = FString::Printf(
TEXT("slot %d Z/height differs (top/bottom/height %d/%d/%d vs %d/%d/%d)"),
SlotIndex, A.TopChunkZ, A.BottomChunkZ, A.HeightInChunks,
B.TopChunkZ, B.BottomChunkZ, B.HeightInChunks);
return false;
}
}
return true;
}
bool SameSnapshot(const FLayoutSnapshot& Expected, const FLayoutSnapshot& Actual,
FString& OutMismatch)
{
if (!SameLayout(Expected.Layout, Actual.Layout, OutMismatch))
{
return false;
}
if (Expected.Passages.Num() != Actual.Passages.Num())
{
OutMismatch = FString::Printf(
TEXT("passage count differs (%d vs %d)"),
Expected.Passages.Num(), Actual.Passages.Num());
return false;
}
for (int32 PassageIndex = 0; PassageIndex < Expected.Passages.Num(); ++PassageIndex)
{
if (!SamePassage(Expected.Passages[PassageIndex], Actual.Passages[PassageIndex],
OutMismatch, PassageIndex))
{
return false;
}
}
return true;
}
}
bool FVoxelForgeLayoutOrderIndependenceTest::RunTest(const FString& Parameters)
{
FLayoutTestWorld World;
World.Build();
TArray<int32> OriginalOrder;
for (int32 Index = 0; Index < NumPoolDefinitions; ++Index)
{
OriginalOrder.Add(Index);
}
TArray<int32> ReversedOrder;
for (int32 Index = NumPoolDefinitions - 1; Index >= 0; --Index)
{
ReversedOrder.Add(Index);
}
TArray<int32> PermutedOrder = OriginalOrder;
PermutedOrder.Swap(0, 2);
TArray<TArray<int32>> Orders;
Orders.Add(OriginalOrder);
Orders.Add(ReversedOrder);
Orders.Add(PermutedOrder);
static const TCHAR* OrderNames[] = {
TEXT("original"),
TEXT("reversal"),
TEXT("swap"),
};
TArray<FLayoutSnapshot> Snapshots;
Snapshots.Reserve(Orders.Num());
for (int32 OrderIndex = 0; OrderIndex < Orders.Num(); ++OrderIndex)
{
const bool bRebuilt = World.Rebuild(Orders[OrderIndex]);
const int32 NumStrates = World.StrateManager.IsValid()
? World.StrateManager->GetNumStrates()
: 0;
if (!bRebuilt || NumStrates != NumPoolDefinitions)
{
AddError(FString::Printf(
TEXT("Permutation '%s' could not build a strate layout at all. The test cannot ")
TEXT("silently pass with an incomplete layout; expected %d resolved slots, got %d. ")
TEXT("Check transient soft-pointer resolution."),
OrderNames[OrderIndex], NumPoolDefinitions, NumStrates));
return false;
}
Snapshots.Add(CaptureSnapshot(*World.StrateManager.Get()));
}
if (Snapshots.Num() == 0 || Snapshots[0].Layout.Num() == 0)
{
AddError(TEXT("No layout snapshot was inspected. This test would otherwise pass without ")
TEXT("checking any slots."));
return false;
}
if (Snapshots[0].Passages.Num() == 0)
{
AddError(TEXT("The known pool generated no passages, so the passage invariant was not ")
TEXT("actually inspected."));
return false;
}
int32 NumSlotsInspected = 0;
int32 NumPassagesInspected = 0;
for (int32 SnapshotIndex = 1; SnapshotIndex < Snapshots.Num(); ++SnapshotIndex)
{
NumSlotsInspected += Snapshots[0].Layout.Num();
NumPassagesInspected += Snapshots[0].Passages.Num();
FString Mismatch;
if (!SameSnapshot(Snapshots[0], Snapshots[SnapshotIndex], Mismatch))
{
AddError(FString::Printf(
TEXT("Pool order changed the built result for permutation '%s': %s"),
OrderNames[SnapshotIndex], *Mismatch));
}
}
// A pool permutation alone cannot expose the old sequential passage stream once the layout
// itself is stable. Perturb one early connection count and verify that every later boundary
// keeps its own (strate index, connection index) values unchanged.
// Une permutation seule ne peut plus révéler l'ancien stream séquentiel une fois le layout
// stable. On perturbe donc une connexion précoce et on vérifie toutes les frontières suivantes.
UVoxelStrateDefinition* EarlyDefinition = Snapshots[0].Layout[0].Definition;
if (!EarlyDefinition)
{
AddError(TEXT("The reference layout has a null definition in slot 0; no passage ")
TEXT("independence check can be performed."));
return false;
}
const int32 OriginalConnections = EarlyDefinition->PassageConfig.Connections;
EarlyDefinition->PassageConfig.Connections = OriginalConnections + 1;
const bool bPerturbedRebuild = World.Rebuild(OriginalOrder);
const int32 NumPerturbedStrates = World.StrateManager.IsValid()
? World.StrateManager->GetNumStrates()
: 0;
if (!bPerturbedRebuild || NumPerturbedStrates != NumPoolDefinitions)
{
AddError(TEXT("Changing the early connection count prevented the known layout from ")
TEXT("rebuilding; the passage-independence check could not run."));
EarlyDefinition->PassageConfig.Connections = OriginalConnections;
return false;
}
const FLayoutSnapshot Perturbed = CaptureSnapshot(*World.StrateManager.Get());
EarlyDefinition->PassageConfig.Connections = OriginalConnections;
FString LayoutMismatch;
if (!SameLayout(Snapshots[0].Layout, Perturbed.Layout, LayoutMismatch))
{
AddError(FString::Printf(
TEXT("Changing only the early connection count changed the strate layout: %s"),
*LayoutMismatch));
}
for (int32 BoundaryIndex = 1; BoundaryIndex < Snapshots[0].Layout.Num() - 1; ++BoundaryIndex)
{
TArray<const FVoxelPassage*> ReferenceBoundary;
TArray<const FVoxelPassage*> PerturbedBoundary;
for (const FVoxelPassage& Passage : Snapshots[0].Passages)
{
if (Passage.UpperStrateIndex == BoundaryIndex)
{
ReferenceBoundary.Add(&Passage);
}
}
for (const FVoxelPassage& Passage : Perturbed.Passages)
{
if (Passage.UpperStrateIndex == BoundaryIndex)
{
PerturbedBoundary.Add(&Passage);
}
}
++NumPassagesInspected;
if (ReferenceBoundary.Num() != PerturbedBoundary.Num())
{
AddError(FString::Printf(
TEXT("Boundary %d passage count changed after an earlier connection count ")
TEXT("was perturbed (%d vs %d)."),
BoundaryIndex, ReferenceBoundary.Num(), PerturbedBoundary.Num()));
continue;
}
for (int32 PassageIndex = 0; PassageIndex < ReferenceBoundary.Num(); ++PassageIndex)
{
FString Mismatch;
if (!SamePassage(*ReferenceBoundary[PassageIndex], *PerturbedBoundary[PassageIndex],
Mismatch, PassageIndex))
{
AddError(FString::Printf(
TEXT("Passage at boundary %d changed when an earlier connection count was ")
TEXT("perturbed: %s"),
BoundaryIndex, *Mismatch));
}
}
}
if (NumSlotsInspected == 0 || NumPassagesInspected == 0)
{
AddError(FString::Printf(
TEXT("The test inspected no comparable data (slots=%d, passages=%d); this is a ")
TEXT("test-harness failure, not a determinism pass."),
NumSlotsInspected, NumPassagesInspected));
return false;
}
AddInfo(FString::Printf(
TEXT("Compared %d layout slots and %d passages across %d pool orders (seed=%d)."),
NumSlotsInspected, NumPassagesInspected, Snapshots.Num(), LayoutSeed));
return true;
}
#endif // WITH_DEV_AUTOMATION_TESTS