test(reach): automation test guarding the per-tile reach proofs; merge closed as load-bearing
VoxelForge.Correctness.TilePostReachProof (VoxelForgeTilePostReachTest.cpp) runs the real mesher twice in one process: at full reach it compares the skipped posts' FINAL field (356,946 comparisons, 0 differences), and at a deliberately shrunk reach it must find differences (774 found). Native/fused sample counts are identical (137,180). So a future tunnel or passage geometry change that breaks a reach proof (ce4d878,a668833) fails loudly instead of silently making holes. VoxelGenLOD::ResetTilePostReachDiagnostics resets every per-kind counter between the two runs, so the proof cannot pass vacuously. Test fixture fix: the strate connectivity test's density-sign/coarse-lie controls use boxes up to 256 voxels, which the DiffLayer budget added in8879045rejected. The fixture now uses an unlimited budget; the test measures the field, not the gameplay brush. The tunnel SDF/core merge was attempted and NOT shipped (Codex; reviewed by me): - The world-space core is load-bearing. WorldControlPoints keep the authored room-floor anchors and walkable tunnel routes before cave warp (8e42d6d, against warped floors becoming vertical breaks), andcf2b2admade the world chain authoritative for option B winding/ledge walkability. EvaluateTunnelCoreWorld also supplies the support/room-floor ownership the tail uses. - Replacing the world shape: player-fit 20,830 -> 19,684, walk 10,909 -> 9,934, components 261 -> 220. Raw world evaluation without floor handling also failed (20,348 / 10,713): a coordinate-frame / junction mismatch, not only floor projection. - The exact shared-segment variant was SLOWER than the committed tip: static worker +5.6%, moving +12.6%, LOD0 p95 +6% / +13%. - A 0.0001-0.01% world-chain blend that merely passed the gate was rejected as cosmetic. All merge source was reverted. This build is bit-identical to the committed tip (311CA42C, 75,482 triangles, capability 20,830 / 10,909); connectivity 6/6 + 16/16, passage open-space 14/14 and sealed proof 22/22 pass. Every launch was serialized. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y
This commit is contained in:
1 parent
cfd81b5ce9
commit
8ef1a3b579
3 files changed
+270
No files matched your search
@@ -1387,6 +1387,12 @@ bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The two density-sign/coarse-lie controls below deliberately use boxes up to 256 voxels
|
||||||
|
// wide. That is larger than the player-facing DiffLayer default radius cap; this test is
|
||||||
|
// validating the measurement field, not the gameplay brush budget. Keep the fixture budget
|
||||||
|
// unlimited so a rejected control cannot masquerade as a density or connectivity failure.
|
||||||
|
World.DiffLayer->SetBudget(/*MaxMods=*/0, /*MaxRadius=*/0.0f, /*MaxVolume=*/0.0f);
|
||||||
|
|
||||||
FVoxelStrateMeasureSettings Settings;
|
FVoxelStrateMeasureSettings Settings;
|
||||||
Settings.SampleStep = 4;
|
Settings.SampleStep = 4;
|
||||||
Settings.RadiusInVoxels = 256;
|
Settings.RadiusInVoxels = 256;
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
// The tile-reach proof must fail loudly when its envelope is deliberately made too small.
|
||||||
|
|
||||||
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "HAL/IConsoleManager.h"
|
||||||
|
|
||||||
|
#include "VoxelForgeTestFixture.h"
|
||||||
|
#include "VoxelMarchingCubesMesher.h"
|
||||||
|
#include "../../Public/VoxelTilePostReach.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using namespace VoxelForgeTest;
|
||||||
|
|
||||||
|
struct FScopedReachTestCVars
|
||||||
|
{
|
||||||
|
IConsoleVariable* Debug = nullptr;
|
||||||
|
IConsoleVariable* Scale = nullptr;
|
||||||
|
int32 OldDebug = 0;
|
||||||
|
float OldScale = 1.0f;
|
||||||
|
bool bValid = false;
|
||||||
|
|
||||||
|
FScopedReachTestCVars()
|
||||||
|
{
|
||||||
|
IConsoleManager& Console = IConsoleManager::Get();
|
||||||
|
Debug = Console.FindConsoleVariable(TEXT("voxel.TilePostReachDebug"));
|
||||||
|
Scale = Console.FindConsoleVariable(TEXT("voxel.TilePostReachScale"));
|
||||||
|
if (Debug == nullptr || Scale == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OldDebug = Debug->GetInt();
|
||||||
|
OldScale = Scale->GetFloat();
|
||||||
|
bValid = true;
|
||||||
|
Debug->Set(1, ECVF_SetByCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
~FScopedReachTestCVars()
|
||||||
|
{
|
||||||
|
if (!bValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Debug->Set(OldDebug, ECVF_SetByCode);
|
||||||
|
Scale->Set(OldScale, ECVF_SetByCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetScale(float InScale)
|
||||||
|
{
|
||||||
|
if (Scale != nullptr)
|
||||||
|
{
|
||||||
|
Scale->Set(InScale, ECVF_SetByCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FReachSnapshot
|
||||||
|
{
|
||||||
|
uint64 Tiles = 0;
|
||||||
|
uint64 FinalComparisons = 0;
|
||||||
|
uint64 FinalDifferences = 0;
|
||||||
|
uint64 AllComparisons = 0;
|
||||||
|
uint64 AllDifferences = 0;
|
||||||
|
int64 DensitySamples = 0;
|
||||||
|
int32 MeshedTiles = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
FReachSnapshot RunReachTiles(
|
||||||
|
UVoxelMarchingCubesMesher& Mesher,
|
||||||
|
const TArray<FIntVector>& Origins)
|
||||||
|
{
|
||||||
|
FReachSnapshot Out;
|
||||||
|
const uint64 TileCountBefore =
|
||||||
|
VoxelGenLOD::GTilePostReachTileCount.load(std::memory_order_relaxed);
|
||||||
|
for (const FIntVector& Origin : Origins)
|
||||||
|
{
|
||||||
|
int64 Samples = 0;
|
||||||
|
Mesher.GenerateMesh(
|
||||||
|
Origin, 8, 16, nullptr, INT32_MIN, INT32_MAX, &Samples);
|
||||||
|
Out.DensitySamples += Samples;
|
||||||
|
Out.MeshedTiles += Samples > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint64 TileCountAfter =
|
||||||
|
VoxelGenLOD::GTilePostReachTileCount.load(std::memory_order_relaxed);
|
||||||
|
Out.Tiles = TileCountAfter - TileCountBefore;
|
||||||
|
Out.FinalComparisons = VoxelGenLOD::GSkippedPostComparisonsByKind[
|
||||||
|
static_cast<uint8>(VoxelGenLOD::ETilePostComparisonKind::FinalField)]
|
||||||
|
.load(std::memory_order_relaxed);
|
||||||
|
Out.FinalDifferences = VoxelGenLOD::GSkippedPostDifferencesByKind[
|
||||||
|
static_cast<uint8>(VoxelGenLOD::ETilePostComparisonKind::FinalField)]
|
||||||
|
.load(std::memory_order_relaxed);
|
||||||
|
Out.AllComparisons = VoxelGenLOD::GSkippedPostComparisons.load(
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
Out.AllDifferences = VoxelGenLOD::GSkippedPostDifferences.load(
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
return Out;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 FloorAlign(int32 Value, int32 Alignment)
|
||||||
|
{
|
||||||
|
return FMath::FloorToInt(static_cast<float>(Value) / static_cast<float>(Alignment))
|
||||||
|
* Alignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildReachTileOrigins(
|
||||||
|
const FTestWorld& World,
|
||||||
|
int32 Slot,
|
||||||
|
TArray<FIntVector>& OutOrigins)
|
||||||
|
{
|
||||||
|
int32 SlotTop = 0;
|
||||||
|
int32 SlotBottom = 0;
|
||||||
|
if (!World.GetSlotVoxelZRange(Slot, SlotTop, SlotBottom))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int32 TileExtent = 16 * 8;
|
||||||
|
const int32 TileZ = FloorAlign(SlotBottom, TileExtent);
|
||||||
|
// Nine XY tiles around the origin cover the guaranteed spine, winding tunnel mouths,
|
||||||
|
// and feature-free graph regions. Include one distant tile to prove the skip side is
|
||||||
|
// exercised rather than relying only on near-tunnel tiles.
|
||||||
|
for (int32 Y = -TileExtent; Y <= TileExtent; Y += TileExtent)
|
||||||
|
{
|
||||||
|
for (int32 X = -TileExtent; X <= TileExtent; X += TileExtent)
|
||||||
|
{
|
||||||
|
OutOrigins.Add(FIntVector(X, Y, TileZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutOrigins.Add(FIntVector(4 * TileExtent, -3 * TileExtent, TileZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendWorldReachTiles(
|
||||||
|
const FTestWorld& World,
|
||||||
|
TArray<FIntVector>& OutOrigins)
|
||||||
|
{
|
||||||
|
BuildReachTileOrigins(World, FTestWorld::SlotTunnelNetwork, OutOrigins);
|
||||||
|
BuildReachTileOrigins(World, FTestWorld::SlotUnderwater, OutOrigins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FVoxelForgeTilePostReachProofTest,
|
||||||
|
"VoxelForge.Correctness.TilePostReachProof",
|
||||||
|
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FVoxelForgeTilePostReachProofTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
FScopedReachTestCVars CVarGuard;
|
||||||
|
if (!CVarGuard.bValid)
|
||||||
|
{
|
||||||
|
AddError(TEXT("Tile-post reach test could not find its two console variables."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FTestWorld NativeWorld;
|
||||||
|
NativeWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
|
||||||
|
FTestWorld FusedWorld;
|
||||||
|
FusedWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
|
||||||
|
if (!NativeWorld.IsValid() || !FusedWorld.IsValid())
|
||||||
|
{
|
||||||
|
AddError(NativeWorld.IsValid() && FusedWorld.IsValid()
|
||||||
|
? TEXT("Unexpected invalid reach-proof fixture")
|
||||||
|
: (!NativeWorld.IsValid() ? NativeWorld.WhyInvalid() : FusedWorld.WhyInvalid()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FIntVector> NativeOrigins;
|
||||||
|
TArray<FIntVector> FusedOrigins;
|
||||||
|
AppendWorldReachTiles(NativeWorld, NativeOrigins);
|
||||||
|
AppendWorldReachTiles(FusedWorld, FusedOrigins);
|
||||||
|
TestTrue(TEXT("the representative native tile set is non-empty"), NativeOrigins.Num() > 0);
|
||||||
|
TestTrue(TEXT("the representative fused tile set is non-empty"), FusedOrigins.Num() > 0);
|
||||||
|
|
||||||
|
TStrongObjectPtr<UVoxelMarchingCubesMesher> NativeMesher(
|
||||||
|
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||||
|
TStrongObjectPtr<UVoxelMarchingCubesMesher> FusedMesher(
|
||||||
|
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||||
|
NativeMesher->SetGenerator(NativeWorld.Generator.Get());
|
||||||
|
FusedMesher->SetGenerator(FusedWorld.Generator.Get());
|
||||||
|
|
||||||
|
VoxelGenLOD::ResetTilePostReachDiagnostics();
|
||||||
|
CVarGuard.SetScale(1.0f);
|
||||||
|
const FReachSnapshot NativeFull = RunReachTiles(*NativeMesher, NativeOrigins);
|
||||||
|
const FReachSnapshot FusedFull = RunReachTiles(*FusedMesher, FusedOrigins);
|
||||||
|
const uint64 FullFinalComparisons = NativeFull.FinalComparisons + FusedFull.FinalComparisons;
|
||||||
|
const uint64 FullFinalDifferences = NativeFull.FinalDifferences + FusedFull.FinalDifferences;
|
||||||
|
TestTrue(TEXT("normal reach compares final fields on the representative tiles"),
|
||||||
|
FullFinalComparisons > 0);
|
||||||
|
TestEqual(TEXT("normal reach has no final-field differences"), FullFinalDifferences, 0ull);
|
||||||
|
TestEqual(TEXT("normal reach has no skipped-post differences"),
|
||||||
|
NativeFull.AllDifferences + FusedFull.AllDifferences, 0ull);
|
||||||
|
|
||||||
|
VoxelGenLOD::ResetTilePostReachDiagnostics();
|
||||||
|
// Use a deliberately tiny but valid scale so at least one envelope is rejected while the same
|
||||||
|
// final-field samples remain exercised; this is the negative control, not a supported setting.
|
||||||
|
CVarGuard.SetScale(0.01f);
|
||||||
|
const FReachSnapshot NativeShrunk = RunReachTiles(*NativeMesher, NativeOrigins);
|
||||||
|
const FReachSnapshot FusedShrunk = RunReachTiles(*FusedMesher, FusedOrigins);
|
||||||
|
const uint64 ShrunkFinalComparisons =
|
||||||
|
NativeShrunk.FinalComparisons + FusedShrunk.FinalComparisons;
|
||||||
|
const uint64 ShrunkFinalDifferences =
|
||||||
|
NativeShrunk.FinalDifferences + FusedShrunk.FinalDifferences;
|
||||||
|
TestTrue(TEXT("shrunk reach still executes final-field comparisons"),
|
||||||
|
ShrunkFinalComparisons > 0);
|
||||||
|
TestTrue(TEXT("shrunk reach demonstrably fails the final-field proof"),
|
||||||
|
ShrunkFinalDifferences > 0);
|
||||||
|
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("tile_post_reach_proof: native_tiles=%llu fused_tiles=%llu full_final=%llu/%llu "
|
||||||
|
"shrunk_final=%llu/%llu native_samples=%lld/%lld fused_samples=%lld/%lld."),
|
||||||
|
static_cast<unsigned long long>(NativeFull.Tiles),
|
||||||
|
static_cast<unsigned long long>(FusedFull.Tiles),
|
||||||
|
static_cast<unsigned long long>(FullFinalComparisons),
|
||||||
|
static_cast<unsigned long long>(FullFinalDifferences),
|
||||||
|
static_cast<unsigned long long>(ShrunkFinalComparisons),
|
||||||
|
static_cast<unsigned long long>(ShrunkFinalDifferences),
|
||||||
|
static_cast<long long>(NativeFull.DensitySamples),
|
||||||
|
static_cast<long long>(NativeShrunk.DensitySamples),
|
||||||
|
static_cast<long long>(FusedFull.DensitySamples),
|
||||||
|
static_cast<long long>(FusedShrunk.DensitySamples)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||||
@@ -129,6 +129,44 @@ namespace VoxelGenLOD
|
|||||||
inline std::atomic<uint64> GTileReachBlockCostCycles[
|
inline std::atomic<uint64> GTileReachBlockCostCycles[
|
||||||
static_cast<uint8>(ETileReachCostKind::Count)][2]{};
|
static_cast<uint8>(ETileReachCostKind::Count)][2]{};
|
||||||
|
|
||||||
|
// Automation tests deliberately run the real mesher twice (full reach and a shrunken reach)
|
||||||
|
// in one process. Keep the reset in the reach owner so a test cannot accidentally omit one
|
||||||
|
// of the per-kind counters and accept a vacuous proof.
|
||||||
|
FORCEINLINE void ResetTilePostReachDiagnostics()
|
||||||
|
{
|
||||||
|
GTilePostReachDebugEnabled.store(false, std::memory_order_relaxed);
|
||||||
|
GTilePostReachTileCount.store(0, std::memory_order_relaxed);
|
||||||
|
GOriginLandingReachableTiles.store(0, std::memory_order_relaxed);
|
||||||
|
GPassageLandingReachableTiles.store(0, std::memory_order_relaxed);
|
||||||
|
GPassageStructuralReachableTiles.store(0, std::memory_order_relaxed);
|
||||||
|
GOriginLandingSkippedTiles.store(0, std::memory_order_relaxed);
|
||||||
|
GPassageLandingSkippedTiles.store(0, std::memory_order_relaxed);
|
||||||
|
GPassageStructuralSkippedTiles.store(0, std::memory_order_relaxed);
|
||||||
|
GTunnelCoreReachableTiles.store(0, std::memory_order_relaxed);
|
||||||
|
GTunnelCoreSkippedTiles.store(0, std::memory_order_relaxed);
|
||||||
|
GPassageCarvingReachableTiles.store(0, std::memory_order_relaxed);
|
||||||
|
GPassageCarvingSkippedTiles.store(0, std::memory_order_relaxed);
|
||||||
|
GSkippedPostComparisons.store(0, std::memory_order_relaxed);
|
||||||
|
GSkippedPostDifferences.store(0, std::memory_order_relaxed);
|
||||||
|
for (uint8 Index = 0;
|
||||||
|
Index < static_cast<uint8>(ETilePostComparisonKind::Count); ++Index)
|
||||||
|
{
|
||||||
|
GSkippedPostComparisonsByKind[Index].store(0, std::memory_order_relaxed);
|
||||||
|
GSkippedPostDifferencesByKind[Index].store(0, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
GTileReachCostDiagnosticsEnabled.store(false, std::memory_order_relaxed);
|
||||||
|
for (uint8 Kind = 0; Kind < static_cast<uint8>(ETileReachCostKind::Count); ++Kind)
|
||||||
|
{
|
||||||
|
for (uint8 Reach = 0; Reach < 2; ++Reach)
|
||||||
|
{
|
||||||
|
GTileReachCostCalls[Kind][Reach].store(0, std::memory_order_relaxed);
|
||||||
|
GTileReachCostCycles[Kind][Reach].store(0, std::memory_order_relaxed);
|
||||||
|
GTileReachBlockCostCalls[Kind][Reach].store(0, std::memory_order_relaxed);
|
||||||
|
GTileReachBlockCostCycles[Kind][Reach].store(0, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FORCEINLINE bool IsTilePostReachable(uint8 Bit)
|
FORCEINLINE bool IsTilePostReachable(uint8 Bit)
|
||||||
{
|
{
|
||||||
return (TilePostReachFlags & Bit) != 0;
|
return (TilePostReachFlags & Bit) != 0;
|
||||||
|
|||||||
Reference in new issue
Block a user