From 8ef1a3b579ad65462d8948f2a944ddba8dd59e5e Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Tue, 15 Sep 2026 17:42:25 +0200 Subject: [PATCH] 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 in 8879045 rejected. 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), and cf2b2ad made 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 Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y --- .../VoxelForgeStrateConnectivityTest.cpp | 6 + .../Tests/VoxelForgeTilePostReachTest.cpp | 226 ++++++++++++++++++ Source/VoxelForge/Public/VoxelTilePostReach.h | 38 +++ 3 files changed, 270 insertions(+) create mode 100644 Source/VoxelForge/Private/Tests/VoxelForgeTilePostReachTest.cpp diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp index 3877e42..707aa5b 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp @@ -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; diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeTilePostReachTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeTilePostReachTest.cpp new file mode 100644 index 0000000..d909cf2 --- /dev/null +++ b/Source/VoxelForge/Private/Tests/VoxelForgeTilePostReachTest.cpp @@ -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& 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(VoxelGenLOD::ETilePostComparisonKind::FinalField)] + .load(std::memory_order_relaxed); + Out.FinalDifferences = VoxelGenLOD::GSkippedPostDifferencesByKind[ + static_cast(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(Value) / static_cast(Alignment)) + * Alignment; + } + + void BuildReachTileOrigins( + const FTestWorld& World, + int32 Slot, + TArray& 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& 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 NativeOrigins; + TArray 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 NativeMesher( + NewObject(GetTransientPackage(), NAME_None, RF_Transient)); + TStrongObjectPtr FusedMesher( + NewObject(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(NativeFull.Tiles), + static_cast(FusedFull.Tiles), + static_cast(FullFinalComparisons), + static_cast(FullFinalDifferences), + static_cast(ShrunkFinalComparisons), + static_cast(ShrunkFinalDifferences), + static_cast(NativeFull.DensitySamples), + static_cast(NativeShrunk.DensitySamples), + static_cast(FusedFull.DensitySamples), + static_cast(FusedShrunk.DensitySamples))); + return true; +} + +#endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Public/VoxelTilePostReach.h b/Source/VoxelForge/Public/VoxelTilePostReach.h index c2d0e60..f2acbe9 100644 --- a/Source/VoxelForge/Public/VoxelTilePostReach.h +++ b/Source/VoxelForge/Public/VoxelTilePostReach.h @@ -129,6 +129,44 @@ namespace VoxelGenLOD inline std::atomic GTileReachBlockCostCycles[ static_cast(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(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(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;