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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
Settings.SampleStep = 4;
|
||||
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[
|
||||
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)
|
||||
{
|
||||
return (TilePostReachFlags & Bit) != 0;
|
||||
|
||||
Reference in new issue
Block a user