diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index 66bab2e..65d4e15 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -7914,10 +7914,20 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( const FTunnelSupportFloorColumn* SupportColumn, bool bUseSpatialIndex) { + VoxelGenLOD::FScopedReachCost ReachCost( + VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld); if (VoxelDensityAblation::IsTunnelCoreOff()) { return FTunnelCoreWorldEvaluation(); } + // GenerateMesh installs the exact tile/block reach mask after building the complete tile + // cache. Point queries retain the canonical all-reachable TLS default; debug proof calls set + // the bypass bit so the same sample can be evaluated as the reference field. + if (!VoxelGenLOD::IsTilePostReachBypassActive() + && !VoxelGenLOD::IsTunnelCoreReachable()) + { + return FTunnelCoreWorldEvaluation(); + } VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCoreWorld); const FVector Pos(WorldX, WorldY, WorldZ); @@ -8042,6 +8052,318 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( return Result; } +bool VoxelCaveMorphology::AnyTunnelCoreWorldNearLattice( + const FChunkSDFCache& Cache, + const FBox& VoxelBox, + float ReachScale) +{ + if (VoxelDensityAblation::IsTunnelCoreOff()) + { + return false; + } + if (!VoxelBox.IsValid + || !VoxelMath::IsFinite(ReachScale) || ReachScale <= 0.0f) + { + return true; + } + + const auto TouchesSphere = [&VoxelBox](const FVector& Center, float Radius) -> bool + { + if (!VoxelMath::IsFinite(Center.X) + || !VoxelMath::IsFinite(Center.Y) + || !VoxelMath::IsFinite(Center.Z) + || !VoxelMath::IsFinite(Radius) || Radius < 0.0f) + { + return true; + } + const FVector Closest( + FMath::Clamp(Center.X, (float)VoxelBox.Min.X, (float)VoxelBox.Max.X), + FMath::Clamp(Center.Y, (float)VoxelBox.Min.Y, (float)VoxelBox.Max.Y), + FMath::Clamp(Center.Z, (float)VoxelBox.Min.Z, (float)VoxelBox.Max.Z)); + return FVector::DistSquared(Closest, Center) <= Radius * Radius; + }; + + const auto TouchesExpandedAabb = [&VoxelBox]( + const FVector& Min, const FVector& Max, float Pad) -> bool + { + if (!VoxelMath::IsFinite(Min.X) || !VoxelMath::IsFinite(Min.Y) + || !VoxelMath::IsFinite(Min.Z) || !VoxelMath::IsFinite(Max.X) + || !VoxelMath::IsFinite(Max.Y) || !VoxelMath::IsFinite(Max.Z) + || !VoxelMath::IsFinite(Pad) || Pad < 0.0f) + { + return true; + } + const float DX = FMath::Max3( + static_cast(VoxelBox.Min.X) - static_cast(Max.X), + static_cast(Min.X) - static_cast(VoxelBox.Max.X), 0.0f); + const float DY = FMath::Max3( + static_cast(VoxelBox.Min.Y) - static_cast(Max.Y), + static_cast(Min.Y) - static_cast(VoxelBox.Max.Y), 0.0f); + const float DZ = FMath::Max3( + static_cast(VoxelBox.Min.Z) - static_cast(Max.Z), + static_cast(Min.Z) - static_cast(VoxelBox.Max.Z), 0.0f); + return DX <= Pad && DY <= Pad && DZ <= Pad; + }; + + const float Fade = FMath::Max(Cache.SDFBlendRadius, 0.0f) * 0.35f; + if (!VoxelMath::IsFinite(Fade)) + { + return true; + } + + for (const FCachedTunnel& Tunnel : Cache.Tunnels) + { + const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2 + && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num(); + const FVector& BoundCenter = bHasWorldChain + ? Tunnel.WorldBoundCenter : Tunnel.BoundCenter; + const float BoundRadiusSq = bHasWorldChain + ? Tunnel.WorldBoundRadiusSq : Tunnel.BoundRadiusSq; + const float InfluenceRadius = bHasWorldChain + ? Tunnel.WorldInfluenceRadius : Tunnel.SDFInfluenceRadius; + const FVector& CenterlineMin = bHasWorldChain + ? Tunnel.WorldCenterlineMin : Tunnel.SDFCenterlineMin; + const FVector& CenterlineMax = bHasWorldChain + ? Tunnel.WorldCenterlineMax : Tunnel.SDFCenterlineMax; + + if (!VoxelMath::IsFinite(BoundCenter.X) + || !VoxelMath::IsFinite(BoundCenter.Y) + || !VoxelMath::IsFinite(BoundCenter.Z) + || !VoxelMath::IsFinite(BoundRadiusSq) || BoundRadiusSq < 0.0f + || !VoxelMath::IsFinite(InfluenceRadius) || InfluenceRadius < 0.0f + || !VoxelMath::IsFinite(CenterlineMin.X) + || !VoxelMath::IsFinite(CenterlineMin.Y) + || !VoxelMath::IsFinite(CenterlineMin.Z) + || !VoxelMath::IsFinite(CenterlineMax.X) + || !VoxelMath::IsFinite(CenterlineMax.Y) + || !VoxelMath::IsFinite(CenterlineMax.Z)) + { + return true; + } + if (bHasWorldChain) + { + for (int32 Index = 0; Index < Tunnel.WorldControlPoints.Num(); ++Index) + { + const FVector& Point = Tunnel.WorldControlPoints[Index]; + if (!VoxelMath::IsFinite(Point.X) || !VoxelMath::IsFinite(Point.Y) + || !VoxelMath::IsFinite(Point.Z) + || !VoxelMath::IsFinite(Tunnel.WorldControlRadii[Index])) + { + return true; + } + } + } + + // VF_TunnelFloorRelief uses the same bounded noise as the floor profile. Use the + // profile-independent maximum scale (1.0) here: the normal authored scale is <= .20, + // but this remains sound for an old or malformed profile that bypassed that clamp. + float ReliefBound = 0.0f; + if (Tunnel.FloorReliefStrength > 0.0f) + { + if (!VoxelMath::IsFinite(Tunnel.FloorReliefStrength) + || !VoxelMath::IsFinite(Tunnel.FloorReliefFrequency)) + { + return true; + } + ReliefBound = FMath::Abs(Tunnel.FloorReliefStrength) + * VOXEL_NOISE_SCALE * 1.01f; + if (!VoxelMath::IsFinite(ReliefBound)) + { + return true; + } + } + + float MouthReach = 0.0f; + float MouthFloorDelta = 0.0f; + if (bHasWorldChain && Tunnel.bHasFloorRoomOwnership) + { + MouthReach = FMath::Max( + FMath::Max(Tunnel.WorldMouthBlendRadiusA, Tunnel.WorldMouthBlendRadiusB), + 0.0f) + Fade + 1.0f; + if (!VoxelMath::IsFinite(MouthReach)) + { + return true; + } + const float NaturalA = static_cast(Tunnel.WorldControlPoints[0].Z) + - FMath::Abs(Tunnel.WorldControlRadii[0]); + const int32 Last = Tunnel.WorldControlPoints.Num() - 1; + const float NaturalB = static_cast(Tunnel.WorldControlPoints[Last].Z) + - FMath::Abs(Tunnel.WorldControlRadii[Last]); + if (!VoxelMath::IsFinite(NaturalA) || !VoxelMath::IsFinite(NaturalB)) + { + return true; + } + if (Tunnel.WorldMouthFloorZA > -FLT_MAX + && !VoxelMath::IsFinite(Tunnel.WorldMouthFloorZA)) + { + return true; + } + if (Tunnel.WorldMouthFloorZB > -FLT_MAX + && !VoxelMath::IsFinite(Tunnel.WorldMouthFloorZB)) + { + return true; + } + if (Tunnel.WorldMouthFloorZA > -FLT_MAX) + { + MouthFloorDelta = FMath::Max( + MouthFloorDelta, FMath::Abs(Tunnel.WorldMouthFloorZA - NaturalA)); + } + if (Tunnel.WorldMouthFloorZB > -FLT_MAX) + { + MouthFloorDelta = FMath::Max( + MouthFloorDelta, FMath::Abs(Tunnel.WorldMouthFloorZB - NaturalB)); + } + } + + // Tighten the construction sphere to the actual swept chain before falling back to the + // legacy bound. The evaluator tests a capsule segment, not the sphere around the whole + // wandering chain; using segment AABBs removes the long-chain false near result while + // retaining a conservative pad for radius, floor band, relief, and SmoothMax blend. + const TArray& ChainPoints = bHasWorldChain + ? Tunnel.WorldControlPoints : Tunnel.ControlPoints; + const TArray& ChainRadii = bHasWorldChain + ? Tunnel.WorldControlRadii : Tunnel.ControlRadii; + bool bHasTightChain = ChainPoints.Num() >= 2 + && ChainRadii.Num() == ChainPoints.Num(); + bool bTightTouch = false; + if (bHasTightChain) + { + float MaxRadius = 0.0f; + for (int32 Index = 0; Index < ChainPoints.Num(); ++Index) + { + const FVector& Point = ChainPoints[Index]; + if (!VoxelMath::IsFinite(Point.X) || !VoxelMath::IsFinite(Point.Y) + || !VoxelMath::IsFinite(Point.Z) + || !VoxelMath::IsFinite(ChainRadii[Index])) + { + bHasTightChain = false; + break; + } + MaxRadius = FMath::Max(MaxRadius, FMath::Abs(ChainRadii[Index])); + } + if (bHasTightChain) + { + float ProfileFloorDelta = 0.0f; + const TArray& Profiles = bHasWorldChain + ? Tunnel.WorldFloorProfiles : Tunnel.FloorProfiles; + if (Tunnel.bTunnelFloorEnabled + && Profiles.Num() == ChainPoints.Num() - 1) + { + for (int32 SegmentIndex = 0; + SegmentIndex < Profiles.Num(); ++SegmentIndex) + { + const FTunnelFloorSegmentProfile& Profile = Profiles[SegmentIndex]; + if (!VoxelMath::IsFinite(Profile.StartFloorZ) + || !VoxelMath::IsFinite(Profile.EndFloorZ)) + { + bHasTightChain = false; + break; + } + const float NaturalA = static_cast( + ChainPoints[SegmentIndex].Z) + - FMath::Abs(ChainRadii[SegmentIndex]); + const float NaturalB = static_cast( + ChainPoints[SegmentIndex + 1].Z) + - FMath::Abs(ChainRadii[SegmentIndex + 1]); + ProfileFloorDelta = FMath::Max( + ProfileFloorDelta, + FMath::Max( + FMath::Abs(Profile.StartFloorZ - NaturalA), + FMath::Abs(Profile.EndFloorZ - NaturalB))); + } + } + if (bHasTightChain) + { + const float FloorPad = Tunnel.bTunnelFloorEnabled + ? FMath::Max( + VoxelPassageGeometry::LandingFloorThicknessVoxels, + FMath::Max(Cache.SDFBlendRadius, 0.0f) * 0.35f) + : 0.0f; + const float SegmentPad = MaxRadius + FloorPad + + ProfileFloorDelta + ReliefBound; + if (!VoxelMath::IsFinite(SegmentPad)) + { + return true; + } + for (int32 SegmentIndex = 0; + SegmentIndex + 1 < ChainPoints.Num(); ++SegmentIndex) + { + const FVector Min( + FMath::Min(ChainPoints[SegmentIndex].X, + ChainPoints[SegmentIndex + 1].X), + FMath::Min(ChainPoints[SegmentIndex].Y, + ChainPoints[SegmentIndex + 1].Y), + FMath::Min(ChainPoints[SegmentIndex].Z, + ChainPoints[SegmentIndex + 1].Z)); + const FVector Max( + FMath::Max(ChainPoints[SegmentIndex].X, + ChainPoints[SegmentIndex + 1].X), + FMath::Max(ChainPoints[SegmentIndex].Y, + ChainPoints[SegmentIndex + 1].Y), + FMath::Max(ChainPoints[SegmentIndex].Z, + ChainPoints[SegmentIndex + 1].Z)); + if (TouchesExpandedAabb(Min, Max, SegmentPad * ReachScale)) + { + bTightTouch = true; + break; + } + } + + if (!bTightTouch && Tunnel.bTunnelFloorEnabled + && bHasWorldChain && Tunnel.bHasFloorRoomOwnership) + { + const float MouthHorizontal = MouthReach; + const float MouthVertical = FMath::Max( + MaxRadius + VoxelPassageGeometry::LandingFloorThicknessVoxels + + ReliefBound, + MouthFloorDelta + VoxelPassageGeometry::LandingFloorThicknessVoxels + + ReliefBound); + const float ScaledHorizontal = MouthHorizontal * ReachScale; + const float ScaledVertical = MouthVertical * ReachScale; + for (int32 Endpoint : {0, ChainPoints.Num() - 1}) + { + const FVector& Point = ChainPoints[Endpoint]; + if (TouchesExpandedAabb( + FVector( + static_cast(Point.X) - ScaledHorizontal, + static_cast(Point.Y) - ScaledHorizontal, + static_cast(Point.Z) - ScaledVertical), + FVector( + static_cast(Point.X) + ScaledHorizontal, + static_cast(Point.Y) + ScaledHorizontal, + static_cast(Point.Z) + ScaledVertical), + 0.0f)) + { + bTightTouch = true; + break; + } + } + } + } + } + } + if (bHasTightChain && !bTightTouch) + { + continue; + } + + // The broad sphere already encloses the swept radius and finite floor band. Add the + // vertical relief and room-mouth ownership envelopes before asking whether the lattice + // box can touch it. A miss is therefore an identity proof for the core evaluator and + // its support/room-floor tail; its only field writes are bSupportFloor, bRoomFloor, or + // CoreSDF < -inset, all of which are inside this envelope. + const float Reach = ( + FMath::Sqrt(FMath::Max(BoundRadiusSq, 0.0f)) + + ReliefBound + MouthReach + MouthFloorDelta) * ReachScale; + if (!VoxelMath::IsFinite(Reach) + || TouchesSphere(BoundCenter, Reach)) + { + return true; + } + } + return false; +} + float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( float WorldX, float WorldY, float WorldZ, const FChunkSDFCache& Cache, diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 4017227..c1cb0a3 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -2370,6 +2370,49 @@ namespace return static_cast(Hash & (TunnelDensityCacheSlotCount - 1)); } + static void UpdateTunnelCoreTileReach( + const FChunkSDFCache& Cache, + bool bUseTileCacheWindow) + { + if (!bUseTileCacheWindow + || !VoxelGenLOD::bTileReachDiagnosticTile + || VoxelGenLOD::TileCellsPerAxis <= 0) + { + return; + } + + const int32 Step = FMath::Max(VoxelGenLOD::GetThreadSampleStep(), 1); + const int64 Extent = static_cast(VoxelGenLOD::TileCellsPerAxis + 1) + * static_cast(Step); + if (Extent <= 0 || Extent > MAX_int32) + { + VoxelGenLOD::RecordTunnelCoreReachDecision(true); + return; + } + const FIntVector& Origin = VoxelGenLOD::TileOriginVoxels; + const FBox LatticeWithHalo( + FVector( + static_cast(static_cast(Origin.X) - Step), + static_cast(static_cast(Origin.Y) - Step), + static_cast(static_cast(Origin.Z) - Step)), + FVector( + static_cast(static_cast(Origin.X) + Extent), + static_cast(static_cast(Origin.Y) + Extent), + static_cast(static_cast(Origin.Z) + Extent))); + const bool bNear = VoxelCaveMorphology::AnyTunnelCoreWorldNearLattice( + Cache, LatticeWithHalo, VoxelGenLOD::TileReachScale); + if (bNear) + { + VoxelGenLOD::TilePostReachFlags |= VoxelGenLOD::TunnelCoreReachable; + } + else + { + VoxelGenLOD::TilePostReachFlags &= ~VoxelGenLOD::TunnelCoreReachable; + } + VoxelGenLOD::bTileCoreReachProofEnabled = true; + VoxelGenLOD::RecordTunnelCoreReachDecision(bNear); + } + void PrepareTunnelCoreCache( const UVoxelStrateManager& Manager, const FIntVector& ChunkCoord, @@ -2398,6 +2441,14 @@ namespace * static_cast(CHUNK_SIZE) * VOXEL_SIZE); const uint32 WorldSeed = static_cast(Manager.GetWorldSeed()); + // A tile-window cache contains the complete graph reach for this tile. Chunk-local + // caches deliberately leave the core bit conservative; updating it there would make the + // first chunk's false result hide a tunnel that enters through a later cache window. + if (bUseTileCacheWindow && OutState.bValid) + { + UpdateTunnelCoreTileReach(OutState.Cache, bUseTileCacheWindow); + } + const bool bSameGraphWindow = OutState.bValid && OutState.OwnerId == OwnerId && OutState.ManagerLifetimeId == ManagerLifetimeId @@ -2457,6 +2508,7 @@ namespace Params, WorldSeed, StrateIndex, nullptr, ERoomGraphBuildSite::GeneratorTunnelCore); + UpdateTunnelCoreTileReach(OutState.Cache, bUseTileCacheWindow); BuildTunnelSupportColumnsForChunk( OutState.Cache, ChunkCoord, bUseSpatialIndex); OutState.OwnerId = OwnerId; @@ -2723,6 +2775,24 @@ void UVoxelGenerator::EndDensityBlock() const float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const { + // Soundness mode: for a sample in a block whose core or passage-carving reach bit is clear, + // evaluate the complete canonical field once with the bypass bit set. The normal call then + // takes the fast path below and compares the final MC field after the boundary seal and diff + // layer—not an intermediate SDF. This is deliberately debug-only; production pays no + // recursive call and only the evaluator/tail flag reads. + const bool bCompareFinalField = VoxelGenLOD::bTilePostReachDebug + && !VoxelGenLOD::IsTilePostReachBypassActive() + && VoxelGenLOD::bTileReachDiagnosticTile + && (!VoxelGenLOD::IsTunnelCoreReachable() + || !VoxelGenLOD::IsPassageCarvingReachable()); + float CanonicalFinalField = 0.0f; + if (bCompareFinalField) + { + const bool PreviousBypass = VoxelGenLOD::bTilePostReachBypass; + VoxelGenLOD::bTilePostReachBypass = true; + CanonicalFinalField = GetDensityAt(WorldX, WorldY, WorldZ); + VoxelGenLOD::bTilePostReachBypass = PreviousBypass; + } TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GetDensityAt); VF_ParseFastIsFiniteSwitch(); VF_ParseFusedEvaluatorSwitch(); @@ -3542,16 +3612,52 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // bridge over a graph tunnel. Reassert the native cached tunnel core here, after every // solid floor writer but before the global XY seal. This cache is built once per chunk, // never once per voxel. + VoxelGenLOD::FScopedReachCost TunnelCoreTailReachCost( + VoxelGenLOD::ETileReachCostKind::TunnelCoreTail); + const bool bTunnelCoreReachable = VoxelGenLOD::IsTunnelCoreReachable(); + const bool bTunnelCoreBypass = VoxelGenLOD::IsTilePostReachBypassActive(); + auto ApplyGraphLandingFloors = [&]() + { + // These are passage/origin landing posts, not tunnel-core writes. Keep them alive when + // the core bit is clear because their own per-tile reach bits may still be set. + { + VoxelDensityProfile::FScopedTimer ProfileTimer( + VoxelDensityProfile::EBucket::PassageLandingFloor); + VF_ApplyOriginLandingFloorMC( + Result, WorldX, WorldY, WorldZ, + CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, + CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); + } + { + VoxelDensityProfile::FScopedTimer ProfileTimer( + VoxelDensityProfile::EBucket::PassageLandingFloor); + StrateManager->ApplyPassageLandingFloorMC( + Result, WorldX, WorldY, WorldZ, LandingBaseDensity); + } + { + VoxelDensityProfile::FScopedTimer ProfileTimer( + VoxelDensityProfile::EBucket::PassageLandingRoomFloor); + StrateManager->ApplyPassageLandingRoomFloorMC( + Result, WorldX, WorldY, WorldZ, LandingBaseDensity); + } + }; if (!VoxelDensityAblation::IsTunnelCoreOff() && (ActiveTunnelCoreCache->bValid || CP_UseOpStack)) { FTunnelCoreWorldEvaluation TunnelCore; bool bHaveTunnelCore = bHavePreDisturbanceTunnelCore; + if (!bTunnelCoreReachable && !bTunnelCoreBypass) + { + // The fused/source hand-off is also empty on the skipped path. Do not let the + // default object turn this into a second tail evaluation. + bHaveTunnelCore = false; + } if (bHaveTunnelCore) { TunnelCore = PreDisturbanceTunnelCore; } - if (!bHaveTunnelCore && ActiveTunnelCoreCache->bValid) + if (!bHaveTunnelCore && ActiveTunnelCoreCache->bValid + && (bTunnelCoreReachable || bTunnelCoreBypass)) { const FTunnelSupportFloorColumn* SupportColumn = nullptr; FTunnelSupportFloorColumn EmptySupportColumn; @@ -3631,30 +3737,10 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co FMath::Max(LandingBaseDensity * 2.0f, 1.0f)); } - // The graph tunnel can overlap an inter-strate landing at a room mouth. Its air - // backstop is allowed to reopen the tunnel, but the landing's proved support floor - // must own the final floor band; otherwise the graph post can erase the only support - // surface at the mouth and leave the player-fit graph with a disconnected pocket. - { - VoxelDensityProfile::FScopedTimer ProfileTimer( - VoxelDensityProfile::EBucket::PassageLandingFloor); - VF_ApplyOriginLandingFloorMC( - Result, WorldX, WorldY, WorldZ, - CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, - CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); - } - { - VoxelDensityProfile::FScopedTimer ProfileTimer( - VoxelDensityProfile::EBucket::PassageLandingFloor); - StrateManager->ApplyPassageLandingFloorMC( - Result, WorldX, WorldY, WorldZ, LandingBaseDensity); - } - { - VoxelDensityProfile::FScopedTimer ProfileTimer( - VoxelDensityProfile::EBucket::PassageLandingRoomFloor); - StrateManager->ApplyPassageLandingRoomFloorMC( - Result, WorldX, WorldY, WorldZ, LandingBaseDensity); - } + } + if (bHaveTunnelCore || (!bTunnelCoreReachable && !bTunnelCoreBypass)) + { + ApplyGraphLandingFloors(); } } // Inter-strate passages own the same final D-floor contract as graph tunnels. This is @@ -3750,6 +3836,14 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co VoxelDensityProfile::FScopedTimer DensityTailTimer( VoxelDensityProfile::EBucket::DensityTail); VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); + if (bCompareFinalField) + { + const bool bFinalFieldDifferent = + !VoxelGenLOD::SameFloatBits(Result, CanonicalFinalField); + VoxelGenLOD::RecordSkippedPostComparison( + VoxelGenLOD::ETilePostComparisonKind::FinalField, + bFinalFieldDifferent); + } return Result; } @@ -4089,6 +4183,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo TerrainOps, ERoomGraphBuildSite::GeneratorTile ); + UpdateTunnelCoreTileReach(SDFCache, bUseTileCacheWindow); CachedSMinX = SMinX; CachedSMaxX = SMaxX; CachedSMinY = SMinY; CachedSMaxY = SMaxY; diff --git a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index cf331f6..9aa4464 100644 --- a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -33,6 +33,12 @@ namespace TEXT("Scale the per-tile post reach for deliberate soundness tests. Default 1.0 is exact; " "values below 1 intentionally shrink the guard.")); + int32 GVoxelForgeTileReachCostDiagnostics = 0; + FAutoConsoleVariableRef CVarVoxelForgeTileReachCostDiagnostics( + TEXT("voxel.TileReachCostDiagnostics"), + GVoxelForgeTileReachCostDiagnostics, + TEXT("Measure core/passage density cost split by tile reach; no field gate.")); + bool GVoxelForgeTilePostReachSwitchesParsed = false; void VF_ParseTilePostReachSwitches() @@ -44,18 +50,24 @@ namespace GVoxelForgeTilePostReachSwitchesParsed = true; int32 DebugValue = GVoxelForgeTilePostReachDebug; float ScaleValue = GVoxelForgeTilePostReachScale; + int32 CostDiagnosticsValue = GVoxelForgeTileReachCostDiagnostics; FParse::Value( FCommandLine::Get(), TEXT("voxel.TilePostReachDebug="), DebugValue); FParse::Value( FCommandLine::Get(), TEXT("voxel.TilePostReachScale="), ScaleValue); + FParse::Value( + FCommandLine::Get(), TEXT("voxel.TileReachCostDiagnostics="), CostDiagnosticsValue); GVoxelForgeTilePostReachDebug = DebugValue != 0 ? 1 : 0; if (!VoxelMath::IsFinite(ScaleValue) || ScaleValue <= 0.0f) { ScaleValue = 1.0f; } GVoxelForgeTilePostReachScale = FMath::Min(ScaleValue, 1.0f); + GVoxelForgeTileReachCostDiagnostics = CostDiagnosticsValue != 0 ? 1 : 0; VoxelGenLOD::GTilePostReachDebugEnabled.store( GVoxelForgeTilePostReachDebug != 0, std::memory_order_relaxed); + VoxelGenLOD::GTileReachCostDiagnosticsEnabled.store( + GVoxelForgeTileReachCostDiagnostics != 0, std::memory_order_relaxed); } void VF_ParseOperatorBlockSwitch() @@ -284,6 +296,23 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, GVoxelForgeTilePostReachDebug != 0); TGuardValue TilePostReachBypassGuard( VoxelGenLOD::bTilePostReachBypass, false); + TGuardValue TileReachDiagnosticGuard( + VoxelGenLOD::bTileReachDiagnosticTile, true); + TGuardValue TileCoreReachProofGuard( + VoxelGenLOD::bTileCoreReachProofEnabled, false); + TGuardValue TileCoreReachDecisionGuard( + VoxelGenLOD::bTileCoreReachDecisionRecorded, false); + TGuardValue TileReachScaleGuard( + VoxelGenLOD::TileReachScale, GVoxelForgeTilePostReachScale); + const float ReachScale = GVoxelForgeTilePostReachScale; + const int32 ReachBlocksPerAxis = FMath::DivideAndRoundUp( + CellsPerAxis, VoxelGenLOD::TileReachBlockCells); + TGuardValue TileReachBlocksGuard( + VoxelGenLOD::TileReachBlocksPerAxis, ReachBlocksPerAxis); + TGuardValue TileReachBlockIndexGuard( + VoxelGenLOD::TileReachBlockIndex, INDEX_NONE); + TGuardValue TileBlockReachValidGuard( + VoxelGenLOD::bTileBlockReachValid, false); uint8 TilePostReachFlags = VoxelGenLOD::AllTilePostReach; if (Generator->StrateManager != nullptr) { @@ -297,8 +326,9 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, static_cast(static_cast(OriginVoxels.X) + Extent), static_cast(static_cast(OriginVoxels.Y) + Extent), static_cast(static_cast(OriginVoxels.Z) + Extent))); - const float ReachScale = GVoxelForgeTilePostReachScale; - TilePostReachFlags = 0; + // Core stays conservative until the tile-window tunnel cache is built. Passage carving + // is manager-owned and can be decided immediately from the same lattice+halo box. + TilePostReachFlags = VoxelGenLOD::TunnelCoreReachable; if (Generator->StrateManager->AnyOriginLandingNearLattice( LatticeWithHalo, OriginVoxels, Step, ReachScale)) { @@ -309,6 +339,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, { TilePostReachFlags |= VoxelGenLOD::PassageLandingReachable; } + if (Generator->StrateManager->AnyPassageCarvingNearLattice( + LatticeWithHalo, OriginVoxels, Step, ReachScale)) + { + TilePostReachFlags |= VoxelGenLOD::PassageCarvingReachable; + } if (Generator->StrateManager->AnyPassageStructuralPostNearLattice( LatticeWithHalo, OriginVoxels, Step, ReachScale)) { @@ -317,6 +352,30 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, } TGuardValue TilePostReachFlagsGuard( VoxelGenLOD::TilePostReachFlags, TilePostReachFlags); + for (int32 BlockZ = 0; BlockZ < ReachBlocksPerAxis; ++BlockZ) + { + for (int32 BlockY = 0; BlockY < ReachBlocksPerAxis; ++BlockY) + { + for (int32 BlockX = 0; BlockX < ReachBlocksPerAxis; ++BlockX) + { + const int32 BlockIndex = + ((BlockZ * ReachBlocksPerAxis) + BlockY) * ReachBlocksPerAxis + BlockX; + uint8 BlockFlags = TilePostReachFlags; + if (Generator->StrateManager != nullptr + && (TilePostReachFlags & VoxelGenLOD::PassageCarvingReachable) != 0 + && !Generator->StrateManager->AnyPassageCarvingNearLattice( + VoxelGenLOD::MakeTileReachBlockBox( + OriginVoxels, Step, CellsPerAxis, + BlockX, BlockY, BlockZ), + OriginVoxels, Step, ReachScale)) + { + BlockFlags &= ~VoxelGenLOD::PassageCarvingReachable; + } + VoxelGenLOD::TileBlockReachFlags[BlockIndex] = BlockFlags; + } + } + } + VoxelGenLOD::bTileBlockReachValid = true; VoxelGenLOD::RecordTileReachMask(TilePostReachFlags); // COUPE DE CONTENU PAR STRATE — restreint le maillage (et l'échantillonnage) aux cellules // dont l'intervalle Z chevauche la bande [BandZMinVox, BandZMaxVox] (voxels inclusifs). @@ -660,6 +719,15 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, else { // World voxel = tile origin + grid offset scaled by the cell size (Step). + const int32 ReachBlockX = FMath::Clamp(gx, 0, CellsPerAxis - 1) + / VoxelGenLOD::TileReachBlockCells; + const int32 ReachBlockY = FMath::Clamp(gy, 0, CellsPerAxis - 1) + / VoxelGenLOD::TileReachBlockCells; + const int32 ReachBlockZ = FMath::Clamp(gz, 0, CellsPerAxis - 1) + / VoxelGenLOD::TileReachBlockCells; + VoxelGenLOD::TileReachBlockIndex = + ((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY) + * ReachBlocksPerAxis + ReachBlockX; DensityGrid[LocalIndex] = Generator->GetDensityAt( OriginVoxels.X + gx * Step, OriginVoxels.Y + gy * Step, diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index aa46b1e..62c6da8 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -2506,10 +2506,21 @@ void UVoxelStrateManager::GeneratePassages() float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float WorldZ) const { + VoxelGenLOD::FScopedReachCost ReachCost( + VoxelGenLOD::ETileReachCostKind::PassageCarving); if (VoxelDensityAblation::IsPassageCarvingOff()) { return FLT_MAX; } + // GenerateMesh installs this once per tile/block. Outside the mesher the TLS defaults to + // the canonical all-reachable state, and debug canonical calls set the bypass bit explicitly. + // A miss is an identity for VF_ApplyPassageCarving, so do not even enter the spatial shortlist + // or capsule chain on a proven-far sample. + if (!VoxelGenLOD::IsTilePostReachBypassActive() + && !VoxelGenLOD::IsPassageCarvingReachable()) + { + return FLT_MAX; + } // The landing terms are part of the passage modifier, so their reach proof is deliberately // wider than the direct post's four-voxel blend: SmoothMin can lower a finite term by K/6 at @@ -5096,6 +5107,224 @@ bool UVoxelStrateManager::AnyPassageLandingNearLattice( return false; } +bool UVoxelStrateManager::AnyPassageCarvingNearLattice( + const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step, + float ReachScale) const +{ + if (VoxelDensityAblation::IsPassageCarvingOff()) + { + return false; + } + if (Step <= 0 || !VoxelBox.IsValid + || !VoxelMath::IsFinite(ReachScale) || ReachScale <= 0.0f) + { + return true; + } + + // BoundRadius is built around every capsule control point and both landing rooms. The + // construction already adds the ModSDF<4 carve band. SmoothMin can lower a chain of + // otherwise-far finite terms by at most K/6 per combine, so include that finite morphology + // loss as well. A miss of this sphere proves every capsule/landing term folds to the hard + // no-op band before VF_ApplyPassageCarving; malformed descriptors remain candidates. + int32 SmoothMinCombines = 0; + for (const FVoxelPassage& Passage : Passages) + { + if (!VoxelMath::IsFinite(Passage.BoundCenter.X) + || !VoxelMath::IsFinite(Passage.BoundCenter.Y) + || !VoxelMath::IsFinite(Passage.BoundCenter.Z) + || !VoxelMath::IsFinite(Passage.BoundRadius) + || !VoxelMath::IsFinite(Passage.BoundRadiusSq) + || Passage.BoundRadius < 0.0f || Passage.BoundRadiusSq < 0.0f) + { + return true; + } + if (Passage.ControlPoints.Num() >= 2) + { + if (Passage.ControlPoints.Num() > 4096) + { + return true; + } + const bool bTaper = Passage.ControlRadii.Num() == Passage.ControlPoints.Num(); + if (!bTaper && !VoxelMath::IsFinite(Passage.Radius)) + { + return true; + } + for (int32 Index = 0; Index < Passage.ControlPoints.Num(); ++Index) + { + const FVector& Point = Passage.ControlPoints[Index]; + if (!VoxelMath::IsFinite(Point.X) || !VoxelMath::IsFinite(Point.Y) + || !VoxelMath::IsFinite(Point.Z) + || (bTaper && !VoxelMath::IsFinite(Passage.ControlRadii[Index]))) + { + return true; + } + } + SmoothMinCombines += FMath::Max(Passage.ControlPoints.Num() - 2, 0); + } + else + { + if (!VoxelMath::IsFinite(Passage.UpperPoint.X) + || !VoxelMath::IsFinite(Passage.UpperPoint.Y) + || !VoxelMath::IsFinite(Passage.UpperPoint.Z) + || !VoxelMath::IsFinite(Passage.LowerPoint.X) + || !VoxelMath::IsFinite(Passage.LowerPoint.Y) + || !VoxelMath::IsFinite(Passage.LowerPoint.Z) + || !VoxelMath::IsFinite(Passage.Radius)) + { + return true; + } + } + + if (Passage.bNativeFloorEnabled) + { + ++SmoothMinCombines; // SmoothMax is the same K/6 polynomial bound. + } + SmoothMinCombines += 3; // passage union plus its two landing terms + 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->FloorZ) + || !VoxelMath::IsFinite(Landing->CeilingZ) + || !VoxelMath::IsFinite(Landing->HalfWidth) + || !VoxelMath::IsFinite(Landing->FloorThickness)) + { + return true; + } + } + } + + constexpr float CarveThreshold = 4.0f; + constexpr float BlendK = 3.0f; + const float MorphologyPad = CarveThreshold + + BlendK * static_cast(FMath::Max(SmoothMinCombines, 0)) / 6.0f; + if (!VoxelMath::IsFinite(MorphologyPad) || MorphologyPad < 0.0f) + { + return true; + } + for (const FVoxelPassage& Passage : Passages) + { + bool bHasTightGeometry = false; + bool bTightTouch = false; + const float PassagePad = MorphologyPad * ReachScale; + if (!VoxelMath::IsFinite(PassagePad) || PassagePad < 0.0f) + { + return true; + } + if (Passage.ControlPoints.Num() >= 2) + { + bHasTightGeometry = Passage.ControlPoints.Num() <= 4096; + const bool bTaper = Passage.ControlRadii.Num() == Passage.ControlPoints.Num(); + const float MaxRadius = bTaper + ? [&Passage]() + { + float Result = 0.0f; + for (const float Radius : Passage.ControlRadii) + { + Result = FMath::Max(Result, FMath::Abs(Radius)); + } + return Result; + }() + : FMath::Abs(Passage.Radius); + if (!VoxelMath::IsFinite(MaxRadius)) + { + return true; + } + const float SegmentPad = (MaxRadius + + (Passage.bNativeFloorEnabled + ? VoxelPassageGeometry::LandingFloorThicknessVoxels : 0.0f) + + PassagePad); + if (!VoxelMath::IsFinite(SegmentPad)) + { + return true; + } + for (int32 SegmentIndex = 0; + bHasTightGeometry && SegmentIndex + 1 < Passage.ControlPoints.Num(); + ++SegmentIndex) + { + const FVector& A = Passage.ControlPoints[SegmentIndex]; + const FVector& B = Passage.ControlPoints[SegmentIndex + 1]; + if (VF_LatticeBoxTouchesAABB( + VoxelBox, LatticeOrigin, Step, + FMath::Min(A.X, B.X) - SegmentPad, + FMath::Max(A.X, B.X) + SegmentPad, + FMath::Min(A.Y, B.Y) - SegmentPad, + FMath::Max(A.Y, B.Y) + SegmentPad, + FMath::Min(A.Z, B.Z) - SegmentPad, + FMath::Max(A.Z, B.Z) + SegmentPad)) + { + bTightTouch = true; + } + } + } + else + { + bHasTightGeometry = true; + const float SegmentPad = FMath::Abs(Passage.Radius) + + (Passage.bNativeFloorEnabled + ? VoxelPassageGeometry::LandingFloorThicknessVoxels : 0.0f) + + PassagePad; + if (!VoxelMath::IsFinite(SegmentPad)) + { + return true; + } + if (VF_LatticeBoxTouchesAABB( + VoxelBox, LatticeOrigin, Step, + FMath::Min(Passage.UpperPoint.X, Passage.LowerPoint.X) - SegmentPad, + FMath::Max(Passage.UpperPoint.X, Passage.LowerPoint.X) + SegmentPad, + FMath::Min(Passage.UpperPoint.Y, Passage.LowerPoint.Y) - SegmentPad, + FMath::Max(Passage.UpperPoint.Y, Passage.LowerPoint.Y) + SegmentPad, + FMath::Min(Passage.UpperPoint.Z, Passage.LowerPoint.Z) - SegmentPad, + FMath::Max(Passage.UpperPoint.Z, Passage.LowerPoint.Z) + SegmentPad)) + { + bTightTouch = true; + } + } + + const float RoomPad = PassagePad; + for (const FVoxelPassageLanding* Landing : { + &Passage.UpperLanding, &Passage.LowerLanding }) + { + if (Landing->HalfWidth <= 0.0f || Landing->CeilingZ <= Landing->FloorZ) + { + continue; + } + constexpr float RoomRounding = 1.5f; + const float Height = Landing->CeilingZ - Landing->FloorZ; + const float HalfX = FMath::Max(Landing->HalfWidth - RoomRounding, 0.25f); + const float HalfZ = FMath::Max(Height * 0.5f - RoomRounding, 0.25f); + const float CenterZ = (Landing->FloorZ + Landing->CeilingZ) * 0.5f; + if (VF_LatticeBoxTouchesAABB( + VoxelBox, LatticeOrigin, Step, + Landing->StandingPoint.X - HalfX - RoomRounding - RoomPad, + Landing->StandingPoint.X + HalfX + RoomRounding + RoomPad, + Landing->StandingPoint.Y - HalfX - RoomRounding - RoomPad, + Landing->StandingPoint.Y + HalfX + RoomRounding + RoomPad, + FMath::Min(Landing->FloorZ, + CenterZ - HalfZ - RoomRounding) - RoomPad, + FMath::Max(Landing->CeilingZ, + CenterZ + HalfZ + RoomRounding) + RoomPad)) + { + bTightTouch = true; + } + } + + if (bHasTightGeometry && !bTightTouch) + { + continue; + } + const float Reach = (Passage.BoundRadius + MorphologyPad) * ReachScale; + if (!VoxelMath::IsFinite(Reach) + || VF_LatticeBoxTouchesSphere(VoxelBox, Passage.BoundCenter, Reach)) + { + return true; + } + } + return false; +} + bool UVoxelStrateManager::AnyPassageStructuralPostNearLattice( const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step, float ReachScale) const diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index a0418a5..828dbcb 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -1162,7 +1162,7 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason) TEXT("[VoxelForgeTilePostReachKinds] comparisons=" "origin_air=%llu/origin_floor=%llu/passage_sdf=%llu/landing_air=%llu/" "tunnel_air=%llu/landing_floor=%llu/structural=%llu/native_floor=%llu/room_floor=%llu " - "differences=%llu/%llu/%llu/%llu/%llu/%llu/%llu/%llu/%llu"), + "final_field=%llu differences=%llu/%llu/%llu/%llu/%llu/%llu/%llu/%llu/%llu/%llu"), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::OriginAir)].load( std::memory_order_relaxed)), @@ -1190,6 +1190,9 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason) static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageLandingRoomFloor)].load( std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ + static_cast(VoxelGenLOD::ETilePostComparisonKind::FinalField)].load( + std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::OriginAir)].load( std::memory_order_relaxed)), @@ -1216,6 +1219,106 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason) std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageLandingRoomFloor)].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ + static_cast(VoxelGenLOD::ETilePostComparisonKind::FinalField)].load( + std::memory_order_relaxed))); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeTileReachDecisions] core_reachable=%llu core_skipped=%llu " + "passage_carving_reachable=%llu passage_carving_skipped=%llu"), + static_cast(VoxelGenLOD::GTunnelCoreReachableTiles.load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTunnelCoreSkippedTiles.load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GPassageCarvingReachableTiles.load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GPassageCarvingSkippedTiles.load( + std::memory_order_relaxed))); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeTileReachCost] core_world_far_calls=%llu far_cycles=%llu " + "near_calls=%llu near_cycles=%llu"), + static_cast(VoxelGenLOD::GTileReachCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][1].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][1].load( + std::memory_order_relaxed))); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeTileReachCost] core_tail_far_calls=%llu far_cycles=%llu " + "near_calls=%llu near_cycles=%llu"), + static_cast(VoxelGenLOD::GTileReachCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][1].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][1].load( + std::memory_order_relaxed))); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeTileReachCost] passage_carving_far_calls=%llu far_cycles=%llu " + "near_calls=%llu near_cycles=%llu"), + static_cast(VoxelGenLOD::GTileReachCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][1].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][1].load( + std::memory_order_relaxed))); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeTileBlockReachCost] core_world_far_calls=%llu far_cycles=%llu " + "near_calls=%llu near_cycles=%llu; core_tail_far_calls=%llu far_cycles=%llu " + "near_calls=%llu near_cycles=%llu; passage_far_calls=%llu far_cycles=%llu " + "near_calls=%llu near_cycles=%llu"), + static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][1].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][1].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][1].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][1].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][0].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ + static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][1].load( + std::memory_order_relaxed)), + static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ + static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][1].load( std::memory_order_relaxed))); if (VoxelGenLOD::GTilePostReachDebugEnabled.load(std::memory_order_relaxed) && TilePostReachDifferences != 0) diff --git a/Source/VoxelForge/Public/VoxelCaveMorphology.h b/Source/VoxelForge/Public/VoxelCaveMorphology.h index a6cf2a7..3b5583c 100644 --- a/Source/VoxelForge/Public/VoxelCaveMorphology.h +++ b/Source/VoxelForge/Public/VoxelCaveMorphology.h @@ -1081,6 +1081,14 @@ namespace VoxelCaveMorphology bool bUseSpatialIndex = true ); + // Per-tile broad reach for the world-space core/floor post. The bound includes the swept + // chain, floor band, relief envelope, and the finite room-mouth ownership apron. + VOXELFORGE_API bool AnyTunnelCoreWorldNearLattice( + const FChunkSDFCache& Cache, + const FBox& VoxelBox, + float ReachScale = 1.0f + ); + // True for the finite support slab beneath a world-space graph tunnel. The native density // path and the operator stack use this same predicate before reopening the air core, keeping // the rounded capsule's bottom from becoming an unsupported point. diff --git a/Source/VoxelForge/Public/VoxelStrateManager.h b/Source/VoxelForge/Public/VoxelStrateManager.h index a31c11b..c6f57be 100644 --- a/Source/VoxelForge/Public/VoxelStrateManager.h +++ b/Source/VoxelForge/Public/VoxelStrateManager.h @@ -476,6 +476,10 @@ public: bool AnyPassageLandingNearLattice(const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step, float ReachScale = 1.0f) const; + /** Per-tile reach for the modifier itself: capsule chain, landing terms, and carve blend. */ + bool AnyPassageCarvingNearLattice(const FBox& VoxelBox, + const FIntVector& LatticeOrigin, int32 Step, + float ReachScale = 1.0f) const; /** Conservative sphere guard for all passage-owned structural posts and native D-floors. */ bool AnyPassageStructuralPostNearLattice(const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step, diff --git a/Source/VoxelForge/Public/VoxelTilePostReach.h b/Source/VoxelForge/Public/VoxelTilePostReach.h index dba39f5..c2d0e60 100644 --- a/Source/VoxelForge/Public/VoxelTilePostReach.h +++ b/Source/VoxelForge/Public/VoxelTilePostReach.h @@ -7,6 +7,7 @@ #pragma once #include "CoreMinimal.h" +#include "HAL/PlatformTime.h" #include @@ -17,9 +18,14 @@ namespace VoxelGenLOD OriginLandingReachable = 1u << 0, PassageLandingReachable = 1u << 1, PassageStructuralReachable= 1u << 2, + TunnelCoreReachable = 1u << 3, + PassageCarvingReachable = 1u << 4, AllTilePostReach = OriginLandingReachable | PassageLandingReachable - | PassageStructuralReachable, + | PassageStructuralReachable + | TunnelCoreReachable + | PassageCarvingReachable, + AllTileReach = AllTilePostReach, }; enum class ETilePostComparisonKind : uint8 @@ -34,14 +40,65 @@ namespace VoxelGenLOD PassageStructuralPosts, PassageNativeFloor, PassageLandingRoomFloor, + TunnelCore, + PassageCarving, + FinalField, Count, }; + enum class ETileReachCostKind : uint8 + { + TunnelCoreWorld = 0, + TunnelCoreTail, + PassageCarving, + Count, + }; + + constexpr int32 TileReachBlockCells = 8; + constexpr int32 TileReachMaxBlocksPerAxis = 4; + constexpr int32 TileReachMaxBlockCount = + TileReachMaxBlocksPerAxis * TileReachMaxBlocksPerAxis * TileReachMaxBlocksPerAxis; + + FORCEINLINE FBox MakeTileReachBlockBox( + const FIntVector& TileOrigin, int32 Step, int32 CellsPerAxis, + int32 BlockX, int32 BlockY, int32 BlockZ) + { + const int32 SafeStep = FMath::Max(Step, 1); + const int32 MinX = BlockX * TileReachBlockCells; + const int32 MinY = BlockY * TileReachBlockCells; + const int32 MinZ = BlockZ * TileReachBlockCells; + const int32 MaxX = FMath::Min( + CellsPerAxis, MinX + TileReachBlockCells); + const int32 MaxY = FMath::Min( + CellsPerAxis, MinY + TileReachBlockCells); + const int32 MaxZ = FMath::Min( + CellsPerAxis, MinZ + TileReachBlockCells); + return FBox( + FVector( + static_cast(static_cast(TileOrigin.X) + MinX * SafeStep - SafeStep), + static_cast(static_cast(TileOrigin.Y) + MinY * SafeStep - SafeStep), + static_cast(static_cast(TileOrigin.Z) + MinZ * SafeStep - SafeStep)), + FVector( + static_cast(static_cast(TileOrigin.X) + MaxX * SafeStep), + static_cast(static_cast(TileOrigin.Y) + MaxY * SafeStep), + static_cast(static_cast(TileOrigin.Z) + MaxZ * SafeStep))); + } + // A tile that is not being meshed, or a point query outside the mesher, must retain the // canonical behaviour. GenerateMesh scopes this to its computed mask. - inline thread_local uint8 TilePostReachFlags = AllTilePostReach; + inline thread_local uint8 TilePostReachFlags = AllTileReach; inline thread_local bool bTilePostReachDebug = false; inline thread_local bool bTilePostReachBypass = false; + inline thread_local bool bTileReachDiagnosticTile = false; + inline thread_local bool bTileCoreReachDecisionRecorded = false; + inline thread_local float TileReachScale = 1.0f; + inline thread_local int32 TileReachBlocksPerAxis = 0; + inline thread_local int32 TileReachBlockIndex = INDEX_NONE; + inline thread_local bool bTileBlockReachValid = false; + inline thread_local uint8 TileBlockReachFlags[TileReachMaxBlockCount]{}; + // A false result is installed only after a cache was built with the whole mesher tile as its + // graph window. Chunk-local caches leave this true (the conservative no-skip fallback). + inline thread_local bool bTileCoreReachProofEnabled = false; // These are diagnostics only. They never participate in a field decision. inline std::atomic GTilePostReachDebugEnabled { false }; @@ -52,12 +109,25 @@ namespace VoxelGenLOD inline std::atomic GOriginLandingSkippedTiles { 0 }; inline std::atomic GPassageLandingSkippedTiles { 0 }; inline std::atomic GPassageStructuralSkippedTiles { 0 }; + inline std::atomic GTunnelCoreReachableTiles { 0 }; + inline std::atomic GTunnelCoreSkippedTiles { 0 }; + inline std::atomic GPassageCarvingReachableTiles { 0 }; + inline std::atomic GPassageCarvingSkippedTiles { 0 }; inline std::atomic GSkippedPostComparisons { 0 }; inline std::atomic GSkippedPostDifferences { 0 }; inline std::atomic GSkippedPostComparisonsByKind[ static_cast(ETilePostComparisonKind::Count)]{}; inline std::atomic GSkippedPostDifferencesByKind[ static_cast(ETilePostComparisonKind::Count)]{}; + inline std::atomic GTileReachCostDiagnosticsEnabled { false }; + inline std::atomic GTileReachCostCalls[ + static_cast(ETileReachCostKind::Count)][2]{}; + inline std::atomic GTileReachCostCycles[ + static_cast(ETileReachCostKind::Count)][2]{}; + inline std::atomic GTileReachBlockCostCalls[ + static_cast(ETileReachCostKind::Count)][2]{}; + inline std::atomic GTileReachBlockCostCycles[ + static_cast(ETileReachCostKind::Count)][2]{}; FORCEINLINE bool IsTilePostReachable(uint8 Bit) { @@ -79,6 +149,25 @@ namespace VoxelGenLOD return IsTilePostReachable(PassageStructuralReachable); } + FORCEINLINE bool IsTunnelCoreReachable() + { + // Core's measured far share is negligible; keep its exact tile decision and avoid a + // second, weaker block geometry approximation. Passage carving is the only block-gated + // work because its tight chain produced a material far share in the pre-gate measure. + return IsTilePostReachable(TunnelCoreReachable); + } + + FORCEINLINE bool IsPassageCarvingReachable() + { + if (bTileBlockReachValid + && TileReachBlockIndex >= 0 + && TileReachBlockIndex < TileReachMaxBlockCount) + { + return (TileBlockReachFlags[TileReachBlockIndex] & PassageCarvingReachable) != 0; + } + return IsTilePostReachable(PassageCarvingReachable); + } + FORCEINLINE bool IsTilePostReachBypassActive() { return bTilePostReachBypass; @@ -111,8 +200,122 @@ namespace VoxelGenLOD { GPassageStructuralSkippedTiles.fetch_add(1, std::memory_order_relaxed); } + if ((Flags & PassageCarvingReachable) != 0) + { + GPassageCarvingReachableTiles.fetch_add(1, std::memory_order_relaxed); + } + else + { + GPassageCarvingSkippedTiles.fetch_add(1, std::memory_order_relaxed); + } } + FORCEINLINE void RecordTunnelCoreReachDecision(bool bReachable) + { + if (bTileCoreReachDecisionRecorded) + { + return; + } + bTileCoreReachDecisionRecorded = true; + if (bReachable) + { + GTunnelCoreReachableTiles.fetch_add(1, std::memory_order_relaxed); + } + else + { + GTunnelCoreSkippedTiles.fetch_add(1, std::memory_order_relaxed); + } + } + + FORCEINLINE uint8 TileReachCostIndex(ETileReachCostKind Kind) + { + switch (Kind) + { + case ETileReachCostKind::TunnelCoreWorld: + case ETileReachCostKind::TunnelCoreTail: + return IsTilePostReachable(TunnelCoreReachable) ? 1u : 0u; + case ETileReachCostKind::PassageCarving: + return IsTilePostReachable(PassageCarvingReachable) ? 1u : 0u; + default: + return 1u; + } + } + + FORCEINLINE uint8 BlockReachCostIndex(ETileReachCostKind Kind) + { + switch (Kind) + { + case ETileReachCostKind::TunnelCoreWorld: + case ETileReachCostKind::TunnelCoreTail: + return IsTunnelCoreReachable() ? 1u : 0u; + case ETileReachCostKind::PassageCarving: + return IsPassageCarvingReachable() ? 1u : 0u; + default: + return 1u; + } + } + + FORCEINLINE void RecordTileReachBlockCost( + ETileReachCostKind Kind, uint8 ReachIndex, uint64 Cycles) + { + const uint8 KindIndex = static_cast(Kind); + if (KindIndex < static_cast(ETileReachCostKind::Count)) + { + GTileReachBlockCostCalls[KindIndex][ReachIndex].fetch_add( + 1, std::memory_order_relaxed); + GTileReachBlockCostCycles[KindIndex][ReachIndex].fetch_add( + Cycles, std::memory_order_relaxed); + } + } + + /** Inclusive timing split used for the pre-gate near/far measurement only. */ + struct FScopedReachCost + { + ETileReachCostKind Kind; + uint8 TileReachIndex = 1; + uint8 BlockReachIndex = 1; + uint64 StartCycles = 0; + bool bActive = false; + + explicit FScopedReachCost(ETileReachCostKind InKind) + : Kind(InKind) + { + if (!bTileReachDiagnosticTile + || !GTileReachCostDiagnosticsEnabled.load(std::memory_order_relaxed)) + { + return; + } + TileReachIndex = TileReachCostIndex(Kind); + BlockReachIndex = BlockReachCostIndex(Kind); + StartCycles = FPlatformTime::Cycles64(); + bActive = true; + } + + ~FScopedReachCost() + { + if (!bActive) + { + return; + } + const uint8 KindIndex = static_cast(Kind); + if (KindIndex < static_cast(ETileReachCostKind::Count)) + { + const uint64 Cycles = FPlatformTime::Cycles64() - StartCycles; + GTileReachCostCalls[KindIndex][TileReachIndex].fetch_add( + 1, std::memory_order_relaxed); + GTileReachCostCycles[KindIndex][TileReachIndex].fetch_add( + Cycles, + std::memory_order_relaxed); + if (bTileBlockReachValid + && TileReachBlockIndex >= 0 + && TileReachBlockIndex < TileReachMaxBlockCount) + { + RecordTileReachBlockCost(Kind, BlockReachIndex, Cycles); + } + } + } + }; + FORCEINLINE bool SameFloatBits(float A, float B) { uint32 ABits = 0;