From 39c306b524ae3a3e8007af43ce331a8b72bad7ce Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 14 Sep 2026 22:08:24 +0200 Subject: [PATCH] perf(streaming): prove sealed rock solid instead of sampling it -- crossing fix's cost recovered 4c8d06f stopped content-cutting LOD0 so the player can cross strates, at a cost of +29% static worker CPU: LOD0 tiles in the sealed rock between strates, which the old cut skipped by accident, were sampled in full and found solid. This proves those tiles solid instead (Codex; reviewed by me). - UVoxelGenerator::TryProveSealedSolidTile: AllSolid only for a tile whose whole lattice lies in a gap/seal region. It rejects player mods (HasAnyModInChunkRange), passages plus carve pads, the origin spine/landing, disturbances, recipes and overrides. It is narrower than the old ClassifyTile, which stays off in streaming. CollisionSolidTiles / CollisionNotRequiredTiles bookkeeping is preserved, so the collision gate's support search still resolves. - TryProveOutOfLayoutAirTile: a separate fail-closed constant-air proof for the zero-triangle tiles outside the strate layout. It never supplies collision support. - voxel.SealedSolidProof (default 1). Mining a proven tile invalidates and re-meshes it. - A soundness test that can fail (VoxelForgeSealedSolidProofTest): 22 candidates, 22 skips, 0 non-solid lattice samples; a deliberately weakened diff guard produces 437 non-solid samples and fails. Verified: mining tile (14,-18,-8) went from proof=1 / 0 triangles to proof=0 / 8 triangles and re-meshed; strate-crossing test PASS (max gate 0.098 s); off/on trace 899/899 identical; export b3e5f4c3..9b377; capability 20,830 / 10,909. Two interleaved rounds, serialized (owner-editor count 0 before each launch): baseline crossing-fixed + proof static worker 35.5 s 41.9 s 36.0 s (+1.4% vs baseline) moving worker 78.1 s 93.2 s 78.2 s (+0.1%) mesher calls static 18.078M 22.280M 18.078M Applied triangles identical: 669,834 static / 1,331,482 moving. Runs: one early automation probe exited 3 with no log (the corrected harness passed); one exploratory moving run was interrupted and excluded. Note: HasAnyModInChunkRange scans every modified chunk per candidate (cold audit #21). That is cheap today (~20 candidates), but it grows with mining; it is queued in the correctness round. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y --- .../Tests/VoxelForgeSealedSolidProofTest.cpp | 179 +++++ Source/VoxelForge/Private/VoxelGenerator.cpp | 641 +++++++++++++++++- .../VoxelForge/Private/VoxelStartupTrace.cpp | 5 +- Source/VoxelForge/Private/VoxelWorld.cpp | 208 +++++- Source/VoxelForge/Public/VoxelGenerator.h | 40 ++ Source/VoxelForge/Public/VoxelStartupTrace.h | 1 + Source/VoxelForge/Public/VoxelWorld.h | 8 + 7 files changed, 1067 insertions(+), 15 deletions(-) create mode 100644 Source/VoxelForge/Private/Tests/VoxelForgeSealedSolidProofTest.cpp diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeSealedSolidProofTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeSealedSolidProofTest.cpp new file mode 100644 index 0000000..abed1f4 --- /dev/null +++ b/Source/VoxelForge/Private/Tests/VoxelForgeSealedSolidProofTest.cpp @@ -0,0 +1,179 @@ +// Sealed-solid proof soundness and invalidation. +// +// The production shortcut is intentionally one-sided: it may return false too often, but a true +// result skips both meshing and level-0 collision generation. This test therefore brute-forces +// every tile that the fixture presents to the proof and treats any non-solid lattice sample as a +// hard failure. The second half deliberately disables the diff guard; a carve must then make the +// weakened proof look true while the same oracle finds an air sample. + +#if WITH_DEV_AUTOMATION_TESTS + +#include "Misc/AutomationTest.h" + +#include "VoxelForgeTestFixture.h" + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeSealedSolidProofTest, + "VoxelForge.Determinism.SealedSolidProofSoundness", + EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) + +namespace +{ + struct FTileSpec + { + FIntVector Origin = FIntVector::ZeroValue; + int32 Step = 1; + int32 Cells = CHUNK_SIZE; + }; + + int32 CountNonSolidSamples(const UVoxelGenerator& Generator, const FTileSpec& Tile) + { + const int32 Cells = FMath::Clamp(Tile.Cells, 2, CHUNK_SIZE); + int32 NonSolid = 0; + for (int32 Z = 0; Z <= Cells; ++Z) + for (int32 Y = 0; Y <= Cells; ++Y) + for (int32 X = 0; X <= Cells; ++X) + { + const float Density = Generator.GetDensityAt( + static_cast(Tile.Origin.X + X * Tile.Step), + static_cast(Tile.Origin.Y + Y * Tile.Step), + static_cast(Tile.Origin.Z + Z * Tile.Step)); + if (!VoxelMath::IsFinite(Density) || !(Density < 0.0f)) + { + ++NonSolid; + } + } + return NonSolid; + } + + void SetSealThickness(UVoxelStrateDefinition& Definition, float Thickness) + { + Definition.GenerationParams.BoundarySealThickness = Thickness; + Definition.SlabParams.BoundarySealThickness = Thickness; + Definition.MazeParams.BoundarySealThickness = Thickness; + Definition.SurfaceParams.BoundarySealThickness = Thickness; + Definition.VerticalShaftParams.BoundarySealThickness = Thickness; + Definition.FloatingIslandParams.BoundarySealThickness = Thickness; + } +} + +bool FVoxelForgeSealedSolidProofTest::RunTest(const FString& Parameters) +{ + using namespace VoxelForgeTest; + + // Forty voxels makes a complete 32-cell level-0 tile fit inside a bottom boundary seal. The + // production asset remains untouched; this is a transient proof-only fixture configuration. + FTestWorld World; + World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false, + /*InStrateHeightInChunks*/8); + if (!World.IsValid()) + { + AddError(World.WhyInvalid()); + return false; + } + for (const TStrongObjectPtr& Definition : World.Definitions) + { + if (Definition.IsValid()) + { + SetSealThickness(*Definition.Get(), 40.0f); + } + } + World.Reinitialize(); + + const UVoxelGenerator* Generator = World.Generator.Get(); + const FIntVector FarXY(4096, 4096, 0); + TArray CandidateTiles; + + // Enumerate every gap chunk and every strate's bottom-seal tile in this fixture. No sample + // is selected after looking at the proof result: all candidates are offered to it first. + for (int32 ChunkZ = World.BottomChunkZ; ChunkZ <= World.TopChunkZ; ++ChunkZ) + { + if (World.StrateManager->IsGapChunk(FIntVector(0, 0, ChunkZ))) + { + CandidateTiles.Add({FIntVector(FarXY.X, FarXY.Y, ChunkZ * CHUNK_SIZE), 1, CHUNK_SIZE}); + } + } + for (const FStrateSlot& Slot : World.StrateManager->GetLayout()) + { + CandidateTiles.Add({ + FIntVector(FarXY.X, FarXY.Y, Slot.BottomChunkZ * CHUNK_SIZE), + 1, CHUNK_SIZE}); + } + + int32 NumProofSkips = 0; + int32 NumNonSolidSamples = 0; + int32 FirstProofTile = INDEX_NONE; + for (int32 Index = 0; Index < CandidateTiles.Num(); ++Index) + { + const FTileSpec& Tile = CandidateTiles[Index]; + if (!Generator->TryProveSealedSolidTile(Tile.Origin, Tile.Step, Tile.Cells)) + { + continue; + } + ++NumProofSkips; + if (FirstProofTile == INDEX_NONE) + { + FirstProofTile = Index; + } + const int32 TileNonSolid = CountNonSolidSamples(*Generator, Tile); + NumNonSolidSamples += TileNonSolid; + if (TileNonSolid != 0) + { + AddError(FString::Printf( + TEXT("FALSE ALL-SOLID PROOF: candidate tile (%d,%d,%d) returned true but has " + "%d non-solid core-lattice samples."), + Tile.Origin.X, Tile.Origin.Y, Tile.Origin.Z, TileNonSolid)); + } + } + + AddInfo(FString::Printf( + TEXT("Sealed-solid proof brute force: %d candidate tiles, %d proof skips, %d non-solid " + "core-lattice samples."), + CandidateTiles.Num(), NumProofSkips, NumNonSolidSamples)); + TestTrue(TEXT("the fixture exercised at least one sealed-solid proof skip"), + NumProofSkips > 0); + TestEqual(TEXT("every skipped tile has zero non-solid core-lattice samples"), + NumNonSolidSamples, 0); + if (FirstProofTile == INDEX_NONE) + { + return false; + } + + // A carve in the proven region is the live-edit case that must invalidate the proof. The + // diff layer stores the modification in the tile's chunk, so this is also the exact guard used + // by the worker before it decides to skip meshing. + const FTileSpec& ProofTile = CandidateTiles[FirstProofTile]; + const FVector CarveCenter( + static_cast(ProofTile.Origin.X + 16), + static_cast(ProofTile.Origin.Y + 16), + static_cast(ProofTile.Origin.Z + 16)); + FVoxelModification Carve; + Carve.Center = CarveCenter; + Carve.Radius = 6.0f; + Carve.Strength = -64.0f; + Carve.Shape = EVoxelBrushShape::Sphere; + TestTrue(TEXT("the adversarial carve was registered"), + World.DiffLayer->ApplyModification(Carve).Num() > 0); + + TestFalse(TEXT("a player carve kills the sealed-solid proof"), + Generator->TryProveSealedSolidTile( + ProofTile.Origin, ProofTile.Step, ProofTile.Cells)); + + const uint8 DiffGuardDisabled = static_cast( + EVoxelSealedSolidProofGuard::DiffLayer); + TestTrue(TEXT("the deliberately weakened proof still exposes the geometric candidate"), + Generator->TryProveSealedSolidTileForTest( + ProofTile.Origin, ProofTile.Step, ProofTile.Cells, DiffGuardDisabled)); + + const int32 WeakenedNonSolidSamples = CountNonSolidSamples(*Generator, ProofTile); + AddInfo(FString::Printf( + TEXT("Weakened proof (diff guard disabled): %d non-solid core-lattice samples after the " + "carve."), + WeakenedNonSolidSamples)); + TestTrue(TEXT("the brute-force oracle fails the deliberately weakened guard"), + WeakenedNonSolidSamples > 0); + + return true; +} + +#endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 0f48ce7..a2841d6 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -1600,8 +1600,624 @@ static FORCEINLINE bool VF_AnyDisturbanceCanTouchBox( || VF_AnyRidgeCanTouchBox(Box, D, Seed); } +static FORCEINLINE bool VF_SealedSolidProofGuardDisabled( + uint8 DisabledGuards, EVoxelSealedSolidProofGuard Guard) +{ + return (DisabledGuards & static_cast(Guard)) != 0; +} + +static FORCEINLINE bool VF_IsSafeBoundarySealLatticeZ( + float WorldZ, float StrateTopZ, float StrateBottomZ, + float SealThickness, float BaseDensity) +{ + // ApplyBoundarySeal is a positive internal-density lower bound. At an exact lattice point, + // the original inequalities are sufficient: a strictly positive seal factor times a finite, + // positive base density makes the final MC density negative. Do not widen the band here; + // false Mixed costs time, while the brute-force test below is the authority for this shortcut. + if (!VoxelMath::IsFinite(WorldZ) + || !VoxelMath::IsFinite(StrateTopZ) + || !VoxelMath::IsFinite(StrateBottomZ) + || !VoxelMath::IsFinite(SealThickness) + || !VoxelMath::IsFinite(BaseDensity) + || !(StrateTopZ > StrateBottomZ) + || !(SealThickness > 0.0f) + || !(BaseDensity > 0.0f)) + { + return false; + } + + const float DistTop = StrateTopZ - WorldZ; + const float DistBottom = WorldZ - StrateBottomZ; + return (DistTop >= 0.0f && DistTop < SealThickness) + || (DistBottom >= 0.0f && DistBottom < SealThickness); +} + +static bool VF_SealedSolidProofPassageInputsAreValid(const UVoxelStrateManager& Manager) +{ + // AnyPassageNearBox is intentionally a cheap bounding-sphere test. Its normal runtime inputs + // are generated data, but a malformed bound must fail closed: NaN distance comparisons would + // otherwise turn an unknown passage into a false "no passage" result. + for (const FVoxelPassage& Passage : Manager.GetPassages()) + { + if (!VoxelMath::IsFinite(Passage.BoundCenter.X) + || !VoxelMath::IsFinite(Passage.BoundCenter.Y) + || !VoxelMath::IsFinite(Passage.BoundCenter.Z) + || !VoxelMath::IsFinite(Passage.BoundRadius) + || Passage.BoundRadius < 0.0f + || !VoxelMath::IsFinite(Passage.Radius) + || Passage.Radius < 0.0f) + { + return false; + } + for (const FVector& Point : Passage.ControlPoints) + { + if (!VoxelMath::IsFinite(Point.X) + || !VoxelMath::IsFinite(Point.Y) + || !VoxelMath::IsFinite(Point.Z)) + { + return false; + } + } + for (const float Radius : Passage.ControlRadii) + { + if (!VoxelMath::IsFinite(Radius) || Radius < 0.0f) + { + return false; + } + } + + const FVoxelPassageLanding* Landings[] = { + &Passage.UpperLanding, &Passage.LowerLanding }; + for (const FVoxelPassageLanding* Landing : Landings) + { + if (!VoxelMath::IsFinite(Landing->StandingPoint.X) + || !VoxelMath::IsFinite(Landing->StandingPoint.Y) + || !VoxelMath::IsFinite(Landing->StandingPoint.Z) + || !VoxelMath::IsFinite(Landing->DoorPoint.X) + || !VoxelMath::IsFinite(Landing->DoorPoint.Y) + || !VoxelMath::IsFinite(Landing->DoorPoint.Z) + || !VoxelMath::IsFinite(Landing->FloorZ) + || !VoxelMath::IsFinite(Landing->CeilingZ) + || !VoxelMath::IsFinite(Landing->HalfWidth) + || !VoxelMath::IsFinite(Landing->FloorThickness) + || Landing->HalfWidth < 0.0f + || Landing->FloorThickness < 0.0f) + { + return false; + } + } + + if (Passage.bWalkableTunnelContract + && (Passage.ControlPoints.Num() < 2 + || Passage.ControlRadii.Num() != Passage.ControlPoints.Num())) + { + return false; + } + } + return true; +} + +static bool VF_TryProveSealedSolidTile( + const UVoxelGenerator& Generator, const FIntVector& OriginVoxels, + int32 Step, int32 CellsPerAxis, uint8 DisabledGuards) +{ + const UVoxelStrateManager* Manager = Generator.StrateManager; + if (Manager == nullptr) + { + return false; + } + + Step = FMath::Max(1, Step); + const int32 CPA = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE); + + // Keep the integer arithmetic that defines the MC lattice identical to ClassifyTile, but + // reject a caller whose endpoint would overflow before any floor division or FBox creation. + const int64 MaxX64 = static_cast(OriginVoxels.X) + + static_cast(CPA) * static_cast(Step); + const int64 MaxY64 = static_cast(OriginVoxels.Y) + + static_cast(CPA) * static_cast(Step); + const int64 MaxZ64 = static_cast(OriginVoxels.Z) + + static_cast(CPA) * static_cast(Step); + if (MaxX64 < MIN_int32 || MaxX64 > MAX_int32 + || MaxY64 < MIN_int32 || MaxY64 > MAX_int32 + || MaxZ64 < MIN_int32 || MaxZ64 > MAX_int32) + { + return false; + } + + const int32 MinX = OriginVoxels.X; + const int32 MinY = OriginVoxels.Y; + const int32 MinZ = OriginVoxels.Z; + const int32 MaxX = static_cast(MaxX64); + const int32 MaxY = static_cast(MaxY64); + const int32 MaxZ = static_cast(MaxZ64); + const FBox TileVoxelBox( + FVector(static_cast(MinX), static_cast(MinY), static_cast(MinZ)), + FVector(static_cast(MaxX), static_cast(MaxY), static_cast(MaxZ))); + + auto FloorDivC = [](int32 A, int32 B) -> int32 + { + const int32 Q = A / B; + const int32 R = A % B; + return (R != 0 && ((R < 0) != (B < 0))) ? Q - 1 : Q; + }; + + // First pass is deliberately Z-only. Most streaming tiles are neither gap nor seal, so they + // leave in this cheap loop without taking a diff lock, scanning passages, or copying params. + enum : uint8 { GapRow = 1u, SealRow = 2u }; + int32 SampleChunkZ[CHUNK_SIZE + 1]; + uint8 SampleKind[CHUNK_SIZE + 1]; + bool bSawSeal = false; + bool bSawSolidRegion = false; + for (int32 G = 0; G <= CPA; ++G) + { + const int64 Z64 = static_cast(OriginVoxels.Z) + + static_cast(G) * static_cast(Step); + const int32 Z = static_cast(Z64); + const int32 ChunkZ = FloorDivC(Z, CHUNK_SIZE); + SampleChunkZ[G] = ChunkZ; + if (Manager->IsGapChunk(FIntVector(0, 0, ChunkZ))) + { + SampleKind[G] = GapRow; + bSawSolidRegion = true; + continue; + } + + int32 UnusedTopChunkZ = 0; + int32 UnusedBottomChunkZ = 0; + if (!Manager->GetStrateChunkZBounds( + ChunkZ, UnusedTopChunkZ, UnusedBottomChunkZ)) + { + // Above/below the stack is open air, not a solid gap. A tile that touches it is not + // a sealed-solid candidate even if another row happens to be bedrock. + return false; + } + SampleKind[G] = SealRow; + bSawSeal = true; + bSawSolidRegion = true; + } + if (!bSawSolidRegion) + { + return false; + } + if (bSawSeal && VoxelDensityAblation::IsBoundarySealOff()) + { + return false; + } + + // Player edits are the final writer and can carve a tile that was structurally solid. This + // is intentionally checked only after the Z pre-pass: ordinary mixed tiles avoid the lock. + const int32 MinChunkX = FloorDivC(MinX, CHUNK_SIZE); + const int32 MaxChunkX = FloorDivC(MaxX, CHUNK_SIZE); + const int32 MinChunkY = FloorDivC(MinY, CHUNK_SIZE); + const int32 MaxChunkY = FloorDivC(MaxY, CHUNK_SIZE); + const int32 MinChunkZ = FloorDivC(MinZ, CHUNK_SIZE); + const int32 MaxChunkZ = FloorDivC(MaxZ, CHUNK_SIZE); + if (!VF_SealedSolidProofGuardDisabled( + DisabledGuards, EVoxelSealedSolidProofGuard::DiffLayer) + && Generator.DiffLayer != nullptr + && Generator.DiffLayer->HasAnyMods()) + { + if (Generator.DiffLayer->HasAnyModInChunkRange( + FIntVector(MinChunkX, MinChunkY, MinChunkZ), + FIntVector(MaxChunkX, MaxChunkY, MaxChunkZ))) + { + return false; + } + } + + // PassageCarving's public box guard includes the 4-voxel carve blend pad on top of each + // generated passage bound. Landing and structural-post air are also handled conservatively: + // if carving is ablated but another passage air writer remains active, reject any passage data + // rather than trusting an API whose historical early-out is specific to carving. + if (!VF_SealedSolidProofGuardDisabled( + DisabledGuards, EVoxelSealedSolidProofGuard::Passages)) + { + const bool bPassageAirWriterActive = + !VoxelDensityAblation::IsPassageCarvingOff() + || !VoxelDensityAblation::IsLandingPostsOff() + || !VoxelDensityAblation::IsPassageStructuralPostsOff(); + if (bPassageAirWriterActive) + { + if (!VF_SealedSolidProofPassageInputsAreValid(*Manager)) + { + return false; + } + if (!VoxelDensityAblation::IsPassageCarvingOff()) + { + if (Manager->AnyPassageNearBox( + TileVoxelBox.Min, TileVoxelBox.Max)) + { + return false; + } + } + else if (Manager->GetPassages().Num() > 0) + { + // AnyPassageNearBox deliberately returns false when the carve stage is off. The + // remaining landing/tunnel posts have no equivalent box API, so this is the + // capability-first answer under that development ablation. + return false; + } + } + } + + // The origin landing is the (0,0) spine's current room-shaped air writer. Its box guard is + // broader than the exact lattice guard used by ClassifyTile; false Mixed is cheap here. + if (!VF_SealedSolidProofGuardDisabled( + DisabledGuards, EVoxelSealedSolidProofGuard::OriginLanding) + && (!VoxelDensityAblation::IsOriginSpineOff() + || !VoxelDensityAblation::IsLandingPostsOff()) + && Manager->AnyOriginLandingNearBox(TileVoxelBox.Min, TileVoxelBox.Max)) + { + return false; + } + + const int64 XYChunkCount = + (static_cast(MaxChunkX) - static_cast(MinChunkX) + 1) + * (static_cast(MaxChunkY) - static_cast(MinChunkY) + 1); + const int64 ZChunkCount = static_cast(MaxChunkZ) - static_cast(MinChunkZ) + 1; + // This is a per-tile proof, not a second coarse mesher. A normal LOD0/LOD1 tile touches at + // most a handful of chunks. Refuse pathological caller boxes before a nested loop can turn + // the cheap path into a large param walk. + constexpr int64 MaxProofChunkCoordinates = 65536; + if (XYChunkCount <= 0 || ZChunkCount <= 0 + || XYChunkCount > MaxProofChunkCoordinates + || XYChunkCount * ZChunkCount > MaxProofChunkCoordinates) + { + return false; + } + + auto ResolveSealFields = [&](const FIntVector& ChunkCoord, + float& OutTop, float& OutBottom, + float& OutThickness, float& OutBase) -> bool + { + switch (Manager->GetGeneratorTypeForChunk(ChunkCoord)) + { + case ECaveGeneratorType::FlatPlain: + case ECaveGeneratorType::CrystalChamber: + { + const FSlabGenerationParams P = Manager->GetSlabParamsForChunk(ChunkCoord); + OutTop = P.StrateTopWorldZ; + OutBottom = P.StrateBottomWorldZ; + OutThickness = P.BoundarySealThickness; + OutBase = P.BaseDensity; + return true; + } + case ECaveGeneratorType::Maze: + { + const FMazeGenerationParams P = Manager->GetMazeParamsForChunk(ChunkCoord); + OutTop = P.StrateTopWorldZ; + OutBottom = P.StrateBottomWorldZ; + OutThickness = P.BoundarySealThickness; + OutBase = P.BaseDensity; + return true; + } + case ECaveGeneratorType::SurfaceWorld: + { + const FSurfaceGenerationParams P = Manager->GetSurfaceParamsForChunk(ChunkCoord); + OutTop = P.StrateTopWorldZ; + OutBottom = P.StrateBottomWorldZ; + OutThickness = P.BoundarySealThickness; + OutBase = P.BaseDensity; + return true; + } + case ECaveGeneratorType::VerticalShafts: + { + const FVerticalShaftParams P = Manager->GetVerticalShaftParamsForChunk(ChunkCoord); + OutTop = P.StrateTopWorldZ; + OutBottom = P.StrateBottomWorldZ; + OutThickness = P.BoundarySealThickness; + OutBase = P.BaseDensity; + return true; + } + case ECaveGeneratorType::FloatingIslands: + { + const FFloatingIslandParams P = Manager->GetFloatingIslandParamsForChunk(ChunkCoord); + OutTop = P.StrateTopWorldZ; + OutBottom = P.StrateBottomWorldZ; + OutThickness = P.BoundarySealThickness; + OutBase = P.BaseDensity; + return true; + } + case ECaveGeneratorType::TunnelNetwork: + case ECaveGeneratorType::Underwater: + { + const FStrateGenerationParams P = Manager->GetGenerationParams(ChunkCoord); + OutTop = P.StrateTopWorldZ; + OutBottom = P.StrateBottomWorldZ; + OutThickness = P.BoundarySealThickness; + OutBase = P.BaseDensity; + return true; + } + default: + return false; + } + }; + + for (int32 ChunkZ = MinChunkZ; ChunkZ <= MaxChunkZ; ++ChunkZ) + { + bool bThisChunkHasSealRow = false; + for (int32 G = 0; G <= CPA; ++G) + { + if (SampleKind[G] == SealRow && SampleChunkZ[G] == ChunkZ) + { + bThisChunkHasSealRow = true; + break; + } + } + if (!bThisChunkHasSealRow) + { + continue; + } + + for (int32 ChunkY = MinChunkY; ChunkY <= MaxChunkY; ++ChunkY) + for (int32 ChunkX = MinChunkX; ChunkX <= MaxChunkX; ++ChunkX) + { + const FIntVector ChunkCoord(ChunkX, ChunkY, ChunkZ); + +#if WITH_EDITOR + // A plain composer override changes the archetype/params, while a region override + // changes the lateral field. Both must remain on the ordinary mesher/classifier path. + int32 ComposerSeed = 0; + ECaveGeneratorType ComposerArchetype = ECaveGeneratorType::TunnelNetwork; + FVoxelStrateArchetypeParams ComposerParams; + bool bComposerRecipe = false; + FVoxelOpStackRecipe ComposerRecipe; + if (Manager->GetComposerOverrideForChunk( + ChunkCoord, ComposerSeed, ComposerArchetype, ComposerParams, + bComposerRecipe, ComposerRecipe)) + { + return false; + } + FVoxelStrateRegionManifest ComposerRegions; + if (Manager->GetComposerRegionOverrideForChunk(ChunkCoord, ComposerRegions)) + { + return false; + } +#endif + + // Cooked seasons can materialise a custom recipe for a chunk. The cheap proof only + // knows the native structural seal contract, so selected recipes fail closed. + int32 RecipeSeed = 0; + ECaveGeneratorType RecipeArchetype = ECaveGeneratorType::TunnelNetwork; + FVoxelStrateArchetypeParams RecipeParams; + FVoxelOpStackRecipe Recipe; + if (Manager->GetRecipeForChunk( + ChunkCoord, RecipeSeed, RecipeArchetype, RecipeParams, Recipe)) + { + return false; + } + + float Top = 0.0f; + float Bottom = 0.0f; + float Thickness = 0.0f; + float BaseDensity = 0.0f; + if (!ResolveSealFields(ChunkCoord, Top, Bottom, Thickness, BaseDensity)) + { + return false; + } + + if (!VF_SealedSolidProofGuardDisabled( + DisabledGuards, EVoxelSealedSolidProofGuard::DisturbanceChasms) + && !VoxelDensityAblation::IsDisturbancesOff()) + { + const FStrateDisturbanceParams D = + Manager->GetDisturbanceParamsForChunk(ChunkCoord); + if (VF_AnyChasmCanTouchBox(TileVoxelBox, D, static_cast(Generator.Seed))) + { + return false; + } + } + + for (int32 G = 0; G <= CPA; ++G) + { + if (SampleKind[G] != SealRow || SampleChunkZ[G] != ChunkZ) + { + continue; + } + const float Z = static_cast( + static_cast(OriginVoxels.Z) + + static_cast(G) * static_cast(Step)); + if (!VF_IsSafeBoundarySealLatticeZ( + Z, Top, Bottom, Thickness, BaseDensity)) + { + return false; + } + } + } + } + + return true; +} + +static bool VF_TryProveOutOfLayoutAirTile( + const UVoxelGenerator& Generator, const FIntVector& OriginVoxels, + int32 Step, int32 CellsPerAxis) +{ + const UVoxelStrateManager* Manager = Generator.StrateManager; + if (Manager == nullptr) + { + return false; + } + + Step = FMath::Max(1, Step); + const int32 CPA = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE); + + const int64 MaxX64 = static_cast(OriginVoxels.X) + + static_cast(CPA) * static_cast(Step); + const int64 MaxY64 = static_cast(OriginVoxels.Y) + + static_cast(CPA) * static_cast(Step); + const int64 MaxZ64 = static_cast(OriginVoxels.Z) + + static_cast(CPA) * static_cast(Step); + if (MaxX64 < MIN_int32 || MaxX64 > MAX_int32 + || MaxY64 < MIN_int32 || MaxY64 > MAX_int32 + || MaxZ64 < MIN_int32 || MaxZ64 > MAX_int32) + { + return false; + } + + const int32 MinX = OriginVoxels.X; + const int32 MinY = OriginVoxels.Y; + const int32 MinZ = OriginVoxels.Z; + const int32 MaxX = static_cast(MaxX64); + const int32 MaxY = static_cast(MaxY64); + const int32 MaxZ = static_cast(MaxZ64); + const FBox TileVoxelBox( + FVector(static_cast(MinX), static_cast(MinY), static_cast(MinZ)), + FVector(static_cast(MaxX), static_cast(MaxY), static_cast(MaxZ))); + + auto FloorDivC = [](int32 A, int32 B) -> int32 + { + const int32 Q = A / B; + const int32 R = A % B; + return (R != 0 && ((R < 0) != (B < 0))) ? Q - 1 : Q; + }; + + // All core lattice rows must be outside the layout. A gap or a real strate row means that + // this is no longer the constant-air case, even when another row is open air. + for (int32 G = 0; G <= CPA; ++G) + { + const int64 Z64 = static_cast(OriginVoxels.Z) + + static_cast(G) * static_cast(Step); + const int32 ChunkZ = FloorDivC(static_cast(Z64), CHUNK_SIZE); + if (Manager->IsGapChunk(FIntVector(0, 0, ChunkZ))) + { + return false; + } + int32 UnusedTopChunkZ = 0; + int32 UnusedBottomChunkZ = 0; + if (Manager->GetStrateChunkZBounds( + ChunkZ, UnusedTopChunkZ, UnusedBottomChunkZ)) + { + return false; + } + } + + const int32 MinChunkX = FloorDivC(MinX, CHUNK_SIZE); + const int32 MaxChunkX = FloorDivC(MaxX, CHUNK_SIZE); + const int32 MinChunkY = FloorDivC(MinY, CHUNK_SIZE); + const int32 MaxChunkY = FloorDivC(MaxY, CHUNK_SIZE); + const int32 MinChunkZ = FloorDivC(MinZ, CHUNK_SIZE); + const int32 MaxChunkZ = FloorDivC(MaxZ, CHUNK_SIZE); + if (Generator.DiffLayer != nullptr + && Generator.DiffLayer->HasAnyMods() + && Generator.DiffLayer->HasAnyModInChunkRange( + FIntVector(MinChunkX, MinChunkY, MinChunkZ), + FIntVector(MaxChunkX, MaxChunkY, MaxChunkZ))) + { + return false; + } + + // The final XY pass can add rock to open air. Radius == 0 (and other non-positive inputs) is + // the documented no-op, so only validate and inspect the band when that writer is live. + if (!VoxelDensityAblation::IsXYEdgeSealOff() + && Generator.WorldRadiusVoxels > 0.0f + && Generator.EdgeSealThickness > 0.0f) + { + if (!VF_IsValidXYEdgeSealProofInput( + TileVoxelBox, Generator.WorldRadiusVoxels, + Generator.EdgeSealThickness, 1.0f)) + { + return false; + } + + // Use double arithmetic here: the production helper is float-based, while this proof + // must not turn a large finite coordinate into an overflowed squared-distance false + // negative. Equality is safe because the production inner test is <= as well. + const double FarthestX = FMath::Max( + FMath::Abs(static_cast(TileVoxelBox.Min.X)), + FMath::Abs(static_cast(TileVoxelBox.Max.X))); + const double FarthestY = FMath::Max( + FMath::Abs(static_cast(TileVoxelBox.Min.Y)), + FMath::Abs(static_cast(TileVoxelBox.Max.Y))); + const double InnerRadius = static_cast(Generator.WorldRadiusVoxels) + - static_cast(Generator.EdgeSealThickness); + if (!(InnerRadius > 0.0) + || FarthestX * FarthestX + FarthestY * FarthestY + > InnerRadius * InnerRadius) + { + return false; + } + } + + if (Manager->GetPassages().Num() > 0) + { + if (!VF_SealedSolidProofPassageInputsAreValid(*Manager)) + { + return false; + } + + // Passage carving is air-monotone and therefore cannot invalidate an AllAir proof. Only + // the solid floor/support writers need a guard here. Use the same exact lattice floor + // predicate as ClassifyTile: its padded AABBs stay cheap, but do not reject an open-air + // tile merely because a harmless carve bound passes near it. + const bool bPassageFloorWriterActive = + !VoxelDensityAblation::IsPassageStructuralPostsOff() + || !VoxelDensityAblation::IsNativeFloorOff() + || !VoxelDensityAblation::IsLandingPostsOff(); + if (bPassageFloorWriterActive + && Manager->AnyPassageLandingFloorNearLattice( + TileVoxelBox, OriginVoxels, Step)) + { + return false; + } + } + + // Origin rooms only carve open air, but their landing floors reassert solid. The manager's + // floor box includes every per-strate room; this is intentionally broader than the exact + // lattice query used by ClassifyTile because a false Mixed is cheaper than a dropped floor. + if (!VoxelDensityAblation::IsOriginSpineOff() + || !VoxelDensityAblation::IsLandingPostsOff()) + { + if (!VoxelMath::IsFinite(Generator.OriginSpineRadius) + || Generator.OriginSpineRadius <= 0.0f) + { + return false; + } + if (Manager->AnyOriginLandingFloorNearLattice( + TileVoxelBox, OriginVoxels, Step)) + { + return false; + } + } + + if (!VoxelDensityAblation::IsDisturbancesOff()) + { + const int64 XYChunkCount = + (static_cast(MaxChunkX) - static_cast(MinChunkX) + 1) + * (static_cast(MaxChunkY) - static_cast(MinChunkY) + 1); + const int64 ZChunkCount = + static_cast(MaxChunkZ) - static_cast(MinChunkZ) + 1; + constexpr int64 MaxProofChunkCoordinates = 65536; + if (XYChunkCount <= 0 || ZChunkCount <= 0 + || XYChunkCount > MaxProofChunkCoordinates + || XYChunkCount * ZChunkCount > MaxProofChunkCoordinates) + { + return false; + } + + // Out-of-layout chunks normally return disabled disturbance params. Still ask the exact + // bridge/ridge predicates for every touched chunk so a future out-of-layout decoration + // cannot silently turn this air proof into an invisible solid patch. + for (int32 ChunkZ = MinChunkZ; ChunkZ <= MaxChunkZ; ++ChunkZ) + for (int32 ChunkY = MinChunkY; ChunkY <= MaxChunkY; ++ChunkY) + for (int32 ChunkX = MinChunkX; ChunkX <= MaxChunkX; ++ChunkX) + { + const FStrateDisturbanceParams D = + Manager->GetDisturbanceParamsForChunk(FIntVector(ChunkX, ChunkY, ChunkZ)); + if (VF_AnyBridgeCanTouchBox(TileVoxelBox, D, static_cast(Generator.Seed)) + || VF_AnyRidgeCanTouchBox(TileVoxelBox, D, static_cast(Generator.Seed))) + { + return false; + } + } + } + + return true; +} + //============================================================================= -// LE MAPPING « ARCHÉTYPE → PILE D'OPÉRATEURS » — UNE SEULE DÉFINITION +// LE MAPPING « ARCHÉTYPE → PILE D’OPÉRATEURS » — UNE SEULE DÉFINITION //============================================================================= // ⚠️ EXTRAIT DE `GetDensityAt` PARCE QUE `ClassifyTile` EN A BESOIN AUSSI, ET QU'UNE DEUXIÈME COPIE // SERAIT LA PIRE FORME DE BUG DISPONIBLE ICI. @@ -5713,6 +6329,29 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, return ClassifyTile(OriginVoxels, Step, CellsPerAxis, nullptr); } +bool UVoxelGenerator::TryProveSealedSolidTile( + const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const +{ + return VF_TryProveSealedSolidTile( + *this, OriginVoxels, Step, CellsPerAxis, /*DisabledGuards=*/0); +} + +bool UVoxelGenerator::TryProveOutOfLayoutAirTile( + const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const +{ + return VF_TryProveOutOfLayoutAirTile(*this, OriginVoxels, Step, CellsPerAxis); +} + +#if WITH_DEV_AUTOMATION_TESTS +bool UVoxelGenerator::TryProveSealedSolidTileForTest( + const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, + uint8 DisabledGuards) const +{ + return VF_TryProveSealedSolidTile( + *this, OriginVoxels, Step, CellsPerAxis, DisabledGuards); +} +#endif + EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, FVoxelTileClassificationStats* OutStats) const diff --git a/Source/VoxelForge/Private/VoxelStartupTrace.cpp b/Source/VoxelForge/Private/VoxelStartupTrace.cpp index 50f8654..414685b 100644 --- a/Source/VoxelForge/Private/VoxelStartupTrace.cpp +++ b/Source/VoxelForge/Private/VoxelStartupTrace.cpp @@ -260,11 +260,12 @@ void RecordTile(const FTileSample& Sample) GState->Samples.Add(Sample); ++GState->TileRecordsWritten; WriteEventLocked(*GState, TEXT("tile"), FString::Printf( - TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"verdict\":%d,\"empty\":%d,\"triangles\":%d," + TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"triangles\":%d," "\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s," "\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"), Sample.TileX, Sample.TileY, Sample.TileZ, - Sample.Level, Sample.Verdict, Sample.bEmpty ? 1 : 0, Sample.Triangles, + Sample.Level, Sample.Verdict, Sample.bSealedSolidProof ? 1 : 0, + Sample.bEmpty ? 1 : 0, Sample.Triangles, *Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds), *Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds), *Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds), diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 537b480..21d328d 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -97,6 +97,15 @@ namespace GVoxelForgeOuterClassifierMode, TEXT("Outer tile classifier mode: 0 off, 1 discarded exact validation.")); + // Cheap structural proofs for LOD tiles whose exact core lattice lies entirely in a gap/seal + // (AllSolid) or constant out-of-layout air (AllAir). They are independent of the discarded + // full classifier and default on; 0 is retained for per-tile identity/A-B traces. + int32 GVoxelForgeSealedSolidProof = 1; + FAutoConsoleVariableRef CVarVoxelForgeSealedSolidProof( + TEXT("voxel.SealedSolidProof"), + GVoxelForgeSealedSolidProof, + TEXT("Skip meshing only when an exact gap/seal or constant-air proof holds: 0 off, 1 on.")); + // Startup command-line cvars can arrive before the game world has spawned AVoxelWorld (or // before its first tiles have become visible). Keep the diagnostic edit pending until Tick has // a loaded tile; otherwise a startup measurement silently becomes a no-op or edits the wrong @@ -104,6 +113,7 @@ namespace int32 GVoxelForgePendingTestModification = 0; float GVoxelForgePendingModificationRadius = 3.0f; float GVoxelForgePendingModificationStrength = 10.0f; + bool GVoxelForgeTestModificationLoadedProofTarget = false; bool GVoxelForgeTestModificationCommandLineConsumed = false; bool GVoxelForgeStreamingBudgetReported = false; bool GVoxelForgeGenerationCapHitReported = false; @@ -1105,6 +1115,9 @@ void AVoxelWorld::BeginPlay() TotalObsoleteWorkerTasks.store(0, std::memory_order_relaxed); TotalValidationDensityCalls.store(0, std::memory_order_relaxed); TotalMesherDensityCalls.store(0, std::memory_order_relaxed); + TotalSealedSolidProofCandidates.store(0, std::memory_order_relaxed); + TotalSealedSolidProofSkips.store(0, std::memory_order_relaxed); + TotalOutOfLayoutAirProofSkips.store(0, std::memory_order_relaxed); AppliedTileCount = 0; AppliedVisibleTileCount = 0; AppliedTriangleCount = 0; @@ -1133,6 +1146,7 @@ void AVoxelWorld::BeginPlay() int32 CommandLineProfileTileGeneration = GVoxelForgeProfileTileGeneration; int32 CommandLineUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut; int32 CommandLineOuterClassifierMode = GVoxelForgeOuterClassifierMode; + int32 CommandLineSealedSolidProof = GVoxelForgeSealedSolidProof; int32 CommandLineSampleStacks = GVoxelForgeSampleStacks; if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile)) { @@ -1161,6 +1175,10 @@ void AVoxelWorld::BeginPlay() { GVoxelForgeOuterClassifierMode = CommandLineOuterClassifierMode; } + if (FParse::Value(FCommandLine::Get(), TEXT("voxel.SealedSolidProof="), CommandLineSealedSolidProof)) + { + GVoxelForgeSealedSolidProof = CommandLineSealedSolidProof; + } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.SampleStacks="), CommandLineSampleStacks)) { GVoxelForgeSampleStacks = CommandLineSampleStacks; @@ -1168,6 +1186,7 @@ void AVoxelWorld::BeginPlay() GVoxelForgeProfileTileGeneration = GVoxelForgeProfileTileGeneration != 0 ? 1 : 0; GVoxelForgeUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0 ? 1 : 0; GVoxelForgeOuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1); + GVoxelForgeSealedSolidProof = GVoxelForgeSealedSolidProof != 0 ? 1 : 0; GVoxelForgeSampleStacks = FMath::Clamp( GVoxelForgeSampleStacks, 0, @@ -1281,8 +1300,9 @@ void AVoxelWorld::BeginPlay() Mesher->bUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0; UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeClassifierConfig] outer_mode=%d nested_block_early_out=%d"), - GVoxelForgeOuterClassifierMode, Mesher->bUseBlockEarlyOut ? 1 : 0); + TEXT("[VoxelForgeClassifierConfig] outer_mode=%d sealed_solid_proof=%d nested_block_early_out=%d"), + GVoxelForgeOuterClassifierMode, GVoxelForgeSealedSolidProof, + Mesher->bUseBlockEarlyOut ? 1 : 0); VoxelForgeStartupTrace::RecordEvent(TEXT("generator_mesher_constructed"), FString::Printf( TEXT("\"generator\":1,\"mesher\":1,\"generate_skirts\":%d,\"full_res_clip_levels\":%d,\"coarse_tile_cells\":%d"), @@ -1377,6 +1397,9 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest() { bHeadlessStreamingTestMovement = false; bHeadlessStreamingTestExitRequested = false; + bStartupTraceThroughCrossing = false; + bHeadlessStreamingTestCenterOverride = false; + HeadlessStreamingTestCenterVoxel = FVector::ZeroVector; HeadlessStreamingTestMoveAttempts = 0; HeadlessStreamingTestDistanceCm = 0.0; HeadlessStreamingTestSpeedCmPerSecond = 0.0f; @@ -1472,6 +1495,33 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest() HeadlessStrateCrossingTestRequestedPassageIndex); } + int32 TraceThroughCrossingValue = 0; + if (FParse::Value( + CommandLine, + TEXT("voxel.StartupTraceThroughCrossing="), + TraceThroughCrossingValue)) + { + bStartupTraceThroughCrossing = TraceThroughCrossingValue != 0; + } + + float CenterX = 0.0f; + float CenterY = 0.0f; + float CenterZ = 0.0f; + const bool bCenterX = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterX="), CenterX); + const bool bCenterY = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterY="), CenterY); + const bool bCenterZ = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterZ="), CenterZ); + if (bCenterX && bCenterY && bCenterZ + && VoxelMath::IsFinite(CenterX) + && VoxelMath::IsFinite(CenterY) + && VoxelMath::IsFinite(CenterZ)) + { + bHeadlessStreamingTestCenterOverride = true; + HeadlessStreamingTestCenterVoxel = FVector(CenterX, CenterY, CenterZ); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeStreamingTest] center_override=1 local_vox=(%.2f,%.2f,%.2f)"), + CenterX, CenterY, CenterZ); + } + if (bMoveRequested && !bHeadlessStrateCrossingTest) { FString DirectionText; @@ -2000,6 +2050,11 @@ void AVoxelWorld::MaybeFinishHeadlessStrateCrossingTest() HeadlessStrateCrossingTestLastActualPosition.X, HeadlessStrateCrossingTestLastActualPosition.Y, HeadlessStrateCrossingTestLastActualPosition.Z); + if (bStartupTraceThroughCrossing && VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::Finish( + bPassed ? TEXT("strate_crossing_passed") : TEXT("strate_crossing_failed")); + } FPlatformMisc::RequestExitWithStatus( false, bPassed ? 0 : 1, TEXT("VoxelForge strate crossing test complete")); } @@ -2110,7 +2165,10 @@ void AVoxelWorld::Tick(float DeltaTime) PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size2D()); UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime); ObserveHeadlessStrateCrossingTest(PlayerLastPos, PlayerPawn); - UpdateChunksAroundPosition(PlayerLastPos, PlayerPawn, PlayerHeading); + const FVector StreamingCenterPosition = bHeadlessStreamingTestCenterOverride + ? LocalVoxelToWorld(HeadlessStreamingTestCenterVoxel) + : PlayerLastPos; + UpdateChunksAroundPosition(StreamingCenterPosition, PlayerPawn, PlayerHeading); if (AtmosphereManager) { AtmosphereManager->UpdateForPlayer(PlayerLastPos); @@ -2165,10 +2223,20 @@ void AVoxelWorld::Tick(float DeltaTime) { GVoxelForgePendingModificationRadius = 3.0f; GVoxelForgePendingModificationStrength = 10.0f; + FString ModificationTarget; + if (FParse::Value( + *CommandLine, + TEXT("voxel.TestModificationTarget="), + ModificationTarget) + && ModificationTarget.Equals(TEXT("loaded-proof"), ESearchCase::IgnoreCase)) + { + GVoxelForgeTestModificationLoadedProofTarget = true; + } UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeTestModification] queued radius=%.2f strength=%.2f"), + TEXT("[VoxelForgeTestModification] queued radius=%.2f strength=%.2f target=%s"), GVoxelForgePendingModificationRadius, - GVoxelForgePendingModificationStrength); + GVoxelForgePendingModificationStrength, + GVoxelForgeTestModificationLoadedProofTarget ? TEXT("loaded-proof") : TEXT("player")); GVoxelForgePendingTestModification = 1; } } @@ -2183,10 +2251,16 @@ void AVoxelWorld::Tick(float DeltaTime) && PendingTiles.Num() == 0 && bStartupTraceDesiredRecorded && PendingCollisionCooks.Num() == 0 + && (!bStartupTraceThroughCrossing + || !bHeadlessStrateCrossingTest + || bHeadlessStrateCrossingTestPassed + || bHeadlessStrateCrossingTestFailed) // The desired set being applied is not the player-ready point: the feet tile must have // received its completed LOD0 RMC body first. If no pawn exists, there is no gate to wait // for and the headless trace keeps its old completion behavior. - && (!bHasPlayer || IsPlayerSupportCollisionReady(PlayerPawn, PlayerLastPos))) + && (!bHasPlayer + || bHeadlessStreamingTestCenterOverride + || IsPlayerSupportCollisionReady(PlayerPawn, PlayerLastPos))) { TMap SteadyLevelCounts; TMap SteadyLevelMins; @@ -2244,22 +2318,75 @@ void AVoxelWorld::Tick(float DeltaTime) // No pawn is a distinct state from a pawn at (0,0,0); keep the diagnostic fallback for // headless/editor use without making the world-origin pawn skip streaming. FVector ModificationPosition = bHasPlayer ? PlayerLastPos : GetActorLocation(); - const FVoxelTileKey ModificationCenterTile( + FVoxelTileKey ModificationCenterTile( WorldToChunkCoord(WorldToLocalCm(ModificationPosition)), 0); + bool bModificationTargetSelected = false; + if (GVoxelForgePendingTestModification != 0 + && GVoxelForgeTestModificationLoadedProofTarget + && Generator) + { + // The diagnostic target is deliberately selected from the applied set. That makes the + // edit exercise the normal loaded-tile synchronous remesh, while the proof query here is + // the same public predicate used by GenerateTileResult before the diff exists. + for (const FVoxelTileKey& CandidateTile : LoadedTiles) + { + if (CandidateTile.Level != 0 + || PendingTiles.Contains(CandidateTile)) + { + continue; + } + const FIntVector CandidateOrigin( + CandidateTile.Coord.X * CHUNK_SIZE, + CandidateTile.Coord.Y * CHUNK_SIZE, + CandidateTile.Coord.Z * CHUNK_SIZE); + if (!Generator->TryProveSealedSolidTile(CandidateOrigin, 1, CHUNK_SIZE)) + { + continue; + } + + const FIntVector CandidateCenter( + CandidateOrigin.X + CHUNK_SIZE / 2, + CandidateOrigin.Y + CHUNK_SIZE / 2, + CandidateOrigin.Z + CHUNK_SIZE / 2); + ModificationPosition = LocalVoxelToWorld(FVector(CandidateCenter)); + ModificationCenterTile = CandidateTile; + bModificationTargetSelected = true; + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeTestModification] player_mine_target tile=(%d,%d,%d) " + "local_center=(%d,%d,%d) before_proof=1"), + CandidateTile.Coord.X, CandidateTile.Coord.Y, CandidateTile.Coord.Z, + CandidateCenter.X, CandidateCenter.Y, CandidateCenter.Z); + break; + } + } if (GVoxelForgePendingTestModification != 0 && DiffLayer + && (!GVoxelForgeTestModificationLoadedProofTarget || bModificationTargetSelected) && LoadedTiles.Contains(ModificationCenterTile) && !PendingTiles.Contains(ModificationCenterTile)) { GVoxelForgePendingTestModification = 0; UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeTestModification] applying position=(%.2f,%.2f,%.2f) radius=%.2f strength=%.2f"), + TEXT("[VoxelForgeTestModification] applying player_mine=1 tile=(%d,%d,%d) " + "position=(%.2f,%.2f,%.2f) radius=%.2f strength=%.2f"), + ModificationCenterTile.Coord.X, ModificationCenterTile.Coord.Y, ModificationCenterTile.Coord.Z, ModificationPosition.X, ModificationPosition.Y, ModificationPosition.Z, GVoxelForgePendingModificationRadius, GVoxelForgePendingModificationStrength); CarveAtPosition( ModificationPosition, GVoxelForgePendingModificationRadius, GVoxelForgePendingModificationStrength); + const FIntVector ModifiedOrigin( + ModificationCenterTile.Coord.X * CHUNK_SIZE, + ModificationCenterTile.Coord.Y * CHUNK_SIZE, + ModificationCenterTile.Coord.Z * CHUNK_SIZE); + const bool bProofSurvivedCarve = Generator + && Generator->TryProveSealedSolidTile(ModifiedOrigin, 1, CHUNK_SIZE); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeTestModification] player_mine_complete tile=(%d,%d,%d) " + "after_proof=%d"), + ModificationCenterTile.Coord.X, ModificationCenterTile.Coord.Y, + ModificationCenterTile.Coord.Z, bProofSurvivedCarve ? 1 : 0); } ProcessUnloadQueue(); MaybeFinishHeadlessStreamingTest(); @@ -2849,6 +2976,12 @@ void AVoxelWorld::LogStreamingLatencySummary() const TotalWorkerGenerationCycles.load(std::memory_order_relaxed); const uint64 ObsoleteWorkerCycles = TotalObsoleteWorkerCycles.load(std::memory_order_relaxed); + const uint64 SealedSolidProofCandidates = + TotalSealedSolidProofCandidates.load(std::memory_order_relaxed); + const uint64 SealedSolidProofSkips = + TotalSealedSolidProofSkips.load(std::memory_order_relaxed); + const uint64 OutOfLayoutAirProofSkips = + TotalOutOfLayoutAirProofSkips.load(std::memory_order_relaxed); const bool bDiagnosticsEnabled = GVoxelForgeProfileTileGeneration != 0 || VoxelForgeStartupTrace::IsActive() || VoxelDensityProfile::GetMode() != VoxelDensityProfile::EMode::Disabled; @@ -2863,6 +2996,8 @@ void AVoxelWorld::LogStreamingLatencySummary() const "applied_tiles=%llu applied_visible_tiles=%llu applied_triangles=%llu " "worker_generation_tasks=%llu worker_generation_sum_s=%.6f " "validation_density_calls=%llu mesher_density_calls=%llu " + "sealed_solid_proof_candidates=%llu sealed_solid_proof_skips=%llu " + "out_of_layout_air_proof_skips=%llu " "obsolete_worker_tasks=%llu obsolete_worker_sum_s=%.6f " "outer_classifier_verdicts=%s applied_level_summary=%s band_level_summary=%s"), bDiagnosticsEnabled ? TEXT("profiled") : TEXT("clean"), @@ -2881,6 +3016,9 @@ void AVoxelWorld::LogStreamingLatencySummary() const FPlatformTime::ToSeconds64(WorkerGenerationCycles), static_cast(TotalValidationDensityCalls.load(std::memory_order_relaxed)), static_cast(TotalMesherDensityCalls.load(std::memory_order_relaxed)), + static_cast(SealedSolidProofCandidates), + static_cast(SealedSolidProofSkips), + static_cast(OutOfLayoutAirProofSkips), static_cast(TotalObsoleteWorkerTasks.load(std::memory_order_relaxed)), FPlatformTime::ToSeconds64(ObsoleteWorkerCycles), *ClassifierVerdictSummary, @@ -3191,6 +3329,7 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result) Sample.TileZ = Result.Tile.Coord.Z; Sample.Level = Result.Tile.Level; Sample.Verdict = Result.ClassifyVerdict; + Sample.bSealedSolidProof = Result.bSealedSolidProof; Sample.bEmpty = bResultEmpty; Sample.Triangles = Result.NumTriangles; Sample.RequestToApplySeconds = RequestToApply; @@ -4523,7 +4662,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d triangles=%d " "band=(%d,%d) " - "verdict=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f " + "verdict=%d proof=%d air_proof=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f " "cache_build=%.6f evaluation=%.6f " "refine=%u stack=%u core_samples=%u final_samples=%u core_hits=%u final_hits=%u " "core_leaves=%u final_leaves=%u tail_queries=%u tail_eval=%u " @@ -4543,7 +4682,9 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells, bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0, Result.bEmpty ? 1 : 0, Result.NumTriangles, BandChunkLo, BandChunkHi, - ClassifyVerdict, ClassifySeconds, MeshSeconds, StreamSeconds, + ClassifyVerdict, Result.bSealedSolidProof ? 1 : 0, + Result.bOutOfLayoutAirProof ? 1 : 0, + ClassifySeconds, MeshSeconds, StreamSeconds, TileSeconds, CacheBuildSeconds, EvaluationSeconds, ClassifierStats.RefineNodes, ClassifierStats.StackBoxCalls, ClassifierStats.ExactCoreSamples, ClassifierStats.ExactFinalSamples, @@ -4637,6 +4778,8 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector Result.bObsolete = false; Result.bEmpty = true; Result.ClassifyVerdict = -1; + Result.bSealedSolidProof = false; + Result.bOutOfLayoutAirProof = false; Result.ClassifySeconds = 0.0; Result.MeshSeconds = 0.0; Result.StreamSeconds = 0.0; @@ -4674,8 +4817,49 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector // même pour les cellules uniformes, et ces tuiles sont rares (fenêtre d'ombre). // (Gate IsoLevel == 0 : les verdicts du classifieur supposent l'iso MC à zéro exactement.) bool bTrivialEmpty = false; + if (GVoxelForgeSealedSolidProof != 0 + && !bSheetTile && !bWantCapture && Generator && Mesher + && Mesher->IsoLevel == 0.0f) + { + TotalSealedSolidProofCandidates.fetch_add(1, std::memory_order_relaxed); + const double ProofStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0; + if (Generator->TryProveSealedSolidTile(OriginVoxels, Step, Cells)) + { + ClassifySeconds = bMeasureTile + ? FPlatformTime::Seconds() - ProofStartSeconds : 0.0; + ClassifyVerdict = static_cast(EVoxelTileClass::AllSolid); + Result.ClassifyVerdict = ClassifyVerdict; + Result.bSealedSolidProof = true; + TotalSealedSolidProofSkips.fetch_add(1, std::memory_order_relaxed); + INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllSolid); + bTrivialEmpty = true; + } + else if (Generator->TryProveOutOfLayoutAirTile(OriginVoxels, Step, Cells)) + { + ClassifySeconds = bMeasureTile + ? FPlatformTime::Seconds() - ProofStartSeconds : 0.0; + ClassifyVerdict = static_cast(EVoxelTileClass::AllAir); + Result.ClassifyVerdict = ClassifyVerdict; + Result.bOutOfLayoutAirProof = true; + TotalOutOfLayoutAirProofSkips.fetch_add(1, std::memory_order_relaxed); + INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllAir); + bTrivialEmpty = true; + } + else if (bMeasureTile) + { + // Keep the measured proof cost visible on a tile that continues through the normal + // path. The clean summary remains counter-only and pays no timer overhead. + ClassifySeconds = FPlatformTime::Seconds() - ProofStartSeconds; + } + } + if (ShouldAbortWork(ObsoleteFlag)) + { + AbortResult(); + EmitTileProfile(); + return; + } const int32 OuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1); - if (OuterClassifierMode != 0 + if (!bTrivialEmpty && OuterClassifierMode != 0 && !bSheetTile && !bWantCapture && Generator && Mesher && Mesher->IsoLevel == 0.0f) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile); @@ -4754,7 +4938,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector EmitTileProfile(); return; } - ClassifySeconds = bMeasureTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0; + ClassifySeconds += bMeasureTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0; ClassifyVerdict = static_cast(ValidatedVerdict); Result.ClassifyVerdict = ClassifyVerdict; const int32 TrackedLOD = FMath::Clamp(Tile.Level, 0, TrackedClassifierLODCount - 1); diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index 0aa1219..9aaf15e 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -22,6 +22,16 @@ class UVoxelDiffLayer; class UVoxelBiomeDefinition; struct FTunnelCoreWorldEvaluation; +/** Guards which may be disabled only by the sealed-solid proof's soundness tests. */ +enum class EVoxelSealedSolidProofGuard : uint8 +{ + None = 0, + DiffLayer = 1u << 0, + Passages = 1u << 1, + OriginLanding = 1u << 2, + DisturbanceChasms = 1u << 3, +}; + //============================================================================= // LOD-AWARE OCTAVE REDUCTION (T2.b) //============================================================================= @@ -166,6 +176,27 @@ public: int32 SizeX, int32 SizeY, int32 SizeZ) const; void EndDensityBlock() const; + /** + * Prove that the mesher's core lattice is entirely solid from the cheap, structural rules + * that own inter-strate gaps and boundary seals. This never samples density. It is intended + * for an empty-tile fast path; a false answer is harmless, while a true answer must make every + * core lattice vertex negative in the final MC field. + * + * The proof is deliberately narrower than ClassifyTile: it accepts only a tile made entirely + * of gap/seal lattice rows and rejects recipes/composer overrides plus every known air-writing + * structural feature. The mesher's +/-1 normal halo is not part of the cell lattice, matching + * ClassifyTile and GenerateMesh. + */ + bool TryProveSealedSolidTile(const FIntVector& OriginVoxels, int32 Step, + int32 CellsPerAxis) const; + +#if WITH_DEV_AUTOMATION_TESTS + /** Test hook: disable named proof guards and require the brute-force oracle to catch it. */ + bool TryProveSealedSolidTileForTest( + const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, + uint8 DisabledGuards) const; +#endif + /** * Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise). * Utilisée en interne par GetDensityAt quand la strate est de ce type. @@ -385,6 +416,15 @@ public: int32 CellsPerAxis, struct FVoxelTileClassificationStats* OutStats) const; + /** + * Prove the exact constant-air field above/below the strate layout for a mesher lattice. + * This is separate from TryProveSealedSolidTile: it never supplies collision support and its + * caller records AllAir, not AllSolid. It shares the layout/diff/structural guards with the + * production classifier and returns false for every mixed or uncertain box. + */ + bool TryProveOutOfLayoutAirTile(const FIntVector& OriginVoxels, int32 Step, + int32 CellsPerAxis) const; + private: /** Identité process-unique du propriétaire des caches `CP_*` thread_local. * Process-unique owner identity for the `CP_*` thread-local cache key. */ diff --git a/Source/VoxelForge/Public/VoxelStartupTrace.h b/Source/VoxelForge/Public/VoxelStartupTrace.h index dac49a0..ca6ba16 100644 --- a/Source/VoxelForge/Public/VoxelStartupTrace.h +++ b/Source/VoxelForge/Public/VoxelStartupTrace.h @@ -14,6 +14,7 @@ namespace VoxelForgeStartupTrace int32 TileZ = 0; int32 Level = -1; int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (sheet/early path) + bool bSealedSolidProof = false; bool bEmpty = true; int32 Triangles = 0; // This is worker-result -> game-thread mesh submission. Collision readiness is a later diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index f8f9c21..7239df9 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -107,6 +107,8 @@ struct FChunkResult uint64 GenerationEndCycles = 0; uint64 ApplyStartCycles = 0; int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0; + bool bSealedSolidProof = false; // true when the cheap gap/seal proof skipped meshing + bool bOutOfLayoutAirProof = false; // true when the exact constant-air guard skipped meshing // Clean, low-overhead work counters. These are carried with the result so the worker can // count exact validation and mesher density calls locally, then publish one aggregate update. int64 ValidationDensityCalls = 0; @@ -934,6 +936,9 @@ public: std::atomic TotalObsoleteWorkerTasks{0}; std::atomic TotalValidationDensityCalls{0}; std::atomic TotalMesherDensityCalls{0}; + std::atomic TotalSealedSolidProofCandidates{0}; + std::atomic TotalSealedSolidProofSkips{0}; + std::atomic TotalOutOfLayoutAirProofSkips{0}; std::atomic OuterClassifierCallsByLOD[TrackedClassifierLODCount]{}; std::atomic OuterClassifierVerdictsByLOD[TrackedClassifierLODCount][3]{}; @@ -959,6 +964,9 @@ public: // by explicit command-line switches; they do not alter ordinary player movement or streaming. bool bHeadlessStreamingTestMovement = false; bool bHeadlessStreamingTestExitRequested = false; + bool bStartupTraceThroughCrossing = false; + bool bHeadlessStreamingTestCenterOverride = false; + FVector HeadlessStreamingTestCenterVoxel = FVector::ZeroVector; int32 HeadlessStreamingTestMoveAttempts = 0; double HeadlessStreamingTestDistanceCm = 0.0; float HeadlessStreamingTestSpeedCmPerSecond = 0.0f;