diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index 55579df..d0f8d03 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -34,8 +34,356 @@ #include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01 #include "VoxelStrateTypes.h" #include "VoxelNoise.h" // Pure FBM used by the slab landing query +#include "VoxelPassageGeometry.h" #include "VoxelTerrainOpDefinition.h" +namespace +{ + // Density coordinates and cached cull radii are float-valued. FVector is double-valued in + // UE5, so using FVector::DistSquared here needlessly widens every candidate reject. Keep the + // cull in the same build-fixed float domain as the density query; the exact field output is + // intentionally no longer required to match the pre-optimization double round-trip. + FORCEINLINE float VF_FloatDistSquared( + float X, float Y, float Z, const FVector& Center) + { + const float DX = X - static_cast(Center.X); + const float DY = Y - static_cast(Center.Y); + const float DZ = Z - static_cast(Center.Z); + return DX * DX + DY * DY + DZ * DZ; + } + + FORCEINLINE float VF_DistanceSquaredToAabb( + float X, float Y, float Z, + const FVector& Min, + const FVector& Max) + { + const float DX = X < static_cast(Min.X) + ? static_cast(Min.X) - X + : (X > static_cast(Max.X) ? X - static_cast(Max.X) : 0.0f); + const float DY = Y < static_cast(Min.Y) + ? static_cast(Min.Y) - Y + : (Y > static_cast(Max.Y) ? Y - static_cast(Max.Y) : 0.0f); + const float DZ = Z < static_cast(Min.Z) + ? static_cast(Min.Z) - Z + : (Z > static_cast(Max.Z) ? Z - static_cast(Max.Z) : 0.0f); + return DX * DX + DY * DY + DZ * DZ; + } + + FORCEINLINE bool VF_IndexCell(float Coordinate, int32& OutCell) + { + const float Scaled = Coordinate / static_cast(FCaveSpatialIndex::BinSizeVoxels); + if (!FMath::IsFinite(Scaled) + || Scaled < static_cast(MIN_int32) + 1.0f + || Scaled > static_cast(MAX_int32) - 1.0f) + { + return false; + } + OutCell = FMath::FloorToInt(Scaled); + return true; + } + + FORCEINLINE bool VF_IndexEntryRange( + const FCaveSpatialIndexEntry& Entry, + int32& OutMinX, int32& OutMinY, int32& OutMinZ, + int32& OutMaxX, int32& OutMaxY, int32& OutMaxZ) + { + const float MinX = static_cast(FMath::Min(Entry.Min.X, Entry.Max.X)); + const float MinY = static_cast(FMath::Min(Entry.Min.Y, Entry.Max.Y)); + const float MinZ = static_cast(FMath::Min(Entry.Min.Z, Entry.Max.Z)); + const float MaxX = static_cast(FMath::Max(Entry.Min.X, Entry.Max.X)); + const float MaxY = static_cast(FMath::Max(Entry.Min.Y, Entry.Max.Y)); + const float MaxZ = static_cast(FMath::Max(Entry.Min.Z, Entry.Max.Z)); + return VF_IndexCell(MinX, OutMinX) + && VF_IndexCell(MinY, OutMinY) + && VF_IndexCell(MinZ, OutMinZ) + && VF_IndexCell(MaxX, OutMaxX) + && VF_IndexCell(MaxY, OutMaxY) + && VF_IndexCell(MaxZ, OutMaxZ) + && OutMinX <= OutMaxX + && OutMinY <= OutMaxY + && OutMinZ <= OutMaxZ; + } +} + +void FCaveSpatialIndex::Reset() +{ + bValid = false; + MinBinX = 0; + MinBinY = 0; + MinBinZ = 0; + CellsX = 0; + CellsY = 0; + CellsZ = 0; + Bins.Reset(); + CandidateIndices.Reset(); +} + +void FCaveSpatialIndex::Build(const TArray& Entries) +{ + Reset(); + + // An empty cache is a valid empty index. This keeps the query path branch-light while still + // making malformed/overflowing non-empty indexes fall back to the exact full scan. + if (Entries.Num() == 0) + { + bValid = true; + return; + } + + constexpr int64 MaxBins = 1ll << 20; + constexpr int64 MaxReferences = 1ll << 23; + int32 GlobalMinX = MAX_int32; + int32 GlobalMinY = MAX_int32; + int32 GlobalMinZ = MAX_int32; + int32 GlobalMaxX = MIN_int32; + int32 GlobalMaxY = MIN_int32; + int32 GlobalMaxZ = MIN_int32; + + TArray> MinCells; + TArray> MaxCells; + MinCells.SetNumUninitialized(Entries.Num()); + MaxCells.SetNumUninitialized(Entries.Num()); + + int64 TotalReferences = 0; + for (int32 EntryIndex = 0; EntryIndex < Entries.Num(); ++EntryIndex) + { + const FCaveSpatialIndexEntry& Entry = Entries[EntryIndex]; + if (Entry.CandidateIndex < 0) + { + return; + } + + int32 MinX, MinY, MinZ, MaxX, MaxY, MaxZ; + if (!VF_IndexEntryRange(Entry, MinX, MinY, MinZ, MaxX, MaxY, MaxZ)) + { + return; + } + + const int64 SpanX = static_cast(MaxX) - MinX + 1; + const int64 SpanY = static_cast(MaxY) - MinY + 1; + const int64 SpanZ = static_cast(MaxZ) - MinZ + 1; + const int64 References = SpanX * SpanY * SpanZ; + if (References <= 0 || References > MaxReferences + || TotalReferences > MaxReferences - References) + { + return; + } + + MinCells[EntryIndex] = FIntVector(MinX, MinY, MinZ); + MaxCells[EntryIndex] = FIntVector(MaxX, MaxY, MaxZ); + GlobalMinX = FMath::Min(GlobalMinX, MinX); + GlobalMinY = FMath::Min(GlobalMinY, MinY); + GlobalMinZ = FMath::Min(GlobalMinZ, MinZ); + GlobalMaxX = FMath::Max(GlobalMaxX, MaxX); + GlobalMaxY = FMath::Max(GlobalMaxY, MaxY); + GlobalMaxZ = FMath::Max(GlobalMaxZ, MaxZ); + TotalReferences += References; + } + + const int64 NumBins64 = + (static_cast(GlobalMaxX) - GlobalMinX + 1) + * (static_cast(GlobalMaxY) - GlobalMinY + 1) + * (static_cast(GlobalMaxZ) - GlobalMinZ + 1); + if (NumBins64 <= 0 || NumBins64 > MaxBins || TotalReferences > MAX_int32) + { + return; + } + + MinBinX = GlobalMinX; + MinBinY = GlobalMinY; + MinBinZ = GlobalMinZ; + CellsX = GlobalMaxX - GlobalMinX + 1; + CellsY = GlobalMaxY - GlobalMinY + 1; + CellsZ = GlobalMaxZ - GlobalMinZ + 1; + + const int32 NumBins = static_cast(NumBins64); + Bins.SetNumZeroed(NumBins); + TArray Counts; + Counts.Init(0, NumBins); + + auto BinNumber = [this](int32 CellX, int32 CellY, int32 CellZ) -> int32 + { + const int64 X = static_cast(CellX) - MinBinX; + const int64 Y = static_cast(CellY) - MinBinY; + const int64 Z = static_cast(CellZ) - MinBinZ; + return static_cast((Z * CellsY + Y) * CellsX + X); + }; + + for (int32 EntryIndex = 0; EntryIndex < Entries.Num(); ++EntryIndex) + { + const FIntVector MinCell = MinCells[EntryIndex]; + const FIntVector MaxCell = MaxCells[EntryIndex]; + for (int32 Z = MinCell.Z; Z <= MaxCell.Z; ++Z) + for (int32 Y = MinCell.Y; Y <= MaxCell.Y; ++Y) + for (int32 X = MinCell.X; X <= MaxCell.X; ++X) + { + ++Counts[BinNumber(X, Y, Z)]; + } + } + + int32 Running = 0; + for (int32 BinIndex = 0; BinIndex < NumBins; ++BinIndex) + { + Bins[BinIndex].First = Running; + Bins[BinIndex].Count = Counts[BinIndex]; + Running += Counts[BinIndex]; + } + + CandidateIndices.SetNumUninitialized(Running); + TArray WriteOffsets; + WriteOffsets.SetNumUninitialized(NumBins); + for (int32 BinIndex = 0; BinIndex < NumBins; ++BinIndex) + { + WriteOffsets[BinIndex] = Bins[BinIndex].First; + } + + // Entries are generated outermost by candidate ID and innermost by segment index. Therefore + // every bin receives nondecreasing IDs here. Duplicates are removed at query time with one + // LastCandidate comparison; preserving this order also preserves the old deterministic SDF + // reduction order. + for (int32 EntryIndex = 0; EntryIndex < Entries.Num(); ++EntryIndex) + { + const FCaveSpatialIndexEntry& Entry = Entries[EntryIndex]; + const FIntVector MinCell = MinCells[EntryIndex]; + const FIntVector MaxCell = MaxCells[EntryIndex]; + for (int32 Z = MinCell.Z; Z <= MaxCell.Z; ++Z) + for (int32 Y = MinCell.Y; Y <= MaxCell.Y; ++Y) + for (int32 X = MinCell.X; X <= MaxCell.X; ++X) + { + const int32 BinIndex = BinNumber(X, Y, Z); + CandidateIndices[WriteOffsets[BinIndex]++] = Entry.CandidateIndex; + } + } + + bValid = true; +} + +bool FCaveSpatialIndex::GetBinAt( + int32 CellX, int32 CellY, int32 CellZ, + int32& OutFirst, int32& OutCount) const +{ + OutFirst = 0; + OutCount = 0; + if (!bValid) + { + return false; + } + if (CellsX <= 0 || CellX < MinBinX || CellX >= MinBinX + CellsX + || CellY < MinBinY || CellY >= MinBinY + CellsY + || CellZ < MinBinZ || CellZ >= MinBinZ + CellsZ) + { + return true; + } + + const int64 LocalX = static_cast(CellX) - MinBinX; + const int64 LocalY = static_cast(CellY) - MinBinY; + const int64 LocalZ = static_cast(CellZ) - MinBinZ; + const int64 BinIndex = (LocalZ * CellsY + LocalY) * CellsX + LocalX; + const FCaveSpatialIndexBin& Bin = Bins[static_cast(BinIndex)]; + OutFirst = Bin.First; + OutCount = Bin.Count; + return true; +} + +bool FCaveSpatialIndex::GetBin( + float X, float Y, float Z, + int32& OutFirst, int32& OutCount) const +{ + OutFirst = 0; + OutCount = 0; + if (!bValid) + { + return false; + } + + int32 CellX, CellY, CellZ; + if (!VF_IndexCell(X, CellX) || !VF_IndexCell(Y, CellY) || !VF_IndexCell(Z, CellZ)) + { + return false; + } + return GetBinAt(CellX, CellY, CellZ, OutFirst, OutCount); +} + +template +bool FCaveSpatialIndex::GetColumnCandidates( + float X, float Y, TArray& OutCandidates) const +{ + OutCandidates.Reset(); + if (!bValid) + { + return false; + } + + int32 CellX, CellY; + if (!VF_IndexCell(X, CellX) || !VF_IndexCell(Y, CellY)) + { + return false; + } + if (CellsX <= 0 || CellX < MinBinX || CellX >= MinBinX + CellsX + || CellY < MinBinY || CellY >= MinBinY + CellsY) + { + return true; + } + + // The support decision is a column query, so intentionally inspect every height bin for this + // XY cell. This work occurs once per column, not once per sample; the regular SDF/core query + // still uses the full 3D bin lookup above. + for (int32 CellZ = MinBinZ; CellZ < MinBinZ + CellsZ; ++CellZ) + { + int32 First, Count; + GetBinAt(CellX, CellY, CellZ, First, Count); + for (int32 Offset = 0; Offset < Count; ++Offset) + { + const int32 Candidate = CandidateIndices[First + Offset]; + if (!OutCandidates.Contains(Candidate)) + { + OutCandidates.Add(Candidate); + } + } + } + return true; +} + +SIZE_T FChunkSDFCache::GetAllocatedSize() const +{ + SIZE_T Bytes = 0; + auto AddArray = [&Bytes](const auto& Array) + { + Bytes += static_cast(Array.GetAllocatedSize()); + }; + + AddArray(Rooms); + AddArray(RoomFloorJoins); + AddArray(Tunnels); + AddArray(Pits); + AddArray(Chimneys); + AddArray(Columns); + AddArray(SupportColumnEntries); + AddArray(SupportColumns); + for (const FTunnelSupportFloorColumn& Column : SupportColumns) + { + AddArray(Column.Intervals); + } + + for (const FCachedTunnel& Tunnel : Tunnels) + { + AddArray(Tunnel.ControlPoints); + AddArray(Tunnel.ControlRadii); + AddArray(Tunnel.WorldControlPoints); + AddArray(Tunnel.WorldControlRadii); + } + + const FCaveSpatialIndex* Indices[] = { + &RoomIndex, &RoomFloorJoinIndex, &TunnelIndex, &TunnelWorldIndex, + }; + for (const FCaveSpatialIndex* Index : Indices) + { + AddArray(Index->Bins); + AddArray(Index->CandidateIndices); + } + return Bytes; +} + //============================================================================= // INTERNAL: Full room data used during cache building only. // The CellX/CellY fields are needed for tunnel pair hashing but NOT for @@ -94,6 +442,258 @@ static void BakeRoomFeature( } } +namespace +{ + FORCEINLINE void VF_AddSpatialEntry( + TArray& Entries, + int32 CandidateIndex, + const FVector& InMin, + const FVector& InMax) + { + constexpr float Epsilon = 0.001f; + FCaveSpatialIndexEntry& Entry = Entries.Emplace_GetRef(); + Entry.CandidateIndex = CandidateIndex; + Entry.Min = FVector( + FMath::Min(InMin.X, InMax.X) - Epsilon, + FMath::Min(InMin.Y, InMax.Y) - Epsilon, + FMath::Min(InMin.Z, InMax.Z) - Epsilon); + Entry.Max = FVector( + FMath::Max(InMin.X, InMax.X) + Epsilon, + FMath::Max(InMin.Y, InMax.Y) + Epsilon, + FMath::Max(InMin.Z, InMax.Z) + Epsilon); + } + + FORCEINLINE void VF_AddSegmentSpatialEntry( + TArray& Entries, + int32 CandidateIndex, + const FVector& A, + const FVector& B, + float PadXY, + float PadZ) + { + const FVector Pad(PadXY, PadXY, PadZ); + VF_AddSpatialEntry(Entries, CandidateIndex, + FVector(FMath::Min(A.X, B.X), FMath::Min(A.Y, B.Y), FMath::Min(A.Z, B.Z)) - Pad, + FVector(FMath::Max(A.X, B.X), FMath::Max(A.Y, B.Y), FMath::Max(A.Z, B.Z)) + Pad); + } + + static void VF_AppendTunnelSpatialEntries( + TArray& Entries, + int32 TunnelIndex, + const TArray& Points, + const TArray& Radii, + float BlendPad, + float VerticalPad) + { + if (Points.Num() >= 2 && Radii.Num() == Points.Num()) + { + for (int32 SegmentIndex = 0; SegmentIndex + 1 < Points.Num(); ++SegmentIndex) + { + const float Radius = FMath::Max( + FMath::Abs(Radii[SegmentIndex]), + FMath::Abs(Radii[SegmentIndex + 1])); + VF_AddSegmentSpatialEntry( + Entries, TunnelIndex, Points[SegmentIndex], Points[SegmentIndex + 1], + Radius + BlendPad, Radius + VerticalPad); + } + return; + } + + // Keep the descriptor compatibility path conservative as well. Current caches always use + // the chain, but cooked/diagnostic caches can still contain the old midpoint descriptor. + if (Points.Num() >= 2) + { + VF_AddSegmentSpatialEntry( + Entries, TunnelIndex, Points[0], Points[1], + BlendPad, VerticalPad); + } + } + + static void VF_AppendTunnelDescriptorSpatialEntries( + TArray& Entries, + int32 TunnelIndex, + const FCachedTunnel& Tunnel, + float BlendPad, + float VerticalPad) + { + if (Tunnel.ControlPoints.Num() >= 2 + && Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num()) + { + VF_AppendTunnelSpatialEntries( + Entries, TunnelIndex, Tunnel.ControlPoints, Tunnel.ControlRadii, + BlendPad, VerticalPad); + return; + } + + // Keep old cooked/diagnostic descriptors conservative too. The current + // cache always has a control chain, but the evaluator still supports the + // endpoint/midpoint representation and its index must cover that path. + if (Tunnel.bHasMidpoint) + { + const float RadiusA = FMath::Max( + FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusMid)); + const float RadiusB = FMath::Max( + FMath::Abs(Tunnel.RadiusMid), FMath::Abs(Tunnel.RadiusB)); + VF_AddSegmentSpatialEntry( + Entries, TunnelIndex, Tunnel.EndpointA, Tunnel.Midpoint, + RadiusA + BlendPad, RadiusA + VerticalPad); + VF_AddSegmentSpatialEntry( + Entries, TunnelIndex, Tunnel.Midpoint, Tunnel.EndpointB, + RadiusB + BlendPad, RadiusB + VerticalPad); + } + else + { + const float Radius = FMath::Max( + FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusB)); + VF_AddSegmentSpatialEntry( + Entries, TunnelIndex, Tunnel.EndpointA, Tunnel.EndpointB, + Radius + BlendPad, Radius + VerticalPad); + } + } + + static void VF_FinalizeTunnelBroadPhaseBounds(FCachedTunnel& Tunnel, float BlendK) + { + const float BlendPad = FMath::Max(BlendK, 0.0f); + // Detail modifiers run while CaveSDF is below 3*K, so the source + // broad phase must cover that full downstream reach. + const float SDFDetailPad = BlendPad * 3.0f; + const float WorldFloorPad = FMath::Max( + VoxelPassageGeometry::LandingFloorThicknessVoxels, BlendPad); + + if (Tunnel.ControlPoints.Num() >= 2 + && Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num()) + { + Tunnel.SDFCenterlineMin = Tunnel.ControlPoints[0]; + Tunnel.SDFCenterlineMax = Tunnel.ControlPoints[0]; + float MaxRadius = 0.0f; + for (int32 Index = 0; Index < Tunnel.ControlPoints.Num(); ++Index) + { + const FVector& Point = Tunnel.ControlPoints[Index]; + Tunnel.SDFCenterlineMin.X = FMath::Min(Tunnel.SDFCenterlineMin.X, Point.X); + Tunnel.SDFCenterlineMin.Y = FMath::Min(Tunnel.SDFCenterlineMin.Y, Point.Y); + Tunnel.SDFCenterlineMin.Z = FMath::Min(Tunnel.SDFCenterlineMin.Z, Point.Z); + Tunnel.SDFCenterlineMax.X = FMath::Max(Tunnel.SDFCenterlineMax.X, Point.X); + Tunnel.SDFCenterlineMax.Y = FMath::Max(Tunnel.SDFCenterlineMax.Y, Point.Y); + Tunnel.SDFCenterlineMax.Z = FMath::Max(Tunnel.SDFCenterlineMax.Z, Point.Z); + MaxRadius = FMath::Max(MaxRadius, FMath::Abs(Tunnel.ControlRadii[Index])); + } + Tunnel.SDFInfluenceRadius = MaxRadius + SDFDetailPad; + } + else + { + Tunnel.SDFCenterlineMin = Tunnel.EndpointA; + Tunnel.SDFCenterlineMax = Tunnel.EndpointA; + Tunnel.SDFCenterlineMin.X = FMath::Min(Tunnel.SDFCenterlineMin.X, Tunnel.EndpointB.X); + Tunnel.SDFCenterlineMin.Y = FMath::Min(Tunnel.SDFCenterlineMin.Y, Tunnel.EndpointB.Y); + Tunnel.SDFCenterlineMin.Z = FMath::Min(Tunnel.SDFCenterlineMin.Z, Tunnel.EndpointB.Z); + Tunnel.SDFCenterlineMax.X = FMath::Max(Tunnel.SDFCenterlineMax.X, Tunnel.EndpointB.X); + Tunnel.SDFCenterlineMax.Y = FMath::Max(Tunnel.SDFCenterlineMax.Y, Tunnel.EndpointB.Y); + Tunnel.SDFCenterlineMax.Z = FMath::Max(Tunnel.SDFCenterlineMax.Z, Tunnel.EndpointB.Z); + float MaxRadius = FMath::Max( + FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusB)); + if (Tunnel.bHasMidpoint) + { + Tunnel.SDFCenterlineMin.X = FMath::Min(Tunnel.SDFCenterlineMin.X, Tunnel.Midpoint.X); + Tunnel.SDFCenterlineMin.Y = FMath::Min(Tunnel.SDFCenterlineMin.Y, Tunnel.Midpoint.Y); + Tunnel.SDFCenterlineMin.Z = FMath::Min(Tunnel.SDFCenterlineMin.Z, Tunnel.Midpoint.Z); + Tunnel.SDFCenterlineMax.X = FMath::Max(Tunnel.SDFCenterlineMax.X, Tunnel.Midpoint.X); + Tunnel.SDFCenterlineMax.Y = FMath::Max(Tunnel.SDFCenterlineMax.Y, Tunnel.Midpoint.Y); + Tunnel.SDFCenterlineMax.Z = FMath::Max(Tunnel.SDFCenterlineMax.Z, Tunnel.Midpoint.Z); + MaxRadius = FMath::Max(MaxRadius, FMath::Abs(Tunnel.RadiusMid)); + } + Tunnel.SDFInfluenceRadius = MaxRadius + SDFDetailPad; + } + + if (Tunnel.WorldControlPoints.Num() >= 2 + && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num()) + { + Tunnel.WorldCenterlineMin = Tunnel.WorldControlPoints[0]; + Tunnel.WorldCenterlineMax = Tunnel.WorldControlPoints[0]; + float MaxRadius = 0.0f; + for (int32 Index = 0; Index < Tunnel.WorldControlPoints.Num(); ++Index) + { + const FVector& Point = Tunnel.WorldControlPoints[Index]; + Tunnel.WorldCenterlineMin.X = FMath::Min(Tunnel.WorldCenterlineMin.X, Point.X); + Tunnel.WorldCenterlineMin.Y = FMath::Min(Tunnel.WorldCenterlineMin.Y, Point.Y); + Tunnel.WorldCenterlineMin.Z = FMath::Min(Tunnel.WorldCenterlineMin.Z, Point.Z); + Tunnel.WorldCenterlineMax.X = FMath::Max(Tunnel.WorldCenterlineMax.X, Point.X); + Tunnel.WorldCenterlineMax.Y = FMath::Max(Tunnel.WorldCenterlineMax.Y, Point.Y); + Tunnel.WorldCenterlineMax.Z = FMath::Max(Tunnel.WorldCenterlineMax.Z, Point.Z); + MaxRadius = FMath::Max(MaxRadius, FMath::Abs(Tunnel.WorldControlRadii[Index])); + } + Tunnel.WorldInfluenceRadius = MaxRadius + WorldFloorPad; + } + else + { + Tunnel.WorldCenterlineMin = Tunnel.SDFCenterlineMin; + Tunnel.WorldCenterlineMax = Tunnel.SDFCenterlineMax; + Tunnel.WorldInfluenceRadius = Tunnel.SDFInfluenceRadius + - SDFDetailPad + WorldFloorPad; + } + } + + static void VF_BuildCacheSpatialIndices( + FChunkSDFCache& Cache, + float BlendK) + { + TArray RoomEntries; + TArray JoinEntries; + TArray TunnelEntries; + TArray WorldTunnelEntries; + RoomEntries.Reserve(Cache.Rooms.Num()); + JoinEntries.Reserve(Cache.RoomFloorJoins.Num()); + + for (int32 Index = 0; Index < Cache.Rooms.Num(); ++Index) + { + const FCachedRoom& Room = Cache.Rooms[Index]; + const float Radius = FMath::Sqrt(FMath::Max(Room.CullRadiusSq, 0.0f)); + const FVector Extent(Radius, Radius, Radius); + VF_AddSpatialEntry(RoomEntries, Index, Room.Center - Extent, Room.Center + Extent); + } + for (int32 Index = 0; Index < Cache.RoomFloorJoins.Num(); ++Index) + { + const FCachedRoomFloorJoin& Join = Cache.RoomFloorJoins[Index]; + const float Radius = FMath::Sqrt(FMath::Max(Join.BoundRadiusSq, 0.0f)); + const FVector Extent(Radius, Radius, Radius); + VF_AddSpatialEntry(JoinEntries, Index, + Join.BoundCenter - Extent, Join.BoundCenter + Extent); + } + + for (int32 Index = 0; Index < Cache.Tunnels.Num(); ++Index) + { + FCachedTunnel& Tunnel = Cache.Tunnels[Index]; + VF_FinalizeTunnelBroadPhaseBounds(Tunnel, BlendK); + + const float SDFPad = FMath::Max(BlendK, 0.0f) * 3.0f; + VF_AppendTunnelDescriptorSpatialEntries( + TunnelEntries, Index, Tunnel, SDFPad, SDFPad); + + const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2 + && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num(); + if (bHasWorldChain) + { + VF_AppendTunnelSpatialEntries( + WorldTunnelEntries, Index, + Tunnel.WorldControlPoints, Tunnel.WorldControlRadii, + VoxelPassageGeometry::LandingFloorThicknessVoxels, + VoxelPassageGeometry::LandingFloorThicknessVoxels); + } + else + { + VF_AppendTunnelDescriptorSpatialEntries( + WorldTunnelEntries, Index, Tunnel, + VoxelPassageGeometry::LandingFloorThicknessVoxels, + VoxelPassageGeometry::LandingFloorThicknessVoxels); + } + } + + Cache.RoomIndex.Build(RoomEntries); + Cache.RoomFloorJoinIndex.Build(JoinEntries); + Cache.TunnelIndex.Build(TunnelEntries); + Cache.TunnelWorldIndex.Build(WorldTunnelEntries); + } +} + namespace { // Runtime landing queries use the same pinned capsule dimensions and support rules as the @@ -2672,6 +3272,16 @@ void VoxelCaveMorphology::BuildChunkCache( OutCache.Pits.Reset(); OutCache.Chimneys.Reset(); OutCache.Columns.Reset(); + OutCache.RoomIndex.Reset(); + OutCache.RoomFloorJoinIndex.Reset(); + OutCache.TunnelIndex.Reset(); + OutCache.TunnelWorldIndex.Reset(); + OutCache.SupportColumnMinX = 0; + OutCache.SupportColumnMinY = 0; + OutCache.SupportColumnCellsX = 0; + OutCache.SupportColumnCellsY = 0; + OutCache.SupportColumnEntries.Reset(); + OutCache.SupportColumns.Reset(); // Combine world seed with strate index so each strate gets unique caves const uint32 StrateSeed = VoxelCaveMorphology::MakeStrateSeed(Seed, StrateIndex); @@ -2687,12 +3297,21 @@ void VoxelCaveMorphology::BuildChunkCache( // of a box can touch a voxel inside that box. // Envelope conservatif / conservative bound: Lerp accepts inverted endpoints, // so max(Min, Max) covers either radius without changing the authored roll. - const float RoomRadiusEnvelope = FMath::Max(Params.MinRoomRadius, Params.MaxRoomRadius); - const float TunnelRadiusEnvelope = FMath::Max(Params.TunnelMinRadius, Params.TunnelMaxRadius); + const float RoomRadiusEnvelope = FMath::Max( + FMath::Abs(Params.MinRoomRadius), FMath::Abs(Params.MaxRoomRadius)); + // Interior control points can carry the authored radius variation (+/-18%). Include that + // reach in collection; the per-tunnel index below uses each baked radius exactly. + const float TunnelRadiusEnvelope = FMath::Max( + 0.5f, + FMath::Max(FMath::Abs(Params.TunnelMinRadius), FMath::Abs(Params.TunnelMaxRadius)) + * 1.18f); + const float FloorReliefEnvelope = FMath::Abs(Params.FloorReliefStrength) + * VOXEL_NOISE_SCALE * 1.5f; + const float BlendEnvelope = FMath::Max(Params.SDFBlendRadius, 0.0f); const float MaxInfluence = FMath::Max( - RoomRadiusEnvelope, - Params.TunnelWarpStrength + TunnelRadiusEnvelope - ) + Params.SDFBlendRadius; + RoomRadiusEnvelope + FloorReliefEnvelope, + FMath::Abs(Params.TunnelWarpStrength) + TunnelRadiusEnvelope + ) + BlendEnvelope; const float MaxTunnelLen = FMath::Max(Params.MaxTunnelLength, 0.0f); @@ -2746,16 +3365,30 @@ void VoxelCaveMorphology::BuildChunkCache( const float BlendK = Params.SDFBlendRadius; - // "This room can reach the search box" test, using the room's OWN reach — the same - // extent formula as its per-voxel cull sphere (1.5x radius for capsule-stretched - // variants + blend margin). The old test compared the center against a box inflated + // "This room can reach the search box" test, using the room's OWN reach. The old test + // compared the center against a box inflated // by the SHARED MaxInfluence, which silently assumed every room's reach <= MaxInfluence. // That's FALSE for the origin room (OriginRoomRadius >> MaxRoomRadius) — chunks inside // the big room but > MaxInfluence from (0,0) didn't store it, so its carve clipped at an // arbitrary chunk-aligned radius — and slightly false even for hash rooms (1.5x stretch). + const auto RoomShapeReachUpperBound = [](float RadiusXY, float RadiusZ) -> float + { + const float RXY = FMath::Abs(RadiusXY); + const float RZ = FMath::Abs(RadiusZ); + const float Ellipsoid = FMath::Max(RXY, RZ); + const FVector BoxExtent( + RXY * 0.8f + RXY * 0.25f, + RXY * 0.8f + RXY * 0.25f, + RZ * 0.8f + RXY * 0.25f); + const float RoundedBox = BoxExtent.Size(); + const float Capsule = RXY * 0.7f + FMath::Min(RXY * 0.6f, RZ); + return FMath::Max3(Ellipsoid, RoundedBox, Capsule); + }; + auto RoomReachesSearchBox = [&](const FVector& C, float RadiusXY, float RadiusZ) -> bool { - const float Reach = FMath::Max(RadiusXY * 1.5f, RadiusZ) + BlendK * 3.0f; + const float ShapeReach = RoomShapeReachUpperBound(RadiusXY, RadiusZ); + const float Reach = ShapeReach + FloorReliefEnvelope + BlendEnvelope * 3.0f; const float dx = FMath::Max3((float)(SearchMinX - C.X), 0.0f, (float)(C.X - SearchMaxX)); const float dy = FMath::Max3((float)(SearchMinY - C.Y), 0.0f, (float)(C.Y - SearchMaxY)); return (dx * dx + dy * dy) <= Reach * Reach; @@ -2840,7 +3473,11 @@ void VoxelCaveMorphology::BuildChunkCache( } const int32 NumRooms = BuildRooms.Num(); - if (NumRooms == 0) return; + if (NumRooms == 0) + { + VF_BuildCacheSpatialIndices(OutCache, BlendK); + return; + } // Keep the room floor calculation in one place so tunnel mouths and overlap joins use the // same deterministic support plane as the emitted room. It includes the room-centre relief; @@ -3260,7 +3897,7 @@ void VoxelCaveMorphology::BuildChunkCache( BoundR = FMath::Max( BoundR, FVector::Dist(CT.BoundCenter, CT.ControlPoints[PointIndex])); } - BoundR += MaxTunnelRadius + BlendK; + BoundR += MaxTunnelRadius + FMath::Max(BlendK, 0.0f); CT.BoundRadiusSq = BoundR * BoundR; // Keep a separate bound for the world-space structural backstop. The SDF and world @@ -3282,7 +3919,11 @@ void VoxelCaveMorphology::BuildChunkCache( WorldBoundR, FVector::Dist(CT.WorldBoundCenter, CT.WorldControlPoints[PointIndex])); } - WorldBoundR += MaxTunnelRadius + BlendK; + // The support predicate reaches LandingFloorThickness below the centreline floor; + // the world sphere must cover that slab, not only the blended SDF tube. + WorldBoundR += MaxTunnelRadius + FMath::Max( + VoxelPassageGeometry::LandingFloorThicknessVoxels, + FMath::Max(BlendK, 0.0f)); CT.WorldBoundRadiusSq = WorldBoundR * WorldBoundR; // Store if either representation can reach a voxel in this chunk. The warped SDF @@ -3401,7 +4042,7 @@ void VoxelCaveMorphology::BuildChunkCache( Join.CeilingZ = CommonFloor + JoinRequiredHeight; Join.BoundCenter = (Join.Start + Join.End) * 0.5f; const float BoundRadius = 0.5f * FVector::Dist(Join.Start, Join.End) - + Join.Radius + BlendK; + + FMath::Abs(Join.Radius) + FMath::Max(BlendK, 0.0f); Join.BoundRadiusSq = BoundRadius * BoundRadius; if (SphereTouchesSearchXY(Join.BoundCenter, Join.BoundRadiusSq)) @@ -3454,8 +4095,10 @@ void VoxelCaveMorphology::BuildChunkCache( // Cull radius: max extent the room can reach + blend margin. // 1.5x accounts for capsule shapes extending beyond nominal radius. - float MaxExtent = FMath::Max(BR.RadiusXY * 1.5f, BR.RadiusZ) + BlendK * 3.0f; - CR.CullRadiusSq = MaxExtent * MaxExtent; + float MaxExtent = FMath::Max( + FMath::Abs(BR.RadiusXY) * 1.5f, + FMath::Abs(BR.RadiusZ)); + MaxExtent += FloorReliefEnvelope + BlendEnvelope * 3.0f; // --- PRE-BAKED SHAPE --- // Same hash roll + thresholds + capsule trig the evaluator used to redo PER VOXEL; @@ -3490,6 +4133,33 @@ void VoxelCaveMorphology::BuildChunkCache( CR.ShapeType = 0; CR.ShapeA = FVector(CR.RadiusXY, CR.RadiusXY, CR.RadiusZ); } + + // Use the actual pre-baked shape's farthest possible distance, not a guessed 1.5x + // sphere. The old guess underbounded a rounded-box corner when RadiusZ was large. + float ShapeReach = 0.0f; + if (CR.ShapeType == 1) + { + const float Round = FMath::Abs(CR.ShapeR); + const FVector Extent( + FMath::Abs(CR.ShapeA.X) + Round, + FMath::Abs(CR.ShapeA.Y) + Round, + FMath::Abs(CR.ShapeA.Z) + Round); + ShapeReach = Extent.Size(); + } + else if (CR.ShapeType == 2) + { + ShapeReach = 0.5f * FVector::Dist(CR.ShapeA, CR.ShapeB) + + FMath::Abs(CR.ShapeR); + } + else + { + ShapeReach = FMath::Max3( + FMath::Abs(CR.ShapeA.X), + FMath::Abs(CR.ShapeA.Y), + FMath::Abs(CR.ShapeA.Z)); + } + MaxExtent = ShapeReach + FloorReliefEnvelope + BlendEnvelope * 3.0f; + CR.CullRadiusSq = MaxExtent * MaxExtent; } // Flat floor cut: soft floor plane per room, hash-rolled from [Min, Max]. @@ -3542,6 +4212,76 @@ void VoxelCaveMorphology::BuildChunkCache( } } + // ARCHES/PINCHES — all geometry below is a pure function of the room hash and the + // room-local terrain parameters. Bake the hash chains and the two trigonometric pairs + // once per room instead of once per near-surface voxel. The active decision uses the + // same `ToFloat01(H) > Density` predicate as the old loop; only its location changes. + FStrateGenerationParams FeatureParams = Params; + if (CR.RoomOp) + { + CR.RoomOp->ApplyTo(FeatureParams, CR.RoomOpWeight); + } + for (int32 i = 0; i < 3; ++i) + { + const uint32 AH = VoxelHash::Mix(CR.Hash ^ (0xA4C400u + (uint32)i * 7369u)); + if (VoxelHash::ToFloat01(AH) <= FeatureParams.ArchDensity) + { + const uint32 AH2 = VoxelHash::Mix(AH ^ 0xA4C4u); + const float ArcCX = CR.Center.X + + VoxelHash::ToFloatSigned(AH2) * CR.RadiusXY * 0.3f; + const float ArcCY = CR.Center.Y + + VoxelHash::ToFloatSigned(VoxelHash::Mix(AH2)) * CR.RadiusXY * 0.3f; + const uint32 AH3 = VoxelHash::Mix(AH2 ^ 0xB41Du); + const float ArcCZ = CR.Center.Z + + VoxelHash::ToFloatSigned(AH3) * CR.RadiusZ * 0.4f; + const uint32 AH4 = VoxelHash::Mix(AH3 ^ 0xCAFEu); + const float Angle = VoxelHash::ToFloat01(AH4) * PI; + const float HalfSpan = CR.RadiusXY + * (0.5f + VoxelHash::ToFloat01(VoxelHash::Mix(AH4)) * 0.35f); + const float CosA = FMath::Cos(Angle); + const float SinA = FMath::Sin(Angle); + FCachedArch& Arch = CR.Arches[i]; + Arch.bActive = true; + Arch.EndpointA = FVector(ArcCX - CosA * HalfSpan, + ArcCY - SinA * HalfSpan, ArcCZ); + Arch.EndpointB = FVector(ArcCX + CosA * HalfSpan, + ArcCY + SinA * HalfSpan, ArcCZ); + const uint32 AH5 = VoxelHash::Mix(AH4 ^ 0xF00Du); + Arch.Radius = FMath::Lerp( + FeatureParams.ArchMinRadius, FeatureParams.ArchMaxRadius, + VoxelHash::ToFloat01(AH5)); + Arch.BaseDensity = FeatureParams.BaseDensity; + } + + const uint32 PnH = VoxelHash::Mix(CR.Hash ^ (0xF1C400u + (uint32)i * 5417u)); + if (VoxelHash::ToFloat01(PnH) <= FeatureParams.PinchDensity) + { + const uint32 PnH2 = VoxelHash::Mix(PnH ^ 0xF1C4u); + const float PnX = CR.Center.X + + VoxelHash::ToFloatSigned(PnH2) * CR.RadiusXY * 0.85f; + const float PnY = CR.Center.Y + + VoxelHash::ToFloatSigned(VoxelHash::Mix(PnH2)) * CR.RadiusXY * 0.85f; + const uint32 PnH3 = VoxelHash::Mix(PnH2 ^ 0x5432u); + const float PnZ = CR.Center.Z + + VoxelHash::ToFloatSigned(PnH3) * CR.RadiusZ * 0.5f; + const uint32 PnH4 = VoxelHash::Mix(PnH3 ^ 0x9A3Bu); + const float PnAngle = VoxelHash::ToFloat01(PnH4) * PI; + FCachedPinch& Pinch = CR.Pinches[i]; + Pinch.bActive = true; + Pinch.CenterX = PnX; + Pinch.CenterY = PnY; + Pinch.CenterZ = PnZ; + Pinch.CosAngle = FMath::Cos(PnAngle); + Pinch.SinAngle = FMath::Sin(PnAngle); + Pinch.MaxExtent = FMath::Max( + FeatureParams.PinchLength, FeatureParams.PinchStrength) + 5.0f; + Pinch.HalfLength = FeatureParams.PinchLength * 0.5f; + Pinch.HalfNarrow = FeatureParams.PinchStrength; + Pinch.HalfVertical = FeatureParams.PinchStrength * 1.5f; + Pinch.BaseDensity = FeatureParams.BaseDensity; + } + } + OutCache.Rooms.Add(CR); //--------------------------------------------------------------------- @@ -3615,8 +4355,12 @@ void VoxelCaveMorphology::BuildChunkCache( const float MaxXYR = ColR + 6.0f; Col.BoundXYRadiusSq = MaxXYR * MaxXYR; OutCache.Columns.Add(Col); - }); + }); } + + // All candidate arrays are complete now. Build the immutable broad phase only after every + // deterministic room/tunnel decision and feature bake has finished. + VF_BuildCacheSpatialIndices(OutCache, BlendK); } //============================================================================= @@ -3644,26 +4388,27 @@ float VoxelCaveMorphology::EvaluateSDFCached( float NearestRoomRawSDF = FLT_MAX; int32 NearestIdx = -1; + int32 RoomFirst = 0; + int32 RoomCount = 0; + const bool bIndexedRooms = Cache.RoomIndex.GetBin( + WorldX, WorldY, WorldZ, RoomFirst, RoomCount); if (VoxelDensityProfile::IsEnabled()) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::CaveRoomCandidates, - static_cast(Cache.Rooms.Num())); - VoxelDensityProfile::AddCounter( - VoxelDensityProfile::ECounter::CaveTunnelCandidates, - static_cast(Cache.Tunnels.Num())); + static_cast(bIndexedRooms ? RoomCount : Cache.Rooms.Num())); } //========================================================================= // Room SDFs //========================================================================= - for (int32 RoomIdx = 0; RoomIdx < Cache.Rooms.Num(); ++RoomIdx) + auto EvaluateRoom = [&](int32 RoomIdx) { const FCachedRoom& Room = Cache.Rooms[RoomIdx]; // --- DISTANCE CULL --- - const float DistSq = FVector::DistSquared(Pos, Room.Center); - if (DistSq > Room.CullRadiusSq) continue; + const float DistSq = VF_FloatDistSquared(WorldX, WorldY, WorldZ, Room.Center); + if (DistSq > Room.CullRadiusSq) return; if (VoxelDensityProfile::IsEnabled()) { VoxelDensityProfile::AddCounter( @@ -3706,6 +4451,21 @@ float VoxelCaveMorphology::EvaluateSDFCached( } MinSDF = VoxelSDF::SmoothMin(MinSDF, RoomSDF, BlendK); + }; + + if (bIndexedRooms) + { + for (int32 Offset = 0; Offset < RoomCount; ++Offset) + { + EvaluateRoom(Cache.RoomIndex.CandidateIndices[RoomFirst + Offset]); + } + } + else + { + for (int32 RoomIdx = 0; RoomIdx < Cache.Rooms.Num(); ++RoomIdx) + { + EvaluateRoom(RoomIdx); + } } //========================================================================== @@ -3715,10 +4475,15 @@ float VoxelCaveMorphology::EvaluateSDFCached( // single horizontal plane, so a player crossing the shared volume cannot meet the tall lip // produced by two independent floor cuts. They are deliberately not reported as rooms and do // not participate in nearest-room terrain-op ownership. - for (const FCachedRoomFloorJoin& Join : Cache.RoomFloorJoins) + int32 JoinFirst = 0; + int32 JoinCount = 0; + const bool bIndexedJoins = Cache.RoomFloorJoinIndex.GetBin( + WorldX, WorldY, WorldZ, JoinFirst, JoinCount); + auto EvaluateJoin = [&](int32 JoinIdx) { - const float DistSq = FVector::DistSquared(Pos, Join.BoundCenter); - if (DistSq > Join.BoundRadiusSq) continue; + const FCachedRoomFloorJoin& Join = Cache.RoomFloorJoins[JoinIdx]; + const float DistSq = VF_FloatDistSquared(WorldX, WorldY, WorldZ, Join.BoundCenter); + if (DistSq > Join.BoundRadiusSq) return; const float HorizontalSDF = VoxelSDF::Capsule( Pos, Join.Start, Join.End, Join.Radius); @@ -3727,16 +4492,50 @@ float VoxelCaveMorphology::EvaluateSDFCached( Pos.Z - Join.CeilingZ); const float JoinSDF = FMath::Max(HorizontalSDF, VerticalSDF); MinSDF = VoxelSDF::SmoothMin(MinSDF, JoinSDF, BlendK); + }; + if (bIndexedJoins) + { + for (int32 Offset = 0; Offset < JoinCount; ++Offset) + { + EvaluateJoin(Cache.RoomFloorJoinIndex.CandidateIndices[JoinFirst + Offset]); + } + } + else + { + for (int32 JoinIdx = 0; JoinIdx < Cache.RoomFloorJoins.Num(); ++JoinIdx) + { + EvaluateJoin(JoinIdx); + } } //========================================================================= // Tunnel SDFs //========================================================================= - for (const FCachedTunnel& Tunnel : Cache.Tunnels) + int32 TunnelFirst = 0; + int32 TunnelCount = 0; + const bool bIndexedTunnels = Cache.TunnelIndex.GetBin( + WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount); + if (VoxelDensityProfile::IsEnabled()) { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::CaveTunnelCandidates, + static_cast(bIndexedTunnels ? TunnelCount : Cache.Tunnels.Num())); + } + + auto EvaluateTunnel = [&](int32 TunnelIdx) + { + const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; + if (Tunnel.SDFInfluenceRadius > 0.0f + && VF_DistanceSquaredToAabb( + WorldX, WorldY, WorldZ, + Tunnel.SDFCenterlineMin, Tunnel.SDFCenterlineMax) + > FMath::Square(Tunnel.SDFInfluenceRadius)) + { + return; + } // --- BOUNDING SPHERE CULL --- - const float DistSq = FVector::DistSquared(Pos, Tunnel.BoundCenter); - if (DistSq > Tunnel.BoundRadiusSq) continue; + const float DistSq = VF_FloatDistSquared(WorldX, WorldY, WorldZ, Tunnel.BoundCenter); + if (DistSq > Tunnel.BoundRadiusSq) return; if (VoxelDensityProfile::IsEnabled()) { VoxelDensityProfile::AddCounter( @@ -3783,6 +4582,25 @@ float VoxelCaveMorphology::EvaluateSDFCached( } MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK); + }; + + if (bIndexedTunnels) + { + int32 LastTunnelIdx = INDEX_NONE; + for (int32 Offset = 0; Offset < TunnelCount; ++Offset) + { + const int32 TunnelIdx = Cache.TunnelIndex.CandidateIndices[TunnelFirst + Offset]; + if (TunnelIdx == LastTunnelIdx) continue; + LastTunnelIdx = TunnelIdx; + EvaluateTunnel(TunnelIdx); + } + } + else + { + for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx) + { + EvaluateTunnel(TunnelIdx); + } } // Write nearest room index for the caller (terrain ops system) @@ -3801,11 +4619,25 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF( const FVector Pos(WorldX, WorldY, WorldZ); float MinSDF = FLT_MAX; - for (const FCachedTunnel& Tunnel : Cache.Tunnels) + int32 TunnelFirst = 0; + int32 TunnelCount = 0; + const bool bIndexedTunnels = Cache.TunnelIndex.GetBin( + WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount); + auto EvaluateTunnel = [&](int32 TunnelIdx) { - if (FVector::DistSquared(Pos, Tunnel.BoundCenter) > Tunnel.BoundRadiusSq) + const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; + if (Tunnel.SDFInfluenceRadius > 0.0f + && VF_DistanceSquaredToAabb( + WorldX, WorldY, WorldZ, + Tunnel.SDFCenterlineMin, Tunnel.SDFCenterlineMax) + > FMath::Square(Tunnel.SDFInfluenceRadius)) { - continue; + return; + } + if (VF_FloatDistSquared(WorldX, WorldY, WorldZ, Tunnel.BoundCenter) + > Tunnel.BoundRadiusSq) + { + return; } if (Tunnel.ControlPoints.Num() >= 2 @@ -3846,6 +4678,25 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF( Pos, Tunnel.EndpointA, Tunnel.EndpointB, Tunnel.RadiusA, Tunnel.RadiusB)); } + }; + + if (bIndexedTunnels) + { + int32 LastTunnelIdx = INDEX_NONE; + for (int32 Offset = 0; Offset < TunnelCount; ++Offset) + { + const int32 TunnelIdx = Cache.TunnelIndex.CandidateIndices[TunnelFirst + Offset]; + if (TunnelIdx == LastTunnelIdx) continue; + LastTunnelIdx = TunnelIdx; + EvaluateTunnel(TunnelIdx); + } + } + else + { + for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx) + { + EvaluateTunnel(TunnelIdx); + } } return MinSDF; @@ -3853,30 +4704,54 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF( FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( float WorldX, float WorldY, float WorldZ, - const FChunkSDFCache& Cache) + const FChunkSDFCache& Cache, + const FTunnelSupportFloorColumn* SupportColumn) { const FVector Pos(WorldX, WorldY, WorldZ); FTunnelCoreWorldEvaluation Result; + int32 TunnelFirst = 0; + int32 TunnelCount = 0; + const bool bIndexedTunnels = Cache.TunnelWorldIndex.GetBin( + WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount); + if (SupportColumn != nullptr && VoxelDensityProfile::IsEnabled()) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TunnelSupportFloorQueries); + } if (VoxelDensityProfile::IsEnabled()) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TunnelCoreCandidates, - static_cast(Cache.Tunnels.Num())); + static_cast(bIndexedTunnels ? TunnelCount : Cache.Tunnels.Num())); } - for (const FCachedTunnel& Tunnel : Cache.Tunnels) + auto EvaluateTunnel = [&](int32 TunnelIdx) { + const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; 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; - if (BoundRadiusSq > 0.0f - && FVector::DistSquared(Pos, BoundCenter) > 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 (InfluenceRadius > 0.0f + && VF_DistanceSquaredToAabb( + WorldX, WorldY, WorldZ, CenterlineMin, CenterlineMax) + > FMath::Square(InfluenceRadius)) { - continue; + return; + } + if (BoundRadiusSq > 0.0f + && VF_FloatDistSquared(WorldX, WorldY, WorldZ, BoundCenter) > BoundRadiusSq) + { + return; } if (VoxelDensityProfile::IsEnabled()) { @@ -3890,18 +4765,31 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( ? Tunnel.WorldControlRadii : Tunnel.ControlRadii; if (bHasWorldChain) { + if (SupportColumn != nullptr && VoxelDensityProfile::IsEnabled()) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TunnelSupportFloorChecks); + } float FloorZ = 0.0f; float SupportRadius = 0.0f; - const bool bProjectedFloor = VoxelPassageGeometry::ProjectWalkableTunnelFloor( - ControlPoints, ControlRadii, Pos, FloorZ, SupportRadius); - const bool bWithinWorldBounds = Tunnel.WorldBoundRadiusSq <= 0.0f - || FVector::DistSquared(Pos, Tunnel.WorldBoundCenter) - <= Tunnel.WorldBoundRadiusSq; - if (bProjectedFloor && bWithinWorldBounds + float SupportMinZ = 0.0f; + float SupportMaxZ = 0.0f; + const bool bProjectedFloor = SupportColumn != nullptr + ? VoxelCaveMorphology::GetTunnelSupportFloorColumnBand( + TunnelIdx, *SupportColumn, FloorZ, SupportMinZ, SupportMaxZ) + : VoxelPassageGeometry::ProjectWalkableTunnelFloor( + ControlPoints, ControlRadii, Pos, FloorZ, SupportRadius); + // The first cull above already used WorldBoundCenter/WorldBoundRadiusSq for a + // world-chain tunnel. Repeating the same squared-distance test here only doubled + // arithmetic on every accepted tunnel; keep the projection and Z checks, which are + // the distinct support-floor predicates. + if (bProjectedFloor && WorldZ <= FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels - && WorldZ >= FloorZ - - VoxelPassageGeometry::LandingFloorThicknessVoxels) + && (SupportColumn == nullptr + ? WorldZ >= FloorZ + - VoxelPassageGeometry::LandingFloorThicknessVoxels + : WorldZ >= SupportMinZ && WorldZ <= SupportMaxZ)) { Result.bSupportFloor = true; } @@ -3911,7 +4799,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( // The finite floor slab owns the bottom of this tunnel. Another intersecting // tunnel may still win below its own floor; the loop continues so that air // ownership remains the union of all valid tunnel cores. - continue; + return; } } if (ControlPoints.Num() >= 2 @@ -3952,6 +4840,25 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( Pos, Tunnel.EndpointA, Tunnel.EndpointB, Tunnel.RadiusA, Tunnel.RadiusB)); } + }; + + if (bIndexedTunnels) + { + int32 LastTunnelIdx = INDEX_NONE; + for (int32 Offset = 0; Offset < TunnelCount; ++Offset) + { + const int32 TunnelIdx = Cache.TunnelWorldIndex.CandidateIndices[TunnelFirst + Offset]; + if (TunnelIdx == LastTunnelIdx) continue; + LastTunnelIdx = TunnelIdx; + EvaluateTunnel(TunnelIdx); + } + } + else + { + for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx) + { + EvaluateTunnel(TunnelIdx); + } } return Result; @@ -3970,18 +4877,33 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( { const FVector Pos(WorldX, WorldY, WorldZ); + int32 TunnelFirst = 0; + int32 TunnelCount = 0; + const bool bIndexedTunnels = Cache.TunnelWorldIndex.GetBin( + WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount); if (VoxelDensityProfile::IsEnabled()) { VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TunnelCoreCandidates, - static_cast(Cache.Tunnels.Num())); + static_cast(bIndexedTunnels ? TunnelCount : Cache.Tunnels.Num())); } - for (const FCachedTunnel& Tunnel : Cache.Tunnels) + + auto TestTunnel = [&](int32 TunnelIdx) -> bool { + const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; if (Tunnel.WorldControlPoints.Num() < 2 || Tunnel.WorldControlRadii.Num() != Tunnel.WorldControlPoints.Num()) { - continue; + return false; + } + + if (Tunnel.WorldInfluenceRadius > 0.0f + && VF_DistanceSquaredToAabb( + WorldX, WorldY, WorldZ, + Tunnel.WorldCenterlineMin, Tunnel.WorldCenterlineMax) + > FMath::Square(Tunnel.WorldInfluenceRadius)) + { + return false; } float FloorZ = 0.0f; @@ -3995,18 +4917,188 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( || WorldZ > FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels || WorldZ < FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels) { - continue; + return false; } if (Tunnel.WorldBoundRadiusSq <= 0.0f - || FVector::DistSquared(Pos, Tunnel.WorldBoundCenter) + || VF_FloatDistSquared(WorldX, WorldY, WorldZ, Tunnel.WorldBoundCenter) <= Tunnel.WorldBoundRadiusSq) { - if (VoxelDensityProfile::IsEnabled()) + return true; + } + return false; + }; + + bool bSupport = false; + if (bIndexedTunnels) + { + int32 LastTunnelIdx = INDEX_NONE; + for (int32 Offset = 0; Offset < TunnelCount; ++Offset) + { + const int32 TunnelIdx = Cache.TunnelWorldIndex.CandidateIndices[TunnelFirst + Offset]; + if (TunnelIdx == LastTunnelIdx) continue; + LastTunnelIdx = TunnelIdx; + if (TestTunnel(TunnelIdx)) { - VoxelDensityProfile::AddCounter( - VoxelDensityProfile::ECounter::TunnelCoreEvaluated); + bSupport = true; + if (VoxelDensityProfile::IsEnabled()) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TunnelCoreEvaluated); + } + break; } + } + } + else + { + for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx) + { + if (TestTunnel(TunnelIdx)) + { + bSupport = true; + if (VoxelDensityProfile::IsEnabled()) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TunnelCoreEvaluated); + } + break; + } + } + } + return bSupport; +} + +void VoxelCaveMorphology::BuildTunnelSupportFloorColumn( + float WorldX, float WorldY, + const FChunkSDFCache& Cache, + FTunnelSupportFloorColumn& OutColumn) +{ + OutColumn.Reset(); + + if (VoxelDensityProfile::IsEnabled()) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TunnelSupportColumnBuilds); + } + + TArray CandidateIds; + const bool bIndexed = Cache.TunnelWorldIndex.GetColumnCandidates(WorldX, WorldY, CandidateIds); + if (!bIndexed) + { + CandidateIds.Reserve(Cache.Tunnels.Num()); + for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx) + { + CandidateIds.Add(TunnelIdx); + } + } + + if (VoxelDensityProfile::IsEnabled()) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TunnelSupportColumnCandidates, + static_cast(CandidateIds.Num())); + } + + for (const int32 TunnelIdx : CandidateIds) + { + if (!Cache.Tunnels.IsValidIndex(TunnelIdx)) + { + continue; + } + const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; + if (Tunnel.WorldControlPoints.Num() < 2 + || Tunnel.WorldControlRadii.Num() != Tunnel.WorldControlPoints.Num()) + { + continue; + } + + const FVector ColumnPosition(WorldX, WorldY, 0.0f); + float FloorZ = 0.0f; + float SupportRadius = 0.0f; + if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor( + Tunnel.WorldControlPoints, + Tunnel.WorldControlRadii, + ColumnPosition, + FloorZ, + SupportRadius)) + { + continue; + } + + float MinZ = FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels; + float MaxZ = FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels; + + // Preserve the public point predicate's world bounding sphere exactly, while resolving + // its Z interval once. The XY projection already proves the support-radius condition. + if (Tunnel.WorldBoundRadiusSq > 0.0f) + { + const float DX = WorldX - static_cast(Tunnel.WorldBoundCenter.X); + const float DY = WorldY - static_cast(Tunnel.WorldBoundCenter.Y); + const float Remaining = Tunnel.WorldBoundRadiusSq - (DX * DX + DY * DY); + if (Remaining < 0.0f) + { + continue; + } + const float HalfZ = FMath::Sqrt(Remaining); + MinZ = FMath::Max(MinZ, static_cast(Tunnel.WorldBoundCenter.Z) - HalfZ); + MaxZ = FMath::Min(MaxZ, static_cast(Tunnel.WorldBoundCenter.Z) + HalfZ); + } + + if (MinZ <= MaxZ) + { + FTunnelSupportFloorInterval& Interval = + OutColumn.Intervals.Emplace_GetRef(); + Interval.TunnelIndex = TunnelIdx; + Interval.FloorZ = FloorZ; + Interval.MinZ = MinZ; + Interval.MaxZ = MaxZ; + } + } + + // Candidate IDs are deterministic and allow the world evaluator to find a + // tunnel's own band without re-running the XY projection. The public union + // query below simply tests every band; graph columns normally contain only a + // handful of candidates. + OutColumn.Intervals.Sort( + [](const FTunnelSupportFloorInterval& A, + const FTunnelSupportFloorInterval& B) + { + return A.TunnelIndex < B.TunnelIndex; + }); +} + +bool VoxelCaveMorphology::IsTunnelSupportFloorColumnZ( + float WorldZ, + const FTunnelSupportFloorColumn& Column) +{ + for (const FTunnelSupportFloorInterval& Interval : Column.Intervals) + { + if (WorldZ <= Interval.MaxZ) + { + if (WorldZ >= Interval.MinZ) + { + return true; + } + } + } + return false; +} + +bool VoxelCaveMorphology::GetTunnelSupportFloorColumnBand( + int32 TunnelIndex, + const FTunnelSupportFloorColumn& Column, + float& OutFloorZ, + float& OutMinZ, + float& OutMaxZ) +{ + for (const FTunnelSupportFloorInterval& Interval : Column.Intervals) + { + if (Interval.TunnelIndex == TunnelIndex) + { + OutFloorZ = Interval.FloorZ; + OutMinZ = Interval.MinZ; + OutMaxZ = Interval.MaxZ; return true; } } @@ -4027,12 +5119,19 @@ float VoxelCaveMorphology::EvaluateSDF( const FStrateGenerationParams& Params, uint32 Seed, int32 StrateIndex) { - const float RoomRadiusEnvelope = FMath::Max(Params.MinRoomRadius, Params.MaxRoomRadius); - const float TunnelRadiusEnvelope = FMath::Max(Params.TunnelMinRadius, Params.TunnelMaxRadius); + const float RoomRadiusEnvelope = FMath::Max( + FMath::Abs(Params.MinRoomRadius), FMath::Abs(Params.MaxRoomRadius)); + const float TunnelRadiusEnvelope = FMath::Max( + 0.5f, + FMath::Max(FMath::Abs(Params.TunnelMinRadius), FMath::Abs(Params.TunnelMaxRadius)) + * 1.18f); + const float FloorReliefEnvelope = FMath::Abs(Params.FloorReliefStrength) + * VOXEL_NOISE_SCALE * 1.5f; + const float BlendEnvelope = FMath::Max(Params.SDFBlendRadius, 0.0f); const float Margin = FMath::Max( - RoomRadiusEnvelope, - Params.TunnelWarpStrength + TunnelRadiusEnvelope - ) + Params.SDFBlendRadius; + RoomRadiusEnvelope + FloorReliefEnvelope, + FMath::Abs(Params.TunnelWarpStrength) + TunnelRadiusEnvelope + ) + BlendEnvelope; FChunkSDFCache TempCache; BuildChunkCache( diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index 7540ade..516763f 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -90,21 +90,20 @@ namespace static constexpr float VF_PerlinAbsBound = 1.5f; /** La même enveloppe que `FractalNoise3D` de VoxelGenerator.cpp (qui y est `static`, donc - * invisible ici). Le détour par `FVector` est délibéré — voir l'en-tête de ce fichier. */ - FORCEINLINE float HFractal3D(const FVector& Position, int32 Octaves = 4, + * invisible ici). Les coordonnées de bruit restent en float : FVector est double dans UE5 + * et son aller-retour ne servait qu'à conserver l'ancien arrondi, exigence abandonnée. */ + FORCEINLINE float HFractal3D(const FVector3f& Position, int32 Octaves = 4, float Lacunarity = 2.0f, float Persistence = 0.5f) { - return VoxelNoise::FBM((float)Position.X, (float)Position.Y, (float)Position.Z, + return VoxelNoise::FBM(Position.X, Position.Y, Position.Z, Octaves, Lacunarity, Persistence); } - /** Idem pour `RidgedNoise3D` (également `static` dans VoxelGenerator.cpp). Le bruit cellulaire, - * lui, n'a pas besoin d'enveloppe : son corps a migré dans VoxelCaveMorphology.h et s'appelle - * `VoxelNoise::Cellular3D`, avec la MÊME signature `const FVector&` que l'original. */ - FORCEINLINE float HRidged3D(const FVector& Position, int32 Octaves = 4, + /** Idem pour `RidgedNoise3D` (également `static` dans VoxelGenerator.cpp). */ + FORCEINLINE float HRidged3D(const FVector3f& Position, int32 Octaves = 4, float Lacunarity = 2.0f, float Persistence = 0.5f) { - return VoxelNoise::Ridged((float)Position.X, (float)Position.Y, (float)Position.Z, + return VoxelNoise::Ridged(Position.X, Position.Y, Position.Z, Octaves, Lacunarity, Persistence); } @@ -285,12 +284,12 @@ namespace void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { const float Frequency = 1.0f / CellSize; - const FVector NoisePos( + const FVector3f NoisePos( WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 3.17f), WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 7.31f), WorldZ * Frequency + VoxelHash::SeedOffset(SeedU, 11.47f)); const float Ribbon = FMath::Clamp( - FMath::Abs(VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z, + FMath::Abs(VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z, VoxelGenLOD::Eff(3), 2.0f, 0.5f)) * VOXEL_NOISE_SCALE, 0.0f, 2.0f); @@ -343,12 +342,12 @@ namespace void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { if (!(Strength > 0.0f)) { return; } - const FVector NoisePos( + const FVector3f NoisePos( WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 13.2f), WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 17.8f), WorldZ * Frequency + VoxelHash::SeedOffset(SeedU, 23.4f)); const float Noise01 = FMath::Clamp( - VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z, + VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z, VoxelGenLOD::Eff(Octaves), 2.0f, 0.5f) * 0.5f + 0.5f, 0.0f, 1.0f); const float Delta = Strength * Noise01; @@ -693,17 +692,16 @@ namespace } private: - // Les deux surfaces, transcrites au caractère près depuis GetSlabDensity — y compris le - // détour par FVector, qui est le même piège d'arrondi que dans FSdfRoughnessMod - // (float → double → float sous /fp:fast). Ne pas « simplifier ». + // The surface coordinates stay in float; the old FVector round-trip only preserved + // legacy rounding and is no longer part of the output contract. float SurfaceFloor(float WorldX, float WorldY) const { if (FloorRoughness <= 0.0f) { return FloorZ; } const float FF = FloorFrequency; - const FVector NoisePos(WorldX * FF + VoxelHash::SeedOffset(SeedU, 7.3f), + const FVector3f NoisePos(WorldX * FF + VoxelHash::SeedOffset(SeedU, 7.3f), WorldY * FF + VoxelHash::SeedOffset(SeedU, 11.1f), 0.0f); - const float N = VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z, + const float N = VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z, VoxelGenLOD::Eff(3), 2.0f, 0.5f) * VOXEL_NOISE_SCALE * FloorRoughness; return FloorZ + N; @@ -715,10 +713,10 @@ namespace if (CeilRoughness > 0.0f) { const float CF = CeilFrequency; - const FVector NoisePos(WorldX * CF + VoxelHash::SeedOffset(SeedU, 17.3f) + 1000.0f, + const FVector3f NoisePos(WorldX * CF + VoxelHash::SeedOffset(SeedU, 17.3f) + 1000.0f, WorldY * CF + VoxelHash::SeedOffset(SeedU, 19.7f) + 2000.0f, 3000.0f); - const float Raw = VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z, + const float Raw = VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z, VoxelGenLOD::Eff(3), 2.0f, 0.5f) * VOXEL_NOISE_SCALE; // abs() ⇒ les formations ne pendent QUE vers le bas. @@ -1199,7 +1197,7 @@ namespace const float f = P.OverhangFrequency; // Bruit de forme d'étagère [0,1] ; le terme en Z fait onduler la portée avec la hauteur // (déchiqueté, pas une lèvre lisse). - const float Ns = HFractal3D(FVector( + const float Ns = HFractal3D(FVector3f( WorldX * f + VoxelHash::SeedOffset(SeedU, 17.3f), WorldY * f + VoxelHash::SeedOffset(SeedU, 23.9f), WorldZ * f * P.OverhangZScale + VoxelHash::SeedOffset(SeedU, 5.1f)), 3) * 0.5f + 0.5f; // [0,1] @@ -1259,24 +1257,10 @@ namespace { if (Strength <= 0.0f || InOut.Sdf >= ApplyWithin) { return; } - // ⚠️ LE DÉTOUR PAR FVector EST DÉLIBÉRÉ — ne pas « simplifier ». - // L'original écrit `FractalNoise3D(FVector(WorldX * 0.12f, ...), Eff(3))`, et - // FractalNoise3D fait `VoxelNoise::FBM((float)Position.X, ...)`. FVector étant en - // DOUBLE (UE5), le produit flottant y transite par un double avant d'être re-arrondi - // en float. Passer directement des floats saute cet aller-retour, et sous /fp:fast - // les deux chemins ne s'arrondissent pas au même endroit : ~1 ULP d'écart sur le SDF, - // qui ressort en 1 ULP sur la densité finale. Reproduire le détour, c'est reproduire - // l'arrondi. VERIFIE par MazeEquivalence : le détour conserve l'identité binaire avec - // GetMazeDensity (0 écart sur 20,000 échantillons). Si cela change, le candidat suivant - // est une contraction FMA entre unités de compilation. - // - // THE FVector ROUND-TRIP IS DELIBERATE — do not "simplify" it. MazeEquivalence - // verifies the resulting path against GetMazeDensity (0 differences / 20,000 samples). - // The original goes float -> double (FVector is double in UE5) -> float; going straight - // through floats skips a rounding step, and under /fp:fast the two paths round in - // different places. Reproducing the detour reproduces the rounding. - const FVector NoisePos(WorldX * Frequency, WorldY * Frequency, WorldZ * Frequency); - InOut.Sdf += VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z, + // Keep hot noise coordinates in float. The old FVector round-trip only preserved + // legacy rounding; output compatibility with that field is intentionally abandoned. + const FVector3f NoisePos(WorldX * Frequency, WorldY * Frequency, WorldZ * Frequency); + InOut.Sdf += VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z, VoxelGenLOD::Eff(BaseOctaves), 2.0f, 0.5f) * VOXEL_NOISE_SCALE * Strength; } @@ -1300,9 +1284,8 @@ namespace } if (Strength <= 0.0f) { return; } - // The noise input is float after the deliberate FVector round-trip in Eval. If an - // authored frequency/box product overflows, Eval can publish a non-finite SDF and no - // finite interval is a proof. Unknown costs the skip and protects the geometry. + // If an authored frequency/box product overflows, Eval can publish a non-finite SDF + // and no finite interval is a proof. Unknown costs the skip and protects the geometry. const float MaxAbsCoord = FMath::Max3( FMath::Max(FMath::Abs((float)VoxelBox.Min.X), FMath::Abs((float)VoxelBox.Max.X)), FMath::Max(FMath::Abs((float)VoxelBox.Min.Y), FMath::Abs((float)VoxelBox.Max.Y)), @@ -2546,10 +2529,10 @@ namespace // partagé par toutes les îles proches — chacune échantillonne une autre partie du champ, // d'où des silhouettes distinctes. const float WarpAmp = (P.IslandMinRadius + P.IslandMaxRadius) * 0.5f * 0.35f; - const float WX = WorldX + HFractal3D(FVector(WorldX * 0.04f + VoxelHash::SeedOffset(Salt, 0.0007f), + const float WX = WorldX + HFractal3D(FVector3f(WorldX * 0.04f + VoxelHash::SeedOffset(Salt, 0.0007f), WorldY * 0.04f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE * WarpAmp; - const float WY = WorldY + HFractal3D(FVector(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f, + const float WY = WorldY + HFractal3D(FVector3f(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE * WarpAmp; @@ -2931,8 +2914,112 @@ namespace /** The warped SDF-space position belonging to the current voxel. */ FVector LastWarpedPosition = FVector::ZeroVector; - /** The unwarped position belonging to the current voxel. */ - FVector LastWorldPosition = FVector::ZeroVector; + /** The unwarped position belonging to the current voxel. */ + FVector LastWorldPosition = FVector::ZeroVector; + + // Support-floor projection depends only on XY for one immutable SDF cache. The + // mesher traverses Z outside X/Y, so keep a small worker-local LRU of XY boxes rather + // than only the immediately previous column; each integer column is then projected + // once and both structural posts reuse its interval for every Z sample. + struct FSupportColumnBox + { + static constexpr int32 Halo = CHUNK_SIZE + 8; + static constexpr int32 Dim = 2 * Halo + 1; + + const FChunkSDFCache* Cache = nullptr; + int32 BaseX = 0; + int32 BaseY = 0; + uint32 LastUse = 0; + bool bValid = false; + TArray SlotToEntry; + TArray Columns; + + void Clear() + { + Cache = nullptr; + bValid = false; + LastUse = 0; + SlotToEntry.Reset(); + Columns.Reset(); + } + + FTunnelSupportFloorColumn* AcquireColumn( + int32 X, int32 Y, const FChunkSDFCache* InCache, + uint32 InUse, bool& bOutNew) + { + bOutNew = false; + if (!bValid || Cache != InCache + || X < BaseX || X >= BaseX + Dim + || Y < BaseY || Y >= BaseY + Dim) + { + Cache = InCache; + BaseX = X - Halo; + BaseY = Y - Halo; + LastUse = InUse; + bValid = true; + SlotToEntry.Init(INDEX_NONE, Dim * Dim); + Columns.Reset(); + } + else + { + LastUse = InUse; + } + + const int32 Slot = (Y - BaseY) * Dim + (X - BaseX); + int32& EntryIndex = SlotToEntry[Slot]; + if (EntryIndex == INDEX_NONE) + { + EntryIndex = Columns.AddDefaulted(); + bOutNew = true; + } + return &Columns[EntryIndex]; + } + }; + + struct FSupportColumnCache + { + static constexpr int32 NumBoxes = 4; + FSupportColumnBox Boxes[NumBoxes]; + uint32 Clock = 0; + + void Invalidate() + { + for (FSupportColumnBox& Box : Boxes) + { + Box.Clear(); + } + Clock = 0; + } + + FSupportColumnBox& AcquireBox( + int32 X, int32 Y, const FChunkSDFCache* Cache) + { + ++Clock; + for (FSupportColumnBox& Box : Boxes) + { + if (Box.bValid && Box.Cache == Cache + && X >= Box.BaseX && X < Box.BaseX + FSupportColumnBox::Dim + && Y >= Box.BaseY && Y < Box.BaseY + FSupportColumnBox::Dim) + { + Box.LastUse = Clock; + return Box; + } + } + + FSupportColumnBox* Victim = &Boxes[0]; + for (FSupportColumnBox& Box : Boxes) + { + if (!Box.bValid || Box.LastUse < Victim->LastUse) + { + Victim = &Box; + } + } + Victim->Clear(); + return *Victim; + } + } SupportColumns; + + FTunnelSupportFloorColumn FractionalSupportColumn; /** ÉTAPE C1 — les params de la strate avec l'op de CETTE salle appliqué par-dessus. * Mémo par voxel : invalidé au début de chaque `Eval`, calculé au PREMIER modificateur @@ -3081,6 +3168,7 @@ namespace { S.Cache = FChunkSDFCache(); S.ActiveCache = &S.Cache; + S.SupportColumns.Invalidate(); InOut.Sdf = FLT_MAX; return; } @@ -3088,6 +3176,7 @@ namespace if (!(P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f)) { S.ActiveCache = &S.Cache; + S.SupportColumns.Invalidate(); return; // Sdf reste FLT_MAX } @@ -3106,15 +3195,15 @@ namespace { const float WF = P.CaveWarpFrequency; const float WS = P.CaveWarpStrength; - WarpedX += VoxelNoise::Perlin3D(FVector( + WarpedX += VoxelNoise::Perlin3D(FVector3f( WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.37f), WorldY * WF + 1.3f, EffectiveZ * WF + 5.7f)) * VOXEL_NOISE_SCALE * WS; - WarpedY += VoxelNoise::Perlin3D(FVector( + WarpedY += VoxelNoise::Perlin3D(FVector3f( WorldX * WF + 7.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.59f), EffectiveZ * WF + 2.3f)) * VOXEL_NOISE_SCALE * WS; - WarpedZ += VoxelNoise::Perlin3D(FVector( + WarpedZ += VoxelNoise::Perlin3D(FVector3f( WorldX * WF + 11.3f, WorldY * WF + 9.7f, EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f))) * VOXEL_NOISE_SCALE * WS; @@ -3219,7 +3308,8 @@ namespace SharedCache->RegionMinX = RegionMinX; SharedCache->RegionMinY = RegionMinY; SharedCache->RegionSize = RegionSize; - const float Expansion = P.CaveWarpStrength + 2.0f; + const float Expansion = FMath::Abs(P.CaveWarpStrength) + * VOXEL_NOISE_SCALE * VF_PerlinAbsBound + 2.0f; VoxelCaveMorphology::BuildChunkCache( SharedCache->Cache, (float)RegionMinX - Expansion, @@ -3228,6 +3318,8 @@ namespace (float)(RegionMinY + RegionSize) + Expansion, P, SeedU, StrateIdx, TerrainOps); SharedCache->bValid = true; + VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint(); + S.SupportColumns.Invalidate(); } S.ActiveCache = &SharedCache->Cache; @@ -3749,9 +3841,48 @@ namespace bool IsTunnelSupportFloor() const { - const FVector& Position = State().LastWorldPosition; - return VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( - Position.X, Position.Y, Position.Z, GetCache()); + FState& S = State(); + const FVector& Position = S.LastWorldPosition; + const FChunkSDFCache& Cache = GetCache(); + if (VoxelDensityProfile::IsEnabled()) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TunnelSupportFloorQueries); + } + + // Density-grid samples are integer XY. Keep fractional callers exact by using a + // scratch column; they are uncommon gradient/debug queries and must not alias a + // neighbouring integer column. + const bool bIntegerXY = + Position.X == FMath::FloorToFloat(static_cast(Position.X)) + && Position.Y == FMath::FloorToFloat(static_cast(Position.Y)); + if (bIntegerXY) + { + const int32 IX = FMath::FloorToInt(static_cast(Position.X)); + const int32 IY = FMath::FloorToInt(static_cast(Position.Y)); + FState::FSupportColumnBox& Box = S.SupportColumns.AcquireBox(IX, IY, &Cache); + bool bNewColumn = false; + FTunnelSupportFloorColumn* Column = Box.AcquireColumn( + IX, IY, &Cache, S.SupportColumns.Clock, bNewColumn); + if (bNewColumn) + { + VoxelCaveMorphology::BuildTunnelSupportFloorColumn( + static_cast(Position.X), + static_cast(Position.Y), + Cache, + *Column); + } + return VoxelCaveMorphology::IsTunnelSupportFloorColumnZ( + static_cast(Position.Z), *Column); + } + + VoxelCaveMorphology::BuildTunnelSupportFloorColumn( + static_cast(Position.X), + static_cast(Position.Y), + Cache, + S.FractionalSupportColumn); + return VoxelCaveMorphology::IsTunnelSupportFloorColumnZ( + static_cast(Position.Z), S.FractionalSupportColumn); } private: @@ -3947,15 +4078,14 @@ namespace const float RF = P.RoughnessFrequency; - // ⚠️ LE DÉTOUR PAR FVector EST DÉLIBÉRÉ (même raison que dans FSdfRoughnessMod) : - // FVector est en DOUBLE, donc chaque produit transite par un double avant d'être - // re-arrondi en float à l'appel du bruit. Sauter l'aller-retour change l'arrondi. - FVector MainPos( + // Keep both hot noise-coordinate chains in float. The old FVector round-trip only + // preserved legacy rounding; output compatibility with that field is abandoned. + FVector3f MainPos( WorldX * RF + VoxelHash::SeedOffset(SeedU, 11.3f), WorldY * RF + VoxelHash::SeedOffset(SeedU, 13.7f), EffectiveZ * RF + VoxelHash::SeedOffset(SeedU, 17.1f) ); - FVector FinePos( + FVector3f FinePos( WorldX * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 19.1f) + 2000.0f, WorldY * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 23.7f) + 2500.0f, EffectiveZ * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 29.3f) + 3000.0f @@ -3969,25 +4099,25 @@ namespace const float WF = P.DomainWarpFrequency; const float WS = P.DomainWarpStrength; - const float WarpX = VoxelNoise::Perlin3D(FVector( + const float WarpX = VoxelNoise::Perlin3D(FVector3f( WorldX * WF + VoxelHash::SeedOffset(SeedU, 5.2f), WorldY * WF + VoxelHash::SeedOffset(SeedU, 1.3f), EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 9.7f) )) * VOXEL_NOISE_SCALE * WS; - const float WarpY = VoxelNoise::Perlin3D(FVector( + const float WarpY = VoxelNoise::Perlin3D(FVector3f( WorldX * WF + 100.0f + VoxelHash::SeedOffset(SeedU, 7.7f), WorldY * WF + 200.0f + VoxelHash::SeedOffset(SeedU, 3.1f), EffectiveZ * WF + 300.0f )) * VOXEL_NOISE_SCALE * WS; - const float WarpZ = VoxelNoise::Perlin3D(FVector( + const float WarpZ = VoxelNoise::Perlin3D(FVector3f( WorldX * WF + 400.0f, WorldY * WF + 500.0f + VoxelHash::SeedOffset(SeedU, 11.9f), EffectiveZ * WF + 600.0f + VoxelHash::SeedOffset(SeedU, 13.3f) )) * VOXEL_NOISE_SCALE * WS; - const FVector WarpOffset(WarpX, WarpY, WarpZ); + const FVector3f WarpOffset(WarpX, WarpY, WarpZ); MainPos += WarpOffset; FinePos += WarpOffset; } @@ -4161,7 +4291,7 @@ namespace float NoisedZ = WorldZ; if (LP.TerraceNoiseDisplacement > 0.0f) { - const float DispNoise = HFractal3D(FVector( + const float DispNoise = HFractal3D(FVector3f( WorldX * 0.04f + VoxelHash::SeedOffset(SeedU, 31.1f), WorldY * 0.04f + VoxelHash::SeedOffset(SeedU, 37.3f), WorldZ * 0.02f + VoxelHash::SeedOffset(SeedU, 41.7f) @@ -4432,7 +4562,7 @@ namespace ? (WorldZ / P.VerticalScale) : WorldZ; // Fréquence en Z à 0.15× celle de XY ⇒ les motifs s'étirent horizontalement. - const float OverhangNoise = HFractal3D(FVector( + const float OverhangNoise = HFractal3D(FVector3f( WorldX * LP.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 53.1f), WorldY * LP.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 59.3f), EffectiveZ * LP.OverhangFrequency * 0.15f + VoxelHash::SeedOffset(SeedU, 61.7f) @@ -4533,7 +4663,7 @@ namespace const float EffectiveZ = (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f) ? (WorldZ / P.VerticalScale) : WorldZ; - const float VertGrad = VoxelNoise::Perlin3D(FVector( + const float VertGrad = VoxelNoise::Perlin3D(FVector3f( WorldX * 0.05f + VoxelHash::SeedOffset(SeedU, 71.3f), WorldY * 0.05f + VoxelHash::SeedOffset(SeedU, 73.7f), EffectiveZ * 0.15f + VoxelHash::SeedOffset(SeedU, 79.1f) // 3× plus vite en Z @@ -4610,7 +4740,7 @@ namespace ? (WorldZ / P.VerticalScale) : WorldZ; const float SF = LP.ScallopFrequency; - const float ScallopNoise = VoxelNoise::Cellular3D(FVector( + const float ScallopNoise = VoxelNoise::Cellular3D(FVector3f( WorldX * SF + VoxelHash::SeedOffset(SeedU, 83.1f), WorldY * SF + VoxelHash::SeedOffset(SeedU, 89.3f), EffectiveZ * SF + VoxelHash::SeedOffset(SeedU, 97.7f) @@ -4710,45 +4840,21 @@ namespace if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; } const FCachedRoom& Room = Cache.Rooms[NearestRoomIdx]; - const int32 MaxArches = 3; const FVector VoxPos(WorldX, WorldY, WorldZ); - for (int32 i = 0; i < MaxArches; i++) + for (int32 i = 0; i < 3; i++) { - const uint32 AH = VoxelHash::Mix(Room.Hash ^ (0xA4C400u + (uint32)i * 7369u)); - - if (VoxelHash::ToFloat01(AH) > LP.ArchDensity) { continue; } - - const uint32 AH2 = VoxelHash::Mix(AH ^ 0xA4C4u); - const float ArcCX = Room.Center.X + VoxelHash::ToFloatSigned(AH2) * Room.RadiusXY * 0.3f; - const float ArcCY = Room.Center.Y - + VoxelHash::ToFloatSigned(VoxelHash::Mix(AH2)) * Room.RadiusXY * 0.3f; - - const uint32 AH3 = VoxelHash::Mix(AH2 ^ 0xB41Du); - const float ArcCZ = Room.Center.Z + VoxelHash::ToFloatSigned(AH3) * Room.RadiusZ * 0.4f; - - const uint32 AH4 = VoxelHash::Mix(AH3 ^ 0xCAFEu); - const float Angle = VoxelHash::ToFloat01(AH4) * PI; - const float HalfSpan = Room.RadiusXY - * (0.5f + VoxelHash::ToFloat01(VoxelHash::Mix(AH4)) * 0.35f); - - const float CosA = FMath::Cos(Angle); - const float SinA = FMath::Sin(Angle); - const FVector ArchA(ArcCX - CosA * HalfSpan, ArcCY - SinA * HalfSpan, ArcCZ); - const FVector ArchB(ArcCX + CosA * HalfSpan, ArcCY + SinA * HalfSpan, ArcCZ); - - const uint32 AH5 = VoxelHash::Mix(AH4 ^ 0xF00Du); - const float ArchRadius = FMath::Lerp(LP.ArchMinRadius, LP.ArchMaxRadius, - VoxelHash::ToFloat01(AH5)); - - const float ArchSDF = VoxelSDF::Capsule(VoxPos, ArchA, ArchB, ArchRadius); + const FCachedArch& Arch = Room.Arches[i]; + if (!Arch.bActive) { continue; } + const float ArchSDF = VoxelSDF::Capsule( + VoxPos, Arch.EndpointA, Arch.EndpointB, Arch.Radius); const float ArchBlend = 2.0f; if (ArchSDF < ArchBlend) { float Fill = FMath::Clamp((ArchBlend - ArchSDF) / (ArchBlend * 2.0f), 0.0f, 1.0f); Fill = SmoothStep01(Fill); - InOut.Density += Fill * LP.BaseDensity * 1.5f; + InOut.Density += Fill * Arch.BaseDensity * 1.5f; } } } @@ -4997,45 +5103,22 @@ namespace if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; } // cf. FCaveArchMod const FCachedRoom& Room = Cache.Rooms[NearestRoomIdx]; - const float Spread = 0.85f; - const int32 MaxPinches = 3; - - for (int32 i = 0; i < MaxPinches; i++) + for (int32 i = 0; i < 3; i++) { - const uint32 PnH = VoxelHash::Mix(Room.Hash ^ (0xF1C400u + (uint32)i * 5417u)); + const FCachedPinch& Pinch = Room.Pinches[i]; + if (!Pinch.bActive) { continue; } + const float DXPn = WorldX - Pinch.CenterX; + const float DYPn = WorldY - Pinch.CenterY; + const float DZPn = WorldZ - Pinch.CenterZ; - if (VoxelHash::ToFloat01(PnH) > LP.PinchDensity) { continue; } + if (FMath::Abs(DXPn) + FMath::Abs(DYPn) + FMath::Abs(DZPn) > Pinch.MaxExtent) { continue; } - const uint32 PnH2 = VoxelHash::Mix(PnH ^ 0xF1C4u); - const float PnX = Room.Center.X + VoxelHash::ToFloatSigned(PnH2) * Room.RadiusXY * Spread; - const float PnY = Room.Center.Y - + VoxelHash::ToFloatSigned(VoxelHash::Mix(PnH2)) * Room.RadiusXY * Spread; + const float Along = DXPn * Pinch.CosAngle + DYPn * Pinch.SinAngle; + const float Across = -DXPn * Pinch.SinAngle + DYPn * Pinch.CosAngle; - const uint32 PnH3 = VoxelHash::Mix(PnH2 ^ 0x5432u); - const float PnZ = Room.Center.Z + VoxelHash::ToFloatSigned(PnH3) * Room.RadiusZ * 0.5f; - - const uint32 PnH4 = VoxelHash::Mix(PnH3 ^ 0x9A3Bu); - const float PnAngle = VoxelHash::ToFloat01(PnH4) * PI; - const float CosPN = FMath::Cos(PnAngle); - const float SinPN = FMath::Sin(PnAngle); - - const float DXPn = WorldX - PnX; - const float DYPn = WorldY - PnY; - const float DZPn = WorldZ - PnZ; - - const float MaxExtent = FMath::Max(LP.PinchLength, LP.PinchStrength) + 5.0f; - if (FMath::Abs(DXPn) + FMath::Abs(DYPn) + FMath::Abs(DZPn) > MaxExtent) { continue; } - - const float Along = DXPn * CosPN + DYPn * SinPN; - const float Across = -DXPn * SinPN + DYPn * CosPN; - - const float HalfLength = LP.PinchLength * 0.5f; - const float HalfNarrow = LP.PinchStrength; - const float HalfVertical = LP.PinchStrength * 1.5f; - - const float NAlong = Along / HalfLength; - const float NAcross = Across / HalfNarrow; - const float NUp = DZPn / HalfVertical; + const float NAlong = Along / Pinch.HalfLength; + const float NAcross = Across / Pinch.HalfNarrow; + const float NUp = DZPn / Pinch.HalfVertical; const float EllipDist = NAlong * NAlong + NAcross * NAcross + NUp * NUp; if (EllipDist < 1.0f) @@ -5044,7 +5127,7 @@ namespace Fill = SmoothStep01(Fill); const float AxisDist = FMath::Sqrt(NAcross * NAcross + NUp * NUp); const float SideFactor = FMath::Clamp(AxisDist * 2.0f, 0.0f, 1.0f); - InOut.Density += Fill * SideFactor * LP.BaseDensity * 1.5f; + InOut.Density += Fill * SideFactor * Pinch.BaseDensity * 1.5f; } } } @@ -5192,7 +5275,7 @@ namespace const float WormZFreq = P.WormFrequency * P.WormHorizontalBias; - const float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector( + const float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f( WorldX * P.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f), WorldY * P.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f), EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f) @@ -5202,7 +5285,7 @@ namespace // second Perlin (le cas courant ; sortie bit-identique). Transcrit tel quel. if (N1 >= P.WormThreshold) { return; } - const float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector( + const float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f( WorldX * P.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f) + 137.0f, WorldY * P.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f) + 259.0f, EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f) + 431.0f @@ -5440,10 +5523,61 @@ VoxelDensityOps::FRoomBoxDiagnostic VoxelDensityOps::GetLastRoomBoxDiagnostic() return D; } +void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint() +{ + if (!VoxelDensityProfile::IsEnabled()) + { + return; + } + + uint64 DynamicBytes = 0; + uint64 EntryBytes = 0; + uint64 LargestEntryBytes = 0; + uint64 ValidEntries = 0; + for (int32 Index = 0; Index < RoomGraphCacheSlotCount; ++Index) + { + const FRoomGraphCacheEntry& Entry = GRoomGraphCache[Index]; + if (!Entry.bValid) + { + continue; + } + + ++ValidEntries; + const uint64 EntryDynamicBytes = static_cast(Entry.Cache.GetAllocatedSize()); + const uint64 FullEntryBytes = static_cast(sizeof(FRoomGraphCacheEntry)) + + EntryDynamicBytes; + DynamicBytes += EntryDynamicBytes; + EntryBytes += FullEntryBytes; + LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes); + } + + VoxelDensityProfile::SetWorkerRoomGraphCacheFootprint( + RoomGraphCacheSlotCount, + ValidEntries, + static_cast(sizeof(GRoomGraphCache)), + DynamicBytes, + EntryBytes, + LargestEntryBytes, + ValidEntries); +} + //============================================================================= // FVoxelOpStack //============================================================================= +SIZE_T FVoxelOpStack::GetAllocatedSize() const +{ + SIZE_T Bytes = static_cast(Ops.GetAllocatedSize()); + for (const FOpEntry& Entry : Ops) + { + if (Entry.Op.IsValid()) + { + Bytes += FMemory::GetAllocSize(const_cast(Entry.Op.Get())); + } + } + return Bytes; +} + bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const { if (OutError != nullptr) { OutError->Reset(); } diff --git a/Source/VoxelForge/Private/VoxelDensityProfile.cpp b/Source/VoxelForge/Private/VoxelDensityProfile.cpp index 727df71..ea78184 100644 --- a/Source/VoxelForge/Private/VoxelDensityProfile.cpp +++ b/Source/VoxelForge/Private/VoxelDensityProfile.cpp @@ -5,9 +5,57 @@ namespace VoxelDensityProfile namespace { std::atomic GEnabled(false); - std::atomic GCalls[BucketCount]; - std::atomic GCycles[BucketCount]; - std::atomic GCounters[CounterCount]; + + // The old implementation performed a contended atomic fetch_add for every timer and + // counter. A TunnelNetwork sample can close twenty-plus ranges and add several + // candidate counters, so profiling changed the workload it was supposed to measure. + // Keep one accumulator per worker and only aggregate at Snapshot(). Commandlet resets + // happen between completed jobs, so the deliberately light-weight list is sufficient. + struct FThreadState + { + uint64 Calls[BucketCount]{}; + uint64 Cycles[BucketCount]{}; + uint64 Counters[CounterCount]{}; + + uint64 TunnelCacheCapacityEntries = 0; + uint64 TunnelCacheValidEntries = 0; + uint64 TunnelCacheStaticBytes = 0; + uint64 TunnelCacheDynamicBytes = 0; + uint64 TunnelCacheEntryBytes = 0; + uint64 TunnelCacheLargestEntryBytes = 0; + uint64 TunnelCacheLargestWorkerValidEntries = 0; + + uint64 RoomGraphCacheCapacityEntries = 0; + uint64 RoomGraphCacheValidEntries = 0; + uint64 RoomGraphCacheStaticBytes = 0; + uint64 RoomGraphCacheDynamicBytes = 0; + uint64 RoomGraphCacheEntryBytes = 0; + uint64 RoomGraphCacheLargestEntryBytes = 0; + uint64 RoomGraphCacheLargestWorkerValidEntries = 0; + + std::atomic Next{nullptr}; + }; + + std::atomic GThreadStates{nullptr}; + thread_local FThreadState* GThreadState = nullptr; + + FThreadState& GetThreadState() + { + if (GThreadState == nullptr) + { + GThreadState = new FThreadState(); + FThreadState* Head = GThreadStates.load(std::memory_order_relaxed); + do + { + GThreadState->Next.store(Head, std::memory_order_relaxed); + } + while (!GThreadStates.compare_exchange_weak( + Head, GThreadState, + std::memory_order_release, + std::memory_order_relaxed)); + } + return *GThreadState; + } int32 ToIndex(EBucket Bucket) { @@ -30,28 +78,78 @@ namespace VoxelDensityProfile void Reset() { - for (int32 Index = 0; Index < BucketCount; ++Index) + for (FThreadState* State = GThreadStates.load(std::memory_order_acquire); + State != nullptr; + State = State->Next.load(std::memory_order_acquire)) { - GCalls[Index].store(0, std::memory_order_relaxed); - GCycles[Index].store(0, std::memory_order_relaxed); - } - for (int32 Index = 0; Index < CounterCount; ++Index) - { - GCounters[Index].store(0, std::memory_order_relaxed); + for (int32 Index = 0; Index < BucketCount; ++Index) + { + State->Calls[Index] = 0; + State->Cycles[Index] = 0; + } + for (int32 Index = 0; Index < CounterCount; ++Index) + { + State->Counters[Index] = 0; + } } } FSnapshot Snapshot() { FSnapshot Result; - for (int32 Index = 0; Index < BucketCount; ++Index) + for (FThreadState* State = GThreadStates.load(std::memory_order_acquire); + State != nullptr; + State = State->Next.load(std::memory_order_acquire)) { - Result.Calls[Index] = GCalls[Index].load(std::memory_order_relaxed); - Result.Cycles[Index] = GCycles[Index].load(std::memory_order_relaxed); - } - for (int32 Index = 0; Index < CounterCount; ++Index) - { - Result.Counters[Index] = GCounters[Index].load(std::memory_order_relaxed); + for (int32 Index = 0; Index < BucketCount; ++Index) + { + Result.Calls[Index] += State->Calls[Index]; + Result.Cycles[Index] += State->Cycles[Index]; + } + for (int32 Index = 0; Index < CounterCount; ++Index) + { + Result.Counters[Index] += State->Counters[Index]; + } + + const uint64 TunnelWorkerBytes = State->TunnelCacheStaticBytes + + State->TunnelCacheDynamicBytes; + if (State->TunnelCacheCapacityEntries > 0) + { + ++Result.TunnelCacheWorkers; + Result.TunnelCacheCapacityEntries += State->TunnelCacheCapacityEntries; + Result.TunnelCacheValidEntries += State->TunnelCacheValidEntries; + Result.TunnelCacheStaticBytes += State->TunnelCacheStaticBytes; + Result.TunnelCacheDynamicBytes += State->TunnelCacheDynamicBytes; + Result.TunnelCacheEntryBytes += State->TunnelCacheEntryBytes; + Result.TunnelCacheLargestEntryBytes = FMath::Max( + Result.TunnelCacheLargestEntryBytes, + State->TunnelCacheLargestEntryBytes); + Result.TunnelCacheLargestWorkerValidEntries = FMath::Max( + Result.TunnelCacheLargestWorkerValidEntries, + State->TunnelCacheValidEntries); + Result.TunnelCacheLargestWorkerBytes = FMath::Max( + Result.TunnelCacheLargestWorkerBytes, TunnelWorkerBytes); + } + + const uint64 RoomGraphWorkerBytes = State->RoomGraphCacheStaticBytes + + State->RoomGraphCacheDynamicBytes; + if (State->RoomGraphCacheCapacityEntries > 0) + { + ++Result.RoomGraphCacheWorkers; + Result.RoomGraphCacheCapacityEntries += State->RoomGraphCacheCapacityEntries; + Result.RoomGraphCacheValidEntries += State->RoomGraphCacheValidEntries; + Result.RoomGraphCacheStaticBytes += State->RoomGraphCacheStaticBytes; + Result.RoomGraphCacheDynamicBytes += State->RoomGraphCacheDynamicBytes; + Result.RoomGraphCacheEntryBytes += State->RoomGraphCacheEntryBytes; + Result.RoomGraphCacheLargestEntryBytes = FMath::Max( + Result.RoomGraphCacheLargestEntryBytes, + State->RoomGraphCacheLargestEntryBytes); + Result.RoomGraphCacheLargestWorkerValidEntries = FMath::Max( + Result.RoomGraphCacheLargestWorkerValidEntries, + State->RoomGraphCacheValidEntries); + Result.RoomGraphCacheLargestWorkerBytes = FMath::Max( + Result.RoomGraphCacheLargestWorkerBytes, RoomGraphWorkerBytes); + } } return Result; } @@ -61,10 +159,52 @@ namespace VoxelDensityProfile const int32 Index = static_cast(Counter); if (Index >= 0 && Index < CounterCount) { - GCounters[Index].fetch_add(Amount, std::memory_order_relaxed); + GetThreadState().Counters[Index] += Amount; } } + void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls) + { + const int32 Index = ToIndex(Bucket); + FThreadState& State = GetThreadState(); + State.Calls[Index] += Calls; + State.Cycles[Index] += Cycles; + } + + void SetWorkerTunnelCacheFootprint( + uint64 CapacityEntries, uint64 ValidEntries, + uint64 StaticBytes, uint64 DynamicBytes, + uint64 EntryBytes, uint64 LargestEntryBytes, + uint64 LargestWorkerValidEntries) + { + if (!IsEnabled()) { return; } + FThreadState& State = GetThreadState(); + State.TunnelCacheCapacityEntries = CapacityEntries; + State.TunnelCacheValidEntries = ValidEntries; + State.TunnelCacheStaticBytes = StaticBytes; + State.TunnelCacheDynamicBytes = DynamicBytes; + State.TunnelCacheEntryBytes = EntryBytes; + State.TunnelCacheLargestEntryBytes = LargestEntryBytes; + State.TunnelCacheLargestWorkerValidEntries = LargestWorkerValidEntries; + } + + void SetWorkerRoomGraphCacheFootprint( + uint64 CapacityEntries, uint64 ValidEntries, + uint64 StaticBytes, uint64 DynamicBytes, + uint64 EntryBytes, uint64 LargestEntryBytes, + uint64 LargestWorkerValidEntries) + { + if (!IsEnabled()) { return; } + FThreadState& State = GetThreadState(); + State.RoomGraphCacheCapacityEntries = CapacityEntries; + State.RoomGraphCacheValidEntries = ValidEntries; + State.RoomGraphCacheStaticBytes = StaticBytes; + State.RoomGraphCacheDynamicBytes = DynamicBytes; + State.RoomGraphCacheEntryBytes = EntryBytes; + State.RoomGraphCacheLargestEntryBytes = LargestEntryBytes; + State.RoomGraphCacheLargestWorkerValidEntries = LargestWorkerValidEntries; + } + const TCHAR* CounterName(ECounter Counter) { switch (Counter) @@ -79,6 +219,10 @@ namespace VoxelDensityProfile case ECounter::CaveTunnelEvaluated:return TEXT("CaveTunnelEvaluated"); case ECounter::TunnelCoreCandidates:return TEXT("TunnelCoreCandidates"); case ECounter::TunnelCoreEvaluated:return TEXT("TunnelCoreEvaluated"); + case ECounter::TunnelSupportColumnBuilds:return TEXT("TunnelSupportColumnBuilds"); + case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates"); + case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries"); + case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks"); case ECounter::PassageCandidates: return TEXT("PassageCandidates"); case ECounter::PassageEvaluated: return TEXT("PassageEvaluated"); case ECounter::Count: break; @@ -130,6 +274,20 @@ namespace VoxelDensityProfile { TEXT("CaveTunnelFloorOp"), EBucket::CaveTunnelFloorOp }, { TEXT("CaveTunnelAirOp"), EBucket::CaveTunnelAirOp }, { TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp }, + { TEXT("DensityPrologue"), EBucket::DensityPrologue }, + { TEXT("DensityCore"), EBucket::DensityCore }, + { TEXT("DensityDisturbances"), EBucket::DensityDisturbances }, + { TEXT("DensityStructuralPosts"), EBucket::DensityStructuralPosts }, + { TEXT("DensityBoundarySeal"), EBucket::DensityBoundarySeal }, + { TEXT("DensityDiffLayer"), EBucket::DensityDiffLayer }, + { TEXT("DensityTail"), EBucket::DensityTail }, + { TEXT("MesherGenerateMesh"), EBucket::MesherGenerateMesh }, + { TEXT("MesherDensityGrid"), EBucket::MesherDensityGrid }, + { TEXT("MesherCellClassification"), EBucket::MesherCellClassification }, + { TEXT("MesherGradientNormals"), EBucket::MesherGradientNormals }, + { TEXT("MesherVertexInterpolation"), EBucket::MesherVertexInterpolation }, + { TEXT("MesherStreamBuilding"), EBucket::MesherStreamBuilding }, + { TEXT("MesherOther"), EBucket::MesherOther }, }; for (const FNameBucket& Entry : Names) { @@ -179,17 +337,34 @@ namespace VoxelDensityProfile case EBucket::CaveTunnelAirOp: return TEXT("CaveTunnelAirOp"); case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp"); case EBucket::OtherOp: return TEXT("OtherOp"); + case EBucket::DensityPrologue: return TEXT("DensityPrologue"); + case EBucket::DensityCore: return TEXT("DensityCore"); + case EBucket::DensityDisturbances:return TEXT("DensityDisturbances"); + case EBucket::DensityStructuralPosts:return TEXT("DensityStructuralPosts"); + case EBucket::DensityBoundarySeal:return TEXT("DensityBoundarySeal"); + case EBucket::DensityDiffLayer: return TEXT("DensityDiffLayer"); + case EBucket::DensityTail: return TEXT("DensityTail"); + case EBucket::MesherGenerateMesh:return TEXT("MesherGenerateMesh"); + case EBucket::MesherDensityGrid:return TEXT("MesherDensityGrid"); + case EBucket::MesherCellClassification:return TEXT("MesherCellClassification"); + case EBucket::MesherGradientNormals:return TEXT("MesherGradientNormals"); + case EBucket::MesherVertexInterpolation:return TEXT("MesherVertexInterpolation"); + case EBucket::MesherStreamBuilding:return TEXT("MesherStreamBuilding"); + case EBucket::MesherOther: return TEXT("MesherOther"); case EBucket::Count: break; } return TEXT("Unknown"); } FScopedTimer::~FScopedTimer() + { + End(); + } + + void FScopedTimer::End() { if (!bActive) { return; } - const int32 Index = ToIndex(Bucket); - GCalls[Index].fetch_add(1, std::memory_order_relaxed); - GCycles[Index].fetch_add(FPlatformTime::Cycles64() - StartCycles, - std::memory_order_relaxed); + AddMeasurement(Bucket, FPlatformTime::Cycles64() - StartCycles, 1); + bActive = false; } } diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 4501647..000a01b 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -181,10 +181,10 @@ thread_local int32 VoxelGenLOD::SampleStep = 1; // 4-wide via SSE (Perlin3D_x4). These thin wrappers keep every call site unchanged. They // sample a DIFFERENT (float hash-gradient) noise field than the old FMath::PerlinNoise3D, // so worlds re-tune once — but the fBm/Ridged math/contracts ([-1,1]) are identical. -static float FractalNoise3D(const FVector& Position, int32 Octaves = 4, - float Lacunarity = 2.0f, float Persistence = 0.5f) +static float FractalNoise3D(const FVector3f& Position, int32 Octaves = 4, + float Lacunarity = 2.0f, float Persistence = 0.5f) { - return VoxelNoise::FBM((float)Position.X, (float)Position.Y, (float)Position.Z, + return VoxelNoise::FBM(Position.X, Position.Y, Position.Z, Octaves, Lacunarity, Persistence); } @@ -199,10 +199,10 @@ static float FractalNoise3D(const FVector& Position, int32 Octaves = 4, // Returns approximately [-1, 1] to match FractalNoise3D's range. // Character: craggy cliffs, natural erosion patterns, sharp corridors. -static float RidgedNoise3D(const FVector& Position, int32 Octaves = 4, - float Lacunarity = 2.0f, float Persistence = 0.5f) +static float RidgedNoise3D(const FVector3f& Position, int32 Octaves = 4, + float Lacunarity = 2.0f, float Persistence = 0.5f) { - return VoxelNoise::Ridged((float)Position.X, (float)Position.Y, (float)Position.Z, + return VoxelNoise::Ridged(Position.X, Position.Y, Position.Z, Octaves, Lacunarity, Persistence); } @@ -225,9 +225,9 @@ static float RidgedNoise3D(const FVector& Position, int32 Octaves = 4, // qui voit déjà `VoxelHash` : la pile d'opérateurs a besoin exactement du même bruit pour la // rugosité (4b, type Cellular) et pour les festons (4f), et deux copies d'une fonction pure finissent // par diverger — c'est littéralement `AUDIT §C1`. Ce forwarder garde les ~3 sites d'appel ci-dessous -// inchangés, comme l'ont fait FractalNoise3D et RidgedNoise3D lors de T2.a. Aucun changement de -// comportement : corps identique, mêmes doubles de `FVector`, même ordre d'opérations. -static float CellularNoise3D(const FVector& Position) +// inchangés, comme l'ont fait FractalNoise3D et RidgedNoise3D lors de T2.a. Le chemin chaud garde +// maintenant les coordonnées en float : la compatibilité d'arrondi historique est abandonnée. +static float CellularNoise3D(const FVector3f& Position) { return VoxelNoise::Cellular3D(Position); } @@ -483,6 +483,16 @@ namespace static thread_local FTunnelCoreCacheState GTunnelCoreCache; + static void BuildTunnelSupportColumnsForChunk( + FChunkSDFCache& Cache, + const FIntVector& ChunkCoord); + + static const FTunnelSupportFloorColumn* FindTunnelSupportColumn( + const FChunkSDFCache& Cache, + int32 X, + int32 Y, + const FTunnelSupportFloorColumn& EmptyColumn); + // A mesher tile samples an expanded grid (-1..Cells+1), so one worker visits several exact // chunk coordinates even while it is generating one tile. The old CP_* cache retained only // the last coordinate and consequently rebuilt the whole TunnelNetwork stack whenever the @@ -491,12 +501,11 @@ namespace // result. Native TunnelNetwork/Underwater states are immutable after construction; the active // pointer below lets the hot sample path read the stored stack and tunnel-core cache without // moving them on every voxel. - // The largest production coarse tile can touch thousands of exact chunk keys once its - // gradient halo is included. A 128-slot direct map repeatedly evicts nearby keys (the LOD - // probe measured 1,734 misses for only 6,859 samples at L4), rebuilding the room/tunnel cache - // in the hot loop. Keep the same deterministic key and open-addressed lookup, but size the - // bounded table for the measured L4 working set rather than the old 128-slot guess. - constexpr int32 TunnelDensityCacheSlotCount = 4096; + // The cache is open-addressed and bounded. The 128-slot budget is evidence-sized: the + // 128³ production export measured at most 102 live tunnel entries on one worker, leaving + // 26 slots of headroom; the old 4,096-slot experiment reserved 3.34 MiB per worker before + // one operator or SDF allocation existed. Collisions only cause a safe deterministic rebuild. + constexpr int32 TunnelDensityCacheSlotCount = 128; struct FTunnelNetworkDensityCacheEntry { @@ -514,6 +523,49 @@ namespace FTunnelCoreCacheState TunnelCore; }; + void ReportTunnelDensityCacheFootprint( + const FTunnelNetworkDensityCacheEntry* Entries) + { + if (!VoxelDensityProfile::IsEnabled() || Entries == nullptr) + { + return; + } + + uint64 DynamicBytes = 0; + uint64 EntryBytes = 0; + uint64 LargestEntryBytes = 0; + uint64 ValidEntries = 0; + for (int32 Index = 0; Index < TunnelDensityCacheSlotCount; ++Index) + { + const FTunnelNetworkDensityCacheEntry& Entry = Entries[Index]; + if (!Entry.bValid) + { + continue; + } + + ++ValidEntries; + const uint64 StackBytes = static_cast(Entry.OpStack.GetAllocatedSize()); + const uint64 TunnelCoreBytes = static_cast( + Entry.TunnelCore.Cache.GetAllocatedSize()); + const uint64 EntryDynamicBytes = StackBytes + TunnelCoreBytes; + const uint64 FullEntryBytes = static_cast( + sizeof(FTunnelNetworkDensityCacheEntry)) + EntryDynamicBytes; + DynamicBytes += EntryDynamicBytes; + EntryBytes += FullEntryBytes; + LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes); + } + + VoxelDensityProfile::SetWorkerTunnelCacheFootprint( + TunnelDensityCacheSlotCount, + ValidEntries, + static_cast(sizeof(FTunnelNetworkDensityCacheEntry)) + * TunnelDensityCacheSlotCount, + DynamicBytes, + EntryBytes, + LargestEntryBytes, + ValidEntries); + } + FORCEINLINE int32 TunnelDensityCacheSlot(const FIntVector& Chunk) { uint32 Hash = static_cast(Chunk.X) * 0x9E3779B9u; @@ -547,7 +599,8 @@ namespace const float ChunkMinY = static_cast(ChunkCoord.Y * CHUNK_SIZE); const float ChunkMaxX = ChunkMinX + static_cast(CHUNK_SIZE); const float ChunkMaxY = ChunkMinY + static_cast(CHUNK_SIZE); - const float Expansion = FMath::Max(Params.CaveWarpStrength, 0.0f) + 2.0f; + const float Expansion = FMath::Abs(Params.CaveWarpStrength) + * VOXEL_NOISE_SCALE * 1.5f + 2.0f; const int32 StrateIndex = Manager.GetStrateIndex( (static_cast(ChunkCoord.Z) + 0.5f) * static_cast(CHUNK_SIZE) * VOXEL_SIZE); @@ -558,6 +611,7 @@ namespace ChunkMaxX + Expansion, ChunkMaxY + Expansion, Params, static_cast(Manager.GetWorldSeed()), StrateIndex, nullptr); + BuildTunnelSupportColumnsForChunk(OutState.Cache, ChunkCoord); OutState.OwnerId = OwnerId; OutState.ManagerLifetimeId = ManagerLifetimeId; OutState.Chunk = ChunkCoord; @@ -566,6 +620,67 @@ namespace OutState.bValid = true; } + static void BuildTunnelSupportColumnsForChunk( + FChunkSDFCache& Cache, + const FIntVector& ChunkCoord) + { + const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1; + const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1; + const int32 Cells = CHUNK_SIZE + 2; + Cache.SupportColumnMinX = MinX; + Cache.SupportColumnMinY = MinY; + Cache.SupportColumnCellsX = Cells; + Cache.SupportColumnCellsY = Cells; + Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells); + Cache.SupportColumns.Reset(); + + // Empty columns are represented only by INDEX_NONE in the slot map, so + // each stored column retains the inline bands for actual graph tubes. + for (int32 Y = 0; Y < Cells; ++Y) + { + for (int32 X = 0; X < Cells; ++X) + { + FTunnelSupportFloorColumn Column; + VoxelCaveMorphology::BuildTunnelSupportFloorColumn( + static_cast(MinX + X), + static_cast(MinY + Y), + Cache, + Column); + if (Column.Intervals.Num() == 0) + { + continue; + } + + const int32 EntryIndex = Cache.SupportColumns.Add(MoveTemp(Column)); + Cache.SupportColumnEntries[Y * Cells + X] = EntryIndex; + } + } + } + + static const FTunnelSupportFloorColumn* FindTunnelSupportColumn( + const FChunkSDFCache& Cache, + int32 X, + int32 Y, + const FTunnelSupportFloorColumn& EmptyColumn) + { + if (Cache.SupportColumnCellsX <= 0 + || X < Cache.SupportColumnMinX + || X >= Cache.SupportColumnMinX + Cache.SupportColumnCellsX + || Y < Cache.SupportColumnMinY + || Y >= Cache.SupportColumnMinY + Cache.SupportColumnCellsY) + { + return nullptr; + } + + const int32 Slot = + (Y - Cache.SupportColumnMinY) * Cache.SupportColumnCellsX + + (X - Cache.SupportColumnMinX); + const int32 EntryIndex = Cache.SupportColumnEntries[Slot]; + return EntryIndex == INDEX_NONE + ? &EmptyColumn + : &Cache.SupportColumns[EntryIndex]; + } + struct FVoxelStackParamRefs { const FSlabGenerationParams* Slab = nullptr; @@ -739,6 +854,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co { VoxelDensityProfile::FScopedTimer DensityProfileTimer( VoxelDensityProfile::EBucket::GetDensityAt); + VoxelDensityProfile::FScopedTimer DensityPrologueTimer( + VoxelDensityProfile::EBucket::DensityPrologue); // ── STRATE SYSTEM ── // Query per-chunk params from the manager so each strate has different caves. float Result; @@ -762,6 +879,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // to reach the next layer. No caves, no spine, no vertical seal — just rock + passage // tube/landing; // the global XY edge seal is applied below for bounded worlds. + DensityPrologueTimer.End(); + VoxelDensityProfile::FScopedTimer DensityCoreTimer( + VoxelDensityProfile::EBucket::DensityCore); float Density = 8.0f; // bedrock solidity (positive = solid) StrateManager->ApplyPassageModifier( Density, WorldX, WorldY, WorldZ, 8.0f, 0.0f); @@ -1161,6 +1281,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co CP_ActiveTunnelDensityCache = &CachedEntry; ActiveOpStack = &CachedEntry.OpStack; ActiveTunnelCoreCache = &CachedEntry.TunnelCore; + ReportTunnelDensityCacheFootprint(CP_TunnelDensityCache); } else if (!bLoadedTunnelDensityCache) { @@ -1170,6 +1291,10 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co } + DensityPrologueTimer.End(); + VoxelDensityProfile::FScopedTimer DensityCoreTimer( + VoxelDensityProfile::EBucket::DensityCore); + // Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention // MC (négatif = solide) comme les fonctions d'archétype, donc les disturbances et la couche // de diff qui suivent ne voient aucune différence. @@ -1226,21 +1351,28 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co default: // Underwater shares tunnel rock (water table is a render-side overlay). Result = GetDensityWithParams(WorldX, WorldY, WorldZ, CP_Tunnel, - CP_TunnelFP, LayoutVersion); break; + CP_TunnelFP, LayoutVersion, + /*bApplyLegacyStructuralPosts=*/false); break; } + DensityCoreTimer.End(); // Disturbance layer (the "wow" post-process) — cached params, MC convention. The // vertical shaft tree opens a walkable route before this shared pass, so a bridge/ridge // must not refill it. Both native and operator-stack paths use the same marker helper. + VoxelDensityProfile::FScopedTimer DensityDisturbancesTimer( + VoxelDensityProfile::EBucket::DensityDisturbances); const bool bProtectVerticalShaftAir = VoxelPassageGeometry::VerticalShaftConnectorAirMarker(); ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed, bProtectVerticalShaftAir); + DensityDisturbancesTimer.End(); // A disturbance is allowed to add visual detail, but it must not refill the landing's // measured air volume or turn its support slab back into a hole. Reassert the landing // air first, then the structural floor, in MC space before the final XY seal. The two // calls use the same landing geometry already evaluated by the legacy and op-stack post. + VoxelDensityProfile::FScopedTimer DensityStructuralPostsTimer( + VoxelDensityProfile::EBucket::DensityStructuralPosts); float LandingBaseDensity = CP_Dist.BaseDensity; #if WITH_EDITOR // A composer parent can deliberately use a different structural base than the authored @@ -1326,12 +1458,26 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // never once per voxel. if (ActiveTunnelCoreCache->bValid) { + const FTunnelSupportFloorColumn* SupportColumn = nullptr; + FTunnelSupportFloorColumn EmptySupportColumn; + const bool bIntegerXY = + WorldX == FMath::FloorToFloat(WorldX) + && WorldY == FMath::FloorToFloat(WorldY); + if (bIntegerXY) + { + const int32 IX = FMath::FloorToInt(WorldX); + const int32 IY = FMath::FloorToInt(WorldY); + SupportColumn = FindTunnelSupportColumn( + ActiveTunnelCoreCache->Cache, IX, IY, EmptySupportColumn); + } + FTunnelCoreWorldEvaluation TunnelCore; { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCorePosts); TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( - WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache); + WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, + SupportColumn); } const bool bTunnelSupportFloor = TunnelCore.bSupportFloor; if (bTunnelSupportFloor) @@ -1362,6 +1508,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co StrateManager->ApplyPassageLandingRoomFloorMC( Result, WorldX, WorldY, WorldZ, LandingBaseDensity); } + DensityStructuralPostsTimer.End(); } else { @@ -1371,16 +1518,23 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // aussi. La calculer une fois évite un CRC par voxel sur un chemin qui n'en a aucun besoin. // `LayoutVersion = 0` : sans `StrateManager` il n'y a pas de layout, donc rien qui puisse // périmer — et l'empreinte constante suffit à distinguer ce cache de tous les autres. + DensityPrologueTimer.End(); + VoxelDensityProfile::FScopedTimer DensityCoreTimer( + VoxelDensityProfile::EBucket::DensityCore); static const FStrateGenerationParams FallbackParams; static const uint32 FallbackFP = FCrc::MemCrc32(&FallbackParams, sizeof(FallbackParams)); Result = GetDensityWithParams(WorldX, WorldY, WorldZ, FallbackParams, FallbackFP, 0); + DensityCoreTimer.End(); } // The edge is the final generated structural invariant. Passage carving is ordered before it // in every archetype/stack, and this MC-facing pass also covers out-of-layout air plus any // disturbance that might otherwise carve back into the shell. Player edits remain separate // below and are deliberately still the user override path. + VoxelDensityProfile::FScopedTimer DensityBoundarySealTimer( + VoxelDensityProfile::EBucket::DensityBoundarySeal); VF_ApplyXYEdgeSealMC(Result, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f); + DensityBoundarySealTimer.End(); //========================================================================= // PLAYER MODIFICATIONS (diff layer) @@ -1391,6 +1545,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // we subtract the diff offset: // Carve (diff < 0) → Result -= negative → Result increases → more air ✓ // Fill (diff > 0) → Result -= positive → Result decreases → more solid ✓ + VoxelDensityProfile::FScopedTimer DensityDiffLayerTimer( + VoxelDensityProfile::EBucket::DensityDiffLayer); if (DiffLayer && DiffLayer->HasAnyMods()) { // ── PER-CHUNK MOD SNAPSHOT ── @@ -1422,14 +1578,18 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co Result -= UVoxelDiffLayer::EvaluateMods(Slot.Mods, WorldX, WorldY, WorldZ); } } + DensityDiffLayerTimer.End(); + VoxelDensityProfile::FScopedTimer DensityTailTimer( + VoxelDensityProfile::EBucket::DensityTail); VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); return Result; } float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ, const FStrateGenerationParams& Params, - uint32 ParamsFingerprint, uint32 LayoutVersion) const + uint32 ParamsFingerprint, uint32 LayoutVersion, + bool bApplyLegacyStructuralPosts) const { //========================================================================= // STRATE DENSITY FUNCTION (Morphology Pipeline) @@ -1505,15 +1665,15 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // Three independent Perlin fields offset by irrational-ish numbers // so the X/Y/Z warp channels don't correlate with each other. // Single octave to keep per-voxel cost low (3 Perlin calls total). - WarpedX += VoxelNoise::Perlin3D(FVector( + WarpedX += VoxelNoise::Perlin3D(FVector3f( WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.37f), WorldY * WF + 1.3f, EffectiveZ * WF + 5.7f)) * VOXEL_NOISE_SCALE * WS; - WarpedY += VoxelNoise::Perlin3D(FVector( + WarpedY += VoxelNoise::Perlin3D(FVector3f( WorldX * WF + 7.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.59f), EffectiveZ * WF + 2.3f)) * VOXEL_NOISE_SCALE * WS; - WarpedZ += VoxelNoise::Perlin3D(FVector( + WarpedZ += VoxelNoise::Perlin3D(FVector3f( WorldX * WF + 11.3f, WorldY * WF + 9.7f, EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f))) * VOXEL_NOISE_SCALE * WS; @@ -1619,7 +1779,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo const float ChunkMinY = CacheChunkY * (float)CHUNK_SIZE; const float ChunkMaxX = ChunkMinX + (float)CHUNK_SIZE; const float ChunkMaxY = ChunkMinY + (float)CHUNK_SIZE; - const float Expansion = Params.CaveWarpStrength + 2.0f; + const float Expansion = FMath::Abs(Params.CaveWarpStrength) + * VOXEL_NOISE_SCALE * 1.5f + 2.0f; const float SMinX = ChunkMinX - Expansion; const float SMinY = ChunkMinY - Expansion; @@ -1786,12 +1947,12 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo float RF = Params.RoughnessFrequency; // Base noise input positions (with seed offsets for uniqueness) - FVector MainPos( + FVector3f MainPos( WorldX * RF + VoxelHash::SeedOffset(SeedU, 11.3f), WorldY * RF + VoxelHash::SeedOffset(SeedU, 13.7f), EffectiveZ * RF + VoxelHash::SeedOffset(SeedU, 17.1f) ); - FVector FinePos( + FVector3f FinePos( WorldX * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 19.1f) + 2000.0f, WorldY * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 23.7f) + 2500.0f, EffectiveZ * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 29.3f) + 3000.0f @@ -1807,26 +1968,26 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo float WS = Params.DomainWarpStrength; // Sample three independent noise fields for X, Y, Z warp - float WarpX = VoxelNoise::Perlin3D(FVector( + float WarpX = VoxelNoise::Perlin3D(FVector3f( WorldX * WF + VoxelHash::SeedOffset(SeedU, 5.2f), WorldY * WF + VoxelHash::SeedOffset(SeedU, 1.3f), EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 9.7f) )) * VOXEL_NOISE_SCALE * WS; - float WarpY = VoxelNoise::Perlin3D(FVector( + float WarpY = VoxelNoise::Perlin3D(FVector3f( WorldX * WF + 100.0f + VoxelHash::SeedOffset(SeedU, 7.7f), WorldY * WF + 200.0f + VoxelHash::SeedOffset(SeedU, 3.1f), EffectiveZ * WF + 300.0f )) * VOXEL_NOISE_SCALE * WS; - float WarpZ = VoxelNoise::Perlin3D(FVector( + float WarpZ = VoxelNoise::Perlin3D(FVector3f( WorldX * WF + 400.0f, WorldY * WF + 500.0f + VoxelHash::SeedOffset(SeedU, 11.9f), EffectiveZ * WF + 600.0f + VoxelHash::SeedOffset(SeedU, 13.3f) )) * VOXEL_NOISE_SCALE * WS; // Apply warp to both noise positions - FVector WarpOffset(WarpX, WarpY, WarpZ); + FVector3f WarpOffset(WarpX, WarpY, WarpZ); MainPos += WarpOffset; FinePos += WarpOffset; } @@ -1978,7 +2139,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo float NoisedZ = WorldZ; if (Params.TerraceNoiseDisplacement > 0.0f) { - float DispNoise = FractalNoise3D(FVector( + float DispNoise = FractalNoise3D(FVector3f( WorldX * 0.04f + VoxelHash::SeedOffset(SeedU, 31.1f), WorldY * 0.04f + VoxelHash::SeedOffset(SeedU, 37.3f), WorldZ * 0.02f + VoxelHash::SeedOffset(SeedU, 41.7f) @@ -2104,7 +2265,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo if (DistFromSurface < OverhangRange) { // Low Z frequency (0.15x of XY) → features extend horizontally - float OverhangNoise = FractalNoise3D(FVector( + float OverhangNoise = FractalNoise3D(FVector3f( WorldX * Params.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 53.1f), WorldY * Params.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 59.3f), EffectiveZ * Params.OverhangFrequency * 0.15f + VoxelHash::SeedOffset(SeedU, 61.7f) @@ -2144,7 +2305,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // Near the surface (CaveSDF ≈ 0), the sign of CaveSDF tells us which // side we're on: negative = inside cave, positive = solid rock. // We use a noise-modulated vertical gradient to detect steep faces. - float VertGrad = VoxelNoise::Perlin3D(FVector( + float VertGrad = VoxelNoise::Perlin3D(FVector3f( WorldX * 0.05f + VoxelHash::SeedOffset(SeedU, 71.3f), WorldY * 0.05f + VoxelHash::SeedOffset(SeedU, 73.7f), EffectiveZ * 0.15f + VoxelHash::SeedOffset(SeedU, 79.1f) // 3x faster in Z → detects vertical features @@ -2185,7 +2346,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo { // Cellular noise: returns ~[-1, 1] where positive = cell interior (bowl) float SF = Params.ScallopFrequency; - float ScallopNoise = CellularNoise3D(FVector( + float ScallopNoise = CellularNoise3D(FVector3f( WorldX * SF + VoxelHash::SeedOffset(SeedU, 83.1f), WorldY * SF + VoxelHash::SeedOffset(SeedU, 89.3f), EffectiveZ * SF + VoxelHash::SeedOffset(SeedU, 97.7f) @@ -2212,47 +2373,21 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo && NearestRoomIdx >= 0) { const FCachedRoom& Room = SDFCache.Rooms[NearestRoomIdx]; - const int32 MaxArches = 3; const FVector VoxPos(WorldX, WorldY, WorldZ); - for (int32 i = 0; i < MaxArches; i++) + for (int32 i = 0; i < 3; i++) { - uint32 AH = VoxelHash::Mix(Room.Hash ^ (0xA4C400u + (uint32)i * 7369u)); - - if (VoxelHash::ToFloat01(AH) > Params.ArchDensity) continue; - - // Arch center XY: small offset from room center - uint32 AH2 = VoxelHash::Mix(AH ^ 0xA4C4u); - float ArcCX = Room.Center.X + VoxelHash::ToFloatSigned(AH2) * Room.RadiusXY * 0.3f; - float ArcCY = Room.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(AH2)) * Room.RadiusXY * 0.3f; - - // Arch height: mid-room Z (bridging the open space) - uint32 AH3 = VoxelHash::Mix(AH2 ^ 0xB41Du); - float ArcCZ = Room.Center.Z + VoxelHash::ToFloatSigned(AH3) * Room.RadiusZ * 0.4f; - - // Arch direction and span: stretch across most of the room - uint32 AH4 = VoxelHash::Mix(AH3 ^ 0xCAFEu); - float Angle = VoxelHash::ToFloat01(AH4) * PI; - float HalfSpan = Room.RadiusXY * (0.5f + VoxelHash::ToFloat01(VoxelHash::Mix(AH4)) * 0.35f); - - float CosA = FMath::Cos(Angle); - float SinA = FMath::Sin(Angle); - FVector ArchA(ArcCX - CosA * HalfSpan, ArcCY - SinA * HalfSpan, ArcCZ); - FVector ArchB(ArcCX + CosA * HalfSpan, ArcCY + SinA * HalfSpan, ArcCZ); - - // Arch thickness - uint32 AH5 = VoxelHash::Mix(AH4 ^ 0xF00Du); - float ArchRadius = FMath::Lerp(Params.ArchMinRadius, Params.ArchMaxRadius, - VoxelHash::ToFloat01(AH5)); - - float ArchSDF = VoxelSDF::Capsule(VoxPos, ArchA, ArchB, ArchRadius); + const FCachedArch& Arch = Room.Arches[i]; + if (!Arch.bActive) { continue; } + const float ArchSDF = VoxelSDF::Capsule( + VoxPos, Arch.EndpointA, Arch.EndpointB, Arch.Radius); const float ArchBlend = 2.0f; if (ArchSDF < ArchBlend) { float Fill = FMath::Clamp((ArchBlend - ArchSDF) / (ArchBlend * 2.0f), 0.0f, 1.0f); Fill = SmoothStep01(Fill); - Density += Fill * Params.BaseDensity * 1.5f; + Density += Fill * Arch.BaseDensity * 1.5f; } } } @@ -2356,58 +2491,32 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo && NearestRoomIdx >= 0) { const FCachedRoom& Room = SDFCache.Rooms[NearestRoomIdx]; - // Pinches on the perimeter of the room (near tunnel entry points) - const float Spread = 0.85f; - const int32 MaxPinches = 3; - - for (int32 i = 0; i < MaxPinches; i++) + for (int32 i = 0; i < 3; i++) { - uint32 PnH = VoxelHash::Mix(Room.Hash ^ (0xF1C400u + (uint32)i * 5417u)); - - if (VoxelHash::ToFloat01(PnH) > Params.PinchDensity) continue; - - // XY: near room perimeter (where tunnels meet the room) - uint32 PnH2 = VoxelHash::Mix(PnH ^ 0xF1C4u); - float PnX = Room.Center.X + VoxelHash::ToFloatSigned(PnH2) * Room.RadiusXY * Spread; - float PnY = Room.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(PnH2)) * Room.RadiusXY * Spread; - - // Z: mid-height within the room - uint32 PnH3 = VoxelHash::Mix(PnH2 ^ 0x5432u); - float PnZ = Room.Center.Z + VoxelHash::ToFloatSigned(PnH3) * Room.RadiusZ * 0.5f; - - // Pinch direction aligned toward room center (squeezes inward) - uint32 PnH4 = VoxelHash::Mix(PnH3 ^ 0x9A3Bu); - float PnAngle = VoxelHash::ToFloat01(PnH4) * PI; - float CosPN = FMath::Cos(PnAngle); - float SinPN = FMath::Sin(PnAngle); - - float DXPn = WorldX - PnX; - float DYPn = WorldY - PnY; - float DZPn = WorldZ - PnZ; + const FCachedPinch& Pinch = Room.Pinches[i]; + if (!Pinch.bActive) { continue; } + const float DXPn = WorldX - Pinch.CenterX; + const float DYPn = WorldY - Pinch.CenterY; + const float DZPn = WorldZ - Pinch.CenterZ; // Quick reject - float MaxExtent = FMath::Max(Params.PinchLength, Params.PinchStrength) + 5.0f; - if (FMath::Abs(DXPn) + FMath::Abs(DYPn) + FMath::Abs(DZPn) > MaxExtent) continue; + if (FMath::Abs(DXPn) + FMath::Abs(DYPn) + FMath::Abs(DZPn) > Pinch.MaxExtent) continue; - float Along = DXPn * CosPN + DYPn * SinPN; - float Across = -DXPn * SinPN + DYPn * CosPN; + const float Along = DXPn * Pinch.CosAngle + DYPn * Pinch.SinAngle; + const float Across = -DXPn * Pinch.SinAngle + DYPn * Pinch.CosAngle; - float HalfLength = Params.PinchLength * 0.5f; - float HalfNarrow = Params.PinchStrength; - float HalfVertical = Params.PinchStrength * 1.5f; - - float NAlong = Along / HalfLength; - float NAcross = Across / HalfNarrow; - float NUp = DZPn / HalfVertical; - float EllipDist = NAlong * NAlong + NAcross * NAcross + NUp * NUp; + const float NAlong = Along / Pinch.HalfLength; + const float NAcross = Across / Pinch.HalfNarrow; + const float NUp = DZPn / Pinch.HalfVertical; + const float EllipDist = NAlong * NAlong + NAcross * NAcross + NUp * NUp; if (EllipDist < 1.0f) { float Fill = 1.0f - EllipDist; Fill = SmoothStep01(Fill); - float AxisDist = FMath::Sqrt(NAcross * NAcross + NUp * NUp); - float SideFactor = FMath::Clamp(AxisDist * 2.0f, 0.0f, 1.0f); - Density += Fill * SideFactor * Params.BaseDensity * 1.5f; + const float AxisDist = FMath::Sqrt(NAcross * NAcross + NUp * NUp); + const float SideFactor = FMath::Clamp(AxisDist * 2.0f, 0.0f, 1.0f); + Density += Fill * SideFactor * Pinch.BaseDensity * 1.5f; } } } @@ -2480,7 +2589,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo { float WormZFreq = Params.WormFrequency * Params.WormHorizontalBias; - float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector( + float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f( WorldX * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f), WorldY * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f), EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f) @@ -2490,7 +2599,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // skip the second Perlin entirely (most voxels; bit-identical output). if (N1 < Params.WormThreshold) { - float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector( + float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f( WorldX * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f) + 137.0f, WorldY * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f) + 259.0f, EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f) + 431.0f @@ -2523,39 +2632,54 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo //========================================================================= if (StrateManager) { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - Params.BaseDensity, Params.BoundarySealThickness); + if (bApplyLegacyStructuralPosts) + { + StrateManager->ApplyPassageModifier( + Density, WorldX, WorldY, WorldZ, + Params.BaseDensity, Params.BoundarySealThickness); + } + else + { + // GetDensityAt owns one common MC-space structural tail after either the legacy or + // operator-stack branch. Keep the actual tube carve here, but do not repeat its + // landing air/floor/tunnel-air posts before the shared disturbance/post sequence. + StrateManager->ApplyPassageCarvingOnly( + Density, WorldX, WorldY, WorldZ, + Params.BaseDensity, Params.BoundarySealThickness); + } } - // Terrain operations and passage support floors are allowed to write solid density, but the - // graph's own tunnel floor/core are structural route geometry. Close the rounded bottom first, - // then reopen only the core above its half-voxel air clearance. Both predicates are shared - // with the operator stack and are evaluated from the per-chunk cache, never per-voxel rebuilt. - const bool bTunnelSupportFloor = - VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( - WorldX, WorldY, WorldZ, SDFCache); - if (bTunnelSupportFloor) + if (bApplyLegacyStructuralPosts) { - Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f)); - } - const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f) - ? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(WorldX, WorldY, WorldZ, SDFCache) - : FLT_MAX; - if (!bTunnelSupportFloor - && TunnelCoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels) - { - Density = FMath::Min( - Density, - -FMath::Max(Params.BaseDensity * 2.0f, 1.0f)); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); + // Terrain operations and passage support floors are allowed to write solid density, but + // the graph's own tunnel floor/core are structural route geometry. Direct callers of + // GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the + // same predicates once in its common MC-space post section below both generation paths. + const bool bTunnelSupportFloor = + VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( + WorldX, WorldY, WorldZ, SDFCache); + if (bTunnelSupportFloor) + { + Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f)); + } + const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f) + ? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(WorldX, WorldY, WorldZ, SDFCache) + : FLT_MAX; + if (!bTunnelSupportFloor + && TunnelCoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels) + { + Density = FMath::Min( + Density, + -FMath::Max(Params.BaseDensity * 2.0f, 1.0f)); + } + ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, + Params.StrateTopWorldZ, Params.StrateBottomWorldZ, + Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - // Fourth structural post: the XY edge wins over a passage near the rim. - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); + // Fourth structural post: the XY edge wins over a passage near the rim. + VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, + WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); + } // Convention MC: négatif = solide, positif = air. // La logique interne utilise positif = solide (plus lisible), donc on négate. @@ -2609,7 +2733,7 @@ float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ, if (Params.FloorRoughness > 0.0f) { float FF = Params.FloorRoughnessFrequency; - FloorNoise = FractalNoise3D(FVector( + FloorNoise = FractalNoise3D(FVector3f( WorldX * FF + VoxelHash::SeedOffset(SeedU, 7.3f), WorldY * FF + VoxelHash::SeedOffset(SeedU, 11.1f), 0.0f // XY-pur : plus aucune dépendance en Z / no Z dependence @@ -2640,7 +2764,7 @@ float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ, if (Params.CeilingRoughness > 0.0f) { float CF = Params.CeilingRoughnessFrequency; - float RawNoise = FractalNoise3D(FVector( + float RawNoise = FractalNoise3D(FVector3f( WorldX * CF + VoxelHash::SeedOffset(SeedU, 17.3f) + 1000.0f, WorldY * CF + VoxelHash::SeedOffset(SeedU, 19.7f) + 2000.0f, 3000.0f // XY-pur : décalage de décorrélation seul / offset only @@ -2872,7 +2996,7 @@ float UVoxelGenerator::GetMazeDensity(float WorldX, float WorldY, float WorldZ, // Wall roughness: perturb the corridor surface. if (Params.SurfaceRoughness > 0.0f && MazeSDF < R + Params.SurfaceRoughness + 2.0f) { - MazeSDF += FractalNoise3D(FVector(WorldX * 0.12f, WorldY * 0.12f, WorldZ * 0.12f), VoxelGenLOD::Eff(3)) + MazeSDF += FractalNoise3D(FVector3f(WorldX * 0.12f, WorldY * 0.12f, WorldZ * 0.12f), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE * Params.SurfaceRoughness; } @@ -2933,8 +3057,8 @@ float UVoxelGenerator::SampleSurfaceStructuralZ(float WorldX, float WorldY, if (Params.HeightWarpStrength > 0.0f) { const float WF = Params.HeightWarpFrequency; - const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.31f), WorldY * WF + 4.2f, VoxelHash::SeedOffset(SeedU, 1.7f))); - const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 8.6f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.53f), VoxelHash::SeedOffset(SeedU, 2.9f))); + const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.31f), WorldY * WF + 4.2f, VoxelHash::SeedOffset(SeedU, 1.7f))); + const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 8.6f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.53f), VoxelHash::SeedOffset(SeedU, 2.9f))); QX += wx * VOXEL_NOISE_SCALE * Params.HeightWarpStrength; QY += wy * VOXEL_NOISE_SCALE * Params.HeightWarpStrength; } @@ -2944,12 +3068,12 @@ float UVoxelGenerator::SampleSurfaceStructuralZ(float WorldX, float WorldY, const float Relief = SampleRelief(WorldX, WorldY, Params.ReliefFrequency, Params.ReliefContrast); const float M = FMath::Lerp(1.0f, Relief, Params.ReliefStrength); - float Cont = FractalNoise3D(FVector( + float Cont = FractalNoise3D(FVector3f( QX * Params.ContinentFrequency + VoxelHash::SeedOffset(SeedU, 3.1f), QY * Params.ContinentFrequency + VoxelHash::SeedOffset(SeedU, 5.7f), VoxelHash::SeedOffset(SeedU, 0.7f)), 4); // [-1,1] - float Detail = FractalNoise3D(FVector( + float Detail = FractalNoise3D(FVector3f( WorldX * Params.DetailFrequency + 11.0f, WorldY * Params.DetailFrequency + 22.0f, VoxelHash::SeedOffset(SeedU, 1.3f)), 3); // [-1,1] @@ -2957,7 +3081,7 @@ float UVoxelGenerator::SampleSurfaceStructuralZ(float WorldX, float WorldY, float Mountain = 0.0f; if (Params.MountainStrength > 0.0f) { - float Ridge = RidgedNoise3D(FVector( + float Ridge = RidgedNoise3D(FVector3f( QX * Params.MountainFrequency + 99.0f, QY * Params.MountainFrequency + 77.0f, VoxelHash::SeedOffset(SeedU, 0.9f)), 4); // [-1,1] @@ -3072,8 +3196,8 @@ float UVoxelGenerator::ComputeSurfaceCeiling(float WorldX, float WorldY, if (Params.CeilingWarpStrength > 0.0f) { const float WF = Params.CeilingWarpFrequency; - const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.71f), WorldY * WF + 2.3f, VoxelHash::SeedOffset(SeedU, 3.3f))); - const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 6.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.19f), VoxelHash::SeedOffset(SeedU, 4.7f))); + const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.71f), WorldY * WF + 2.3f, VoxelHash::SeedOffset(SeedU, 3.3f))); + const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 6.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.19f), VoxelHash::SeedOffset(SeedU, 4.7f))); QX += wx * VOXEL_NOISE_SCALE * Params.CeilingWarpStrength; QY += wy * VOXEL_NOISE_SCALE * Params.CeilingWarpStrength; } @@ -3081,7 +3205,7 @@ float UVoxelGenerator::ComputeSurfaceCeiling(float WorldX, float WorldY, // Broad SIGNED swell: raises/lowers the whole cap → big inverted hills and valleys. if (Params.CeilingUndulation > 0.0f) { - const float Swell = FractalNoise3D(FVector( + const float Swell = FractalNoise3D(FVector3f( QX * Params.CeilingUndulationFrequency + VoxelHash::SeedOffset(SeedU, 1.9f), QY * Params.CeilingUndulationFrequency + 13.0f, VoxelHash::SeedOffset(SeedU, 0.5f)), 3); // [-1,1] @@ -3093,14 +3217,14 @@ float UVoxelGenerator::ComputeSurfaceCeiling(float WorldX, float WorldY, float Hang = 0.0f; if (Params.CeilingRoughness > 0.0f) { - Hang += FMath::Abs(FractalNoise3D(FVector( + Hang += FMath::Abs(FractalNoise3D(FVector3f( WorldX * Params.CeilingRoughnessFrequency + 5.0f, WorldY * Params.CeilingRoughnessFrequency + 6.0f, VoxelHash::SeedOffset(SeedU, 2.1f)), 3)) * VOXEL_NOISE_SCALE * Params.CeilingRoughness; } if (Params.CeilingRidgeStrength > 0.0f) { - float Ridge = RidgedNoise3D(FVector( + float Ridge = RidgedNoise3D(FVector3f( QX * Params.CeilingRidgeFrequency + 31.0f, QY * Params.CeilingRidgeFrequency + 47.0f, VoxelHash::SeedOffset(SeedU, 1.1f)), 4); // [-1,1] @@ -3132,7 +3256,7 @@ float UVoxelGenerator::SurfaceDensityFromColumn(float WorldX, float WorldY, floa const uint32 SeedU = (uint32)Seed; const float f = S.OverhangFrequency; // Shelf-shape noise [0,1]; the Z term makes the reach fold/curl with height (ragged, not a lip). - const float Ns = FractalNoise3D(FVector( + const float Ns = FractalNoise3D(FVector3f( WorldX * f + VoxelHash::SeedOffset(SeedU, 17.3f), WorldY * f + VoxelHash::SeedOffset(SeedU, 23.9f), WorldZ * f * S.OverhangZScale + VoxelHash::SeedOffset(SeedU, 5.1f)), 3) * 0.5f + 0.5f; // [0,1] @@ -4146,7 +4270,7 @@ float UVoxelGenerator::SampleRelief(float WorldX, float WorldY, float Frequency, const uint32 SeedU = (uint32)Seed; // Same offsets/octaves as the original SurfaceWorld relief so existing worlds are // unchanged (this is the function that code path now calls). - float R = FractalNoise3D(FVector( + float R = FractalNoise3D(FVector3f( WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 7.3f), WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 2.1f), VoxelHash::SeedOffset(SeedU, 0.5f)), 2) * 0.5f + 0.5f; // [0,1] @@ -4157,7 +4281,7 @@ float UVoxelGenerator::SampleRelief(float WorldX, float WorldY, float Frequency, float UVoxelGenerator::SampleMoisture(float WorldX, float WorldY, float Frequency) const { const uint32 SeedU = (uint32)Seed; - const float N = FractalNoise3D(FVector( + const float N = FractalNoise3D(FVector3f( WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 4.7f), WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 8.9f), VoxelHash::SeedOffset(SeedU, 1.3f)), 2) * 0.5f + 0.5f; // [0,1] @@ -4241,8 +4365,8 @@ FBiomeSample UVoxelGenerator::SampleBiomeAt(float WorldX, float WorldY, const FB { const uint32 SeedU = (uint32)Seed; const float WF = MP.WarpFrequency; - const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.27f), WorldY * WF + 3.1f, VoxelHash::SeedOffset(SeedU, 1.1f))); - const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 7.7f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.61f), VoxelHash::SeedOffset(SeedU, 2.3f))); + const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.27f), WorldY * WF + 3.1f, VoxelHash::SeedOffset(SeedU, 1.1f))); + const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 7.7f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.61f), VoxelHash::SeedOffset(SeedU, 2.3f))); QX += wx * VOXEL_NOISE_SCALE * MP.WarpStrength; QY += wy * VOXEL_NOISE_SCALE * MP.WarpStrength; } @@ -4373,8 +4497,8 @@ FBiomeSample UVoxelGenerator::ResolveBiomeSampleAt(float WorldX, float WorldY, i { const uint32 SeedU = (uint32)Seed; const float WF = MP.WarpFrequency; - const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.27f), WorldY * WF + 3.1f, VoxelHash::SeedOffset(SeedU, 1.1f))); - const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 7.7f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.61f), VoxelHash::SeedOffset(SeedU, 2.3f))); + const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.27f), WorldY * WF + 3.1f, VoxelHash::SeedOffset(SeedU, 1.1f))); + const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 7.7f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.61f), VoxelHash::SeedOffset(SeedU, 2.3f))); QX += wx * VOXEL_NOISE_SCALE * MP.WarpStrength; QY += wy * VOXEL_NOISE_SCALE * MP.WarpStrength; } @@ -4861,7 +4985,7 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float // Wall roughness. if (Params.SurfaceRoughness > 0.0f && CaveSDF < Params.SurfaceRoughness + 4.0f) { - CaveSDF += FractalNoise3D(FVector(WorldX * 0.1f, WorldY * 0.1f, WorldZ * 0.1f), VoxelGenLOD::Eff(3)) + CaveSDF += FractalNoise3D(FVector3f(WorldX * 0.1f, WorldY * 0.1f, WorldZ * 0.1f), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE * Params.SurfaceRoughness; } @@ -4986,9 +5110,9 @@ float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, floa // NOTE : `SeedOffset` quantifie la clé de site par ×100, donc 0.0007 → site 0. Unique aujourd'hui // (toutes les autres clés du plugin sont ≥ 0.19) ; la prochaine clé sous 0.005 devra en choisir // une autre plutôt que de collisionner en silence. - const float WX = WorldX + FractalNoise3D(FVector(WorldX * 0.04f + VoxelHash::SeedOffset(S, 0.0007f), WorldY * 0.04f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) + const float WX = WorldX + FractalNoise3D(FVector3f(WorldX * 0.04f + VoxelHash::SeedOffset(S, 0.0007f), WorldY * 0.04f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE * WarpAmp; - const float WY = WorldY + FractalNoise3D(FVector(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) + const float WY = WorldY + FractalNoise3D(FVector3f(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE * WarpAmp; // Per-island constants (existence roll, jitter, radius, Z anchor, taper) are pure functions @@ -5075,7 +5199,7 @@ float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, floa // Craggy shells. if (Params.SurfaceRoughness > 0.0f && IslandSDF < Params.SurfaceRoughness + BlendK + 2.0f) { - IslandSDF += FractalNoise3D(FVector(WorldX * 0.08f, WorldY * 0.08f, WorldZ * 0.08f), VoxelGenLOD::Eff(4)) + IslandSDF += FractalNoise3D(FVector3f(WorldX * 0.08f, WorldY * 0.08f, WorldZ * 0.08f), VoxelGenLOD::Eff(4)) * VOXEL_NOISE_SCALE * Params.SurfaceRoughness; } diff --git a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index 06bcbc2..6593646 100644 --- a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -3,6 +3,7 @@ #include "VoxelMarchingCubesMesher.h" #include "MarchingCubesTables.h" +#include "VoxelDensityProfile.h" //============================================================================= // MAIN ALGORITHM @@ -17,6 +18,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, FVoxelMeshData MeshData; if (OutCaptureGrid) { OutCaptureGrid->Reset(); } if (!Generator) return MeshData; + VoxelDensityProfile::FScopedTimer MesherProfileTimer( + VoxelDensityProfile::EBucket::MesherGenerateMesh); + VoxelDensityProfile::FScopedTimer MesherSetupTimer( + VoxelDensityProfile::EBucket::MesherOther); // Cell size in voxels. No upper clamp: coarse clipmap levels use bigger steps (the EXTENT // grows). Coarse tiles also use FEWER cells (InCellsPerAxis) for cheaper gen. @@ -218,12 +223,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, const int32 GzLo = CzLo - 1; // coins CzLo..CzHi+1, gradients ±1 const int32 GzHi = CzHi + 2; // (== -1..GridDim sans bande) + MesherSetupTimer.End(); + // Réutilise le tampon entre tuiles (thread_local) : SetNumUninitialized garde la // capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile. static thread_local TArray DensityGrid; DensityGrid.SetNumUninitialized(MDim * MDim * MDim); // Bande de strate : seules les rangées Z réellement lues (cellules CzLo..CzHi + marges de // gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu. + VoxelDensityProfile::FScopedTimer MesherDensityGridTimer( + VoxelDensityProfile::EBucket::MesherDensityGrid); for (int32 gz = GzLo; gz <= GzHi; gz++) { if (ShouldAbortWork()) return FVoxelMeshData(); @@ -241,6 +250,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, } } } + MesherDensityGridTimer.End(); // ── CAPTURE-DURING-MESHING ── // Si demandé et que la tuile est pleine résolution (CellsPerAxis==CHUNK_SIZE ⇒ Step==1< CapTris; GroundTris.Reset(); CapTris.Reset(); + const bool bProfileMesher = VoxelDensityProfile::IsEnabled(); + uint64 CellClassificationCycles = 0; + uint64 GradientCycles = 0; + uint64 VertexInterpolationCycles = 0; + uint64 StreamBuildingCycles = 0; + uint64 CellClassificationCalls = 0; + uint64 SurfaceCellCalls = 0; for (int32 cz = CzLo; cz <= CzHi; cz++) // bande de strate : cf. CzLo/CzHi plus haut { if (ShouldAbortWork()) return FVoxelMeshData(); @@ -298,6 +315,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, if (ShouldAbortWork()) return FVoxelMeshData(); for (int32 cx = 0; cx < CellsPerAxis; cx++) { + const uint64 ClassificationStart = bProfileMesher + ? FPlatformTime::Cycles64() : 0; // PASSE 1 : densités aux 8 coins + index de cas MC SEULEMENT. // ~70% des cellules d'un chunk sont tout-roc ou tout-air (aucune surface) ; // on les rejette ICI, AVANT de payer les 8 positions + 8 gradients (48 lectures @@ -314,9 +333,18 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, if (D >= IsoLevel) CaseIndex |= (1 << i); } - if (MCEdgeTable[CaseIndex] == 0) continue; // Pas de surface ici → skip + const bool bHasSurface = MCEdgeTable[CaseIndex] != 0; + if (bProfileMesher) + { + CellClassificationCycles += FPlatformTime::Cycles64() - ClassificationStart; + ++CellClassificationCalls; + } + if (!bHasSurface) continue; // Pas de surface ici → skip + if (bProfileMesher) { ++SurfaceCellCalls; } // PASSE 2 : positions + gradients aux 8 coins (uniquement si surface présente). + const uint64 GradientStart = bProfileMesher + ? FPlatformTime::Cycles64() : 0; FVector Positions[8]; FVector Gradients[8]; for (int32 i = 0; i < 8; i++) @@ -328,10 +356,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, + FVector(GX * Step, GY * Step, GZ * Step) * VOXEL_SIZE; Gradients[i] = GradAt(GX, GY, GZ); } + if (bProfileMesher) + { + GradientCycles += FPlatformTime::Cycles64() - GradientStart; + } // Interpolation des positions + normales sur les arêtes traversées. t = point de // traversée de l'iso entre les deux coins (clampé, milieu si densités quasi-égales) ; // la normale interpole les gradients de coin par le même t. + const uint64 VertexInterpolationStart = bProfileMesher + ? FPlatformTime::Cycles64() : 0; FVector EdgeVertices[12]; FVector EdgeNormals[12]; for (int32 i = 0; i < 12; i++) @@ -348,9 +382,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, EdgeNormals[i] = Gradients[A] + T * (Gradients[B] - Gradients[A]); } } + if (bProfileMesher) + { + VertexInterpolationCycles += + FPlatformTime::Cycles64() - VertexInterpolationStart; + } // Génère les triangles avec vertices dédupliqués. // Ordre 0, 2, 1 (pas 0, 1, 2) pour le winding attendu par RealtimeMesh. + const uint64 StreamBuildingStart = bProfileMesher + ? FPlatformTime::Cycles64() : 0; for (int32 i = 0; MCTriTable[CaseIndex][i] != -1; i += 3) { const int32 E0 = MCTriTable[CaseIndex][i]; @@ -369,10 +410,33 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, Dst.Add(Idx2); Dst.Add(Idx1); } + if (bProfileMesher) + { + StreamBuildingCycles += FPlatformTime::Cycles64() - StreamBuildingStart; + } } } } + if (bProfileMesher) + { + VoxelDensityProfile::AddMeasurement( + VoxelDensityProfile::EBucket::MesherCellClassification, + CellClassificationCycles, CellClassificationCalls); + VoxelDensityProfile::AddMeasurement( + VoxelDensityProfile::EBucket::MesherGradientNormals, + GradientCycles, SurfaceCellCalls); + VoxelDensityProfile::AddMeasurement( + VoxelDensityProfile::EBucket::MesherVertexInterpolation, + VertexInterpolationCycles, SurfaceCellCalls); + VoxelDensityProfile::AddMeasurement( + VoxelDensityProfile::EBucket::MesherStreamBuilding, + StreamBuildingCycles, SurfaceCellCalls); + } + + VoxelDensityProfile::FScopedTimer MesherFinalisationTimer( + VoxelDensityProfile::EBucket::MesherOther); + //========================================================================= // SKIRTS — boucheurs de fissures aux coutures de LOD (clipmap) //========================================================================= diff --git a/Source/VoxelForge/Public/VoxelCaveMorphology.h b/Source/VoxelForge/Public/VoxelCaveMorphology.h index fe6c21f..372c101 100644 --- a/Source/VoxelForge/Public/VoxelCaveMorphology.h +++ b/Source/VoxelForge/Public/VoxelCaveMorphology.h @@ -439,16 +439,16 @@ namespace VoxelMazeTopology // point le plus bas qui voit déjà le hash, et le générateur comme la pile d'opérateurs l'appellent. // // Ce corps était `static float CellularNoise3D(const FVector&)` dans VoxelGenerator.cpp, invisible -// à la pile d'opérateurs. Déplacement LITTÉRAL : mêmes opérations, même ordre, même passage par -// `FVector` (donc par des doubles) — l'égalité binaire du portage TunnelNetwork en dépend. -// `UVoxelGenerator`'s copy is now a one-line forwarder; the body moved verbatim. +// à la pile d'opérateurs. Il vit ici pour que les deux chemins partagent la même implémentation. +// The hot overload uses FVector3f: noise coordinates are floats, so no double round-trip is paid +// before the hash/lattice arithmetic. Output compatibility with the old field is not a contract. // // Algorithme : distance au point-feature le plus proche dans une grille hachée. // 1. cellule entière du point 2. voisinage 3×3×3 3. rendre (F2 − F1), normalisé ~[-1, 1] // F2−F1 donne des frontières de cellules lisses avec des arêtes entre elles. namespace VoxelNoise { - FORCEINLINE float Cellular3D(const FVector& Position) + FORCEINLINE float Cellular3D(const FVector3f& Position) { // Integer cell coordinates int32 CellX = FMath::FloorToInt(Position.X); @@ -508,6 +508,11 @@ namespace VoxelNoise // Result is in [0, ~1.0]. Map to [-1, 1] for compatibility with other noise types. return Result * 2.0f - 1.0f; } + + FORCEINLINE float Cellular3D(const FVector& Position) + { + return Cellular3D(FVector3f((float)Position.X, (float)Position.Y, (float)Position.Z)); + } } //============================================================================= @@ -524,6 +529,32 @@ namespace VoxelNoise // With caching: build once, evaluate 32K times with just SDF math. // This is the single biggest CPU performance win for cave generation. +// Room-relative detail geometry whose hash/trigonometry is constant for a cached room. +// These are built once with the room's terrain-op parameters and then only evaluated per voxel. +struct FCachedArch +{ + bool bActive = false; + FVector EndpointA = FVector::ZeroVector; + FVector EndpointB = FVector::ZeroVector; + float Radius = 0.0f; + float BaseDensity = 0.0f; +}; + +struct FCachedPinch +{ + bool bActive = false; + float CenterX = 0.0f; + float CenterY = 0.0f; + float CenterZ = 0.0f; + float CosAngle = 1.0f; + float SinAngle = 0.0f; + float MaxExtent = 0.0f; + float HalfLength = 0.0f; + float HalfNarrow = 0.0f; + float HalfVertical = 0.0f; + float BaseDensity = 0.0f; +}; + // A room in the cache — everything needed for per-voxel SDF evaluation. // Internal details (CellX, CellY) used during cache building are NOT stored here. struct FCachedRoom @@ -568,6 +599,11 @@ struct FCachedRoom FVector ShapeA = FVector::ZeroVector; FVector ShapeB = FVector::ZeroVector; float ShapeR = 0.0f; + + // Hash-derived room details. The old evaluator rebuilt these hashes and angles for every + // surface sample; the cache build is the only place where their geometry can change. + FCachedArch Arches[3]; + FCachedPinch Pinches[3]; }; // A local flat-floor bridge over the overlap of two room bodies. It is not a new graph edge: it @@ -605,11 +641,112 @@ struct FCachedTunnel TArray WorldControlRadii; FVector WorldBoundCenter = FVector::ZeroVector; float WorldBoundRadiusSq = 0.0f; + // Centerline AABBs and scalar influence radii used by the immutable broad phase. The + // AABB test is a cheap lower bound on distance to the whole chain; the existing squared + // sphere test remains the exact conservative second-stage reject. + FVector SDFCenterlineMin = FVector::ZeroVector; + FVector SDFCenterlineMax = FVector::ZeroVector; + float SDFInfluenceRadius = 0.0f; + FVector WorldCenterlineMin = FVector::ZeroVector; + FVector WorldCenterlineMax = FVector::ZeroVector; + float WorldInfluenceRadius = 0.0f; // Bounding sphere for quick per-voxel rejection FVector BoundCenter; // Center of the bounding sphere float BoundRadiusSq; // Squared radius — if voxel is further, skip this tunnel }; +// A deterministic uniform-grid broad phase. Build() is called while a chunk cache is being +// assembled; after that the arrays are immutable and workers only read a bin range. Entries are +// supplied in candidate order, so each bin is already sorted by candidate ID and no runtime sort +// or allocation is needed. A failed/overflowing build leaves bValid=false and callers fall back +// to the exact legacy scan rather than risking a false negative. +struct FCaveSpatialIndexEntry +{ + int32 CandidateIndex = INDEX_NONE; + FVector Min = FVector::ZeroVector; + FVector Max = FVector::ZeroVector; +}; + +struct FCaveSpatialIndexBin +{ + int32 First = 0; + int32 Count = 0; +}; + +struct FCaveSpatialIndex +{ + static constexpr int32 BinSizeVoxels = 16; + + bool bValid = false; + int32 MinBinX = 0; + int32 MinBinY = 0; + int32 MinBinZ = 0; + int32 CellsX = 0; + int32 CellsY = 0; + int32 CellsZ = 0; + TArray Bins; + TArray CandidateIndices; + + void Reset(); + void Build(const TArray& Entries); + + // Returns false only when the index is invalid. A valid query outside its extent returns + // true with an empty range, which is the normal cheap reject for that sample. + bool GetBin(float X, float Y, float Z, int32& OutFirst, int32& OutCount) const; + + // Build-time/column-query helper. Coordinates are already integer cell coordinates. + bool GetBinAt(int32 CellX, int32 CellY, int32 CellZ, + int32& OutFirst, int32& OutCount) const; + + // A support-floor column is Z-dependent but not Z-search-dependent. Gather the unique + // tunnel IDs from every height bin crossing one XY cell once, then reuse the resulting floor + // intervals for all samples in that column. + template + bool GetColumnCandidates(float X, float Y, TArray& OutCandidates) const; +}; + +struct FTunnelSupportFloorInterval +{ + int32 TunnelIndex = INDEX_NONE; + float FloorZ = 0.0f; + float MinZ = 0.0f; + float MaxZ = 0.0f; +}; + +struct FTunnelSupportFloorColumn +{ + TArray> Intervals; + + FTunnelSupportFloorColumn() = default; + + FTunnelSupportFloorColumn(const FTunnelSupportFloorColumn& Other) + : Intervals(Other.Intervals) + { + } + + FTunnelSupportFloorColumn& operator=(const FTunnelSupportFloorColumn& Other) + { + Intervals = Other.Intervals; + return *this; + } + + FTunnelSupportFloorColumn(FTunnelSupportFloorColumn&& Other) + : Intervals(MoveTemp(Other.Intervals)) + { + } + + FTunnelSupportFloorColumn& operator=(FTunnelSupportFloorColumn&& Other) + { + Intervals = MoveTemp(Other.Intervals); + return *this; + } + + void Reset() + { + Intervals.Reset(); + } +}; + // A pre-baked pit shaft — position and dimensions resolved during BuildChunkCache. // // Pits are included in the main SDF evaluation (SmoothMin alongside rooms and @@ -667,6 +804,27 @@ struct FChunkSDFCache TArray Pits; TArray Chimneys; TArray Columns; + + // Immutable broad-phase indices. The world index is also the source for per-column support + // intervals; its Z bins are traversed only when a column is first built. + FCaveSpatialIndex RoomIndex; + FCaveSpatialIndex RoomFloorJoinIndex; + FCaveSpatialIndex TunnelIndex; + FCaveSpatialIndex TunnelWorldIndex; + + // Sparse immutable support-column table. SupportColumnEntries is a dense + // integer-XY slot map whose values index only non-empty SupportColumns. + int32 SupportColumnMinX = 0; + int32 SupportColumnMinY = 0; + int32 SupportColumnCellsX = 0; + int32 SupportColumnCellsY = 0; + TArray SupportColumnEntries; + TArray SupportColumns; + + // Allocator-backed bytes owned by this cache, including the nested tunnel chains and index + // arrays. The returned value excludes sizeof(FChunkSDFCache) itself so callers can add the + // enclosing entry's inline storage exactly once. + SIZE_T GetAllocatedSize() const; }; /** Combined world-space tunnel post query; one cache scan supplies both support and air tests. */ @@ -766,7 +924,8 @@ namespace VoxelCaveMorphology VOXELFORGE_API FTunnelCoreWorldEvaluation EvaluateTunnelCoreWorld( float WorldX, float WorldY, float WorldZ, - const FChunkSDFCache& Cache + const FChunkSDFCache& Cache, + const FTunnelSupportFloorColumn* SupportColumn = nullptr ); // True for the finite support slab beneath a world-space graph tunnel. The native density @@ -777,6 +936,30 @@ namespace VoxelCaveMorphology const FChunkSDFCache& Cache ); + // Build/query the same support-floor predicate at column granularity. This changes only + // where the deterministic projection is computed; it does not change the floor interval. + VOXELFORGE_API void BuildTunnelSupportFloorColumn( + float WorldX, float WorldY, + const FChunkSDFCache& Cache, + FTunnelSupportFloorColumn& OutColumn + ); + + VOXELFORGE_API bool IsTunnelSupportFloorColumnZ( + float WorldZ, + const FTunnelSupportFloorColumn& Column + ); + + // Returns the exact projected floor band for one tunnel at this XY column. + // The world evaluator uses FloorZ for its per-tunnel air ownership rule and + // Min/Max for the support slab itself. + VOXELFORGE_API bool GetTunnelSupportFloorColumnBand( + int32 TunnelIndex, + const FTunnelSupportFloorColumn& Column, + float& OutFloorZ, + float& OutMinZ, + float& OutMaxZ + ); + // CONVENIENCE WRAPPER: builds a temporary cache and evaluates in one call. // Use this for one-off queries (debug visualization, single-point sampling). // For chunk generation, use BuildChunkCache + EvaluateSDFCached instead. diff --git a/Source/VoxelForge/Public/VoxelDensityOpStack.h b/Source/VoxelForge/Public/VoxelDensityOpStack.h index 879a88a..97af679 100644 --- a/Source/VoxelForge/Public/VoxelDensityOpStack.h +++ b/Source/VoxelForge/Public/VoxelDensityOpStack.h @@ -130,6 +130,10 @@ public: int32 Num() const { return Ops.Num(); } + // Allocator-backed bytes owned by the stack, including each concrete operator allocation. + // The enclosing cache entry adds sizeof(FVoxelOpStack) separately. + SIZE_T GetAllocatedSize() const; + /** Hoist chunk-constant work for every op. Une fois par chunk et par worker. * Non-const : ça MUTE l'état par-chunk des opérateurs, et le prétendre const serait un * mensonge utile qui finirait par masquer une course. */ @@ -449,6 +453,9 @@ namespace VoxelDensityOps VOXELFORGE_API FRoomBoxDiagnostic GetLastRoomBoxDiagnostic(); + /** Current worker-local room-graph cache footprint; profiling/diagnostic use only. */ + VOXELFORGE_API void ReportWorkerRoomGraphCacheFootprint(); + /** * FloatingIslands — 8 ops, et **la pile tourne à l'ENVERS** : * ConstantVoid → IslandBlob → SdfRoughness → SdfFill → [structural post ×4] diff --git a/Source/VoxelForge/Public/VoxelDensityProfile.h b/Source/VoxelForge/Public/VoxelDensityProfile.h index 50611bf..e5c618e 100644 --- a/Source/VoxelForge/Public/VoxelDensityProfile.h +++ b/Source/VoxelForge/Public/VoxelDensityProfile.h @@ -47,6 +47,25 @@ namespace VoxelDensityProfile CaveTunnelAirOp, XYEdgeSealOp, OtherOp, + // Non-overlapping top-level phases. The named operation buckets above are intentionally + // nested diagnostics; these phase buckets are the accounting ledger that must add up to + // GetDensityAt without double-counting a stack op inside its enclosing phase. + DensityPrologue, + DensityCore, + DensityDisturbances, + DensityStructuralPosts, + DensityBoundarySeal, + DensityDiffLayer, + DensityTail, + // Mesher-side accounting. GenerateMesh is the inclusive total; the following buckets + // cover its regular-grid work and its non-grid setup/finalisation. + MesherGenerateMesh, + MesherDensityGrid, + MesherCellClassification, + MesherGradientNormals, + MesherVertexInterpolation, + MesherStreamBuilding, + MesherOther, Count }; @@ -64,6 +83,10 @@ namespace VoxelDensityProfile CaveTunnelEvaluated, TunnelCoreCandidates, TunnelCoreEvaluated, + TunnelSupportColumnBuilds, + TunnelSupportColumnCandidates, + TunnelSupportFloorQueries, + TunnelSupportFloorChecks, PassageCandidates, PassageEvaluated, Count @@ -76,6 +99,29 @@ namespace VoxelDensityProfile uint64 Calls[BucketCount]{}; uint64 Cycles[BucketCount]{}; uint64 Counters[CounterCount]{}; + + // Opt-in worker-cache footprint snapshot. These are current per-thread values (not + // allocation-event counters), so Snapshot() reports the resident cache footprint at the + // end of a profiling run rather than multiplying bytes by cache churn. + uint64 TunnelCacheWorkers = 0; + uint64 TunnelCacheCapacityEntries = 0; + uint64 TunnelCacheValidEntries = 0; + uint64 TunnelCacheStaticBytes = 0; + uint64 TunnelCacheDynamicBytes = 0; + uint64 TunnelCacheEntryBytes = 0; + uint64 TunnelCacheLargestEntryBytes = 0; + uint64 TunnelCacheLargestWorkerValidEntries = 0; + uint64 TunnelCacheLargestWorkerBytes = 0; + + uint64 RoomGraphCacheWorkers = 0; + uint64 RoomGraphCacheCapacityEntries = 0; + uint64 RoomGraphCacheValidEntries = 0; + uint64 RoomGraphCacheStaticBytes = 0; + uint64 RoomGraphCacheDynamicBytes = 0; + uint64 RoomGraphCacheEntryBytes = 0; + uint64 RoomGraphCacheLargestEntryBytes = 0; + uint64 RoomGraphCacheLargestWorkerValidEntries = 0; + uint64 RoomGraphCacheLargestWorkerBytes = 0; }; VOXELFORGE_API void SetEnabled(bool bEnabled); @@ -83,6 +129,18 @@ namespace VoxelDensityProfile VOXELFORGE_API void Reset(); VOXELFORGE_API FSnapshot Snapshot(); VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1); + VOXELFORGE_API void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls = 1); + + VOXELFORGE_API void SetWorkerTunnelCacheFootprint( + uint64 CapacityEntries, uint64 ValidEntries, + uint64 StaticBytes, uint64 DynamicBytes, + uint64 EntryBytes, uint64 LargestEntryBytes, + uint64 LargestWorkerValidEntries); + VOXELFORGE_API void SetWorkerRoomGraphCacheFootprint( + uint64 CapacityEntries, uint64 ValidEntries, + uint64 StaticBytes, uint64 DynamicBytes, + uint64 EntryBytes, uint64 LargestEntryBytes, + uint64 LargestWorkerValidEntries); VOXELFORGE_API const TCHAR* CounterName(ECounter Counter); VOXELFORGE_API EBucket BucketFromName(const TCHAR* Name); VOXELFORGE_API const TCHAR* BucketName(EBucket Bucket); @@ -111,6 +169,10 @@ namespace VoxelDensityProfile ~FScopedTimer(); + // End a range before its lexical scope ends. This lets GetDensityAt split its enclosing + // total into disjoint prologue/core/post phases without moving the large cache block. + void End(); + FScopedTimer(const FScopedTimer&) = delete; FScopedTimer& operator=(const FScopedTimer&) = delete; diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index 2c2d16d..b895944 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -160,7 +160,8 @@ public: */ float GetDensityWithParams(float WorldX, float WorldY, float WorldZ, const FStrateGenerationParams& Params, - uint32 ParamsFingerprint, uint32 LayoutVersion) const; + uint32 ParamsFingerprint, uint32 LayoutVersion, + bool bApplyLegacyStructuralPosts = true) const; /** * Densité pour une strate Slab (FlatPlain / CrystalChamber). diff --git a/Source/VoxelForge/Public/VoxelNoise.h b/Source/VoxelForge/Public/VoxelNoise.h index 9071f06..78e9cfd 100644 --- a/Source/VoxelForge/Public/VoxelNoise.h +++ b/Source/VoxelForge/Public/VoxelNoise.h @@ -132,6 +132,15 @@ FORCEINLINE float Perlin3D(const FVector& P) return Perlin3D((float)P.X, (float)P.Y, (float)P.Z); } +// Hot density callers already own float voxel coordinates. Keeping this overload separate from +// FVector avoids constructing UE5's double-precision FVector merely to round the coordinates back +// to float at the noise entry point. It is the same scalar formula and remains deterministic on +// every build of the module. +FORCEINLINE float Perlin3D(const FVector3f& P) +{ + return Perlin3D(P.X, P.Y, P.Z); +} + //============================================================================= // 4-WIDE BATCH — the SIMD multiplier. Computes 4 independent Perlin samples. // Inputs are 4-element arrays; unused lanes must be zero-filled by the caller diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 773766d..b9689a2 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -173,6 +173,7 @@ struct FExploreArguments bool bRender = true; bool bWalk = true; bool bExport = true; + bool bUseOperatorStack = true; bool bProfileDensity = false; bool bProfileLod = false; bool bFailureFocusRender = false; @@ -212,6 +213,9 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr FParse::Value(*Params, TEXT("archetype="), ArchetypeText); FParse::Value(*Params, TEXT("slot="), OutArguments.Slot); FParse::Value(*Params, TEXT("modes="), ModesText); + int32 UseOperatorStack = 1; + FParse::Value(*Params, TEXT("opstack="), UseOperatorStack); + OutArguments.bUseOperatorStack = UseOperatorStack != 0; OutArguments.bProfileDensity = FParse::Param(*Params, TEXT("profiledensity")); OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod")); FParse::Value(*Params, TEXT("out="), OutText); @@ -528,9 +532,9 @@ struct FExploreWorld Definition->GeneratorType = Index == Arguments.Slot ? Arguments.Archetype : ECaveGeneratorType::TunnelNetwork; - // Exercise the production operator-stack opt-in where it is implemented (Maze), - // while the runtime switch safely falls back for other archetypes. - Definition->bUseOperatorStack = true; + // Explicitly select the production switch so the same synthetic layout, seed and + // mesher invocation can be measured through both density branches. + Definition->bUseOperatorStack = Arguments.bUseOperatorStack; const TSoftObjectPtr SoftDefinition(Definition); Settings->FixedStrates.Add(Index, SoftDefinition); @@ -3270,6 +3274,7 @@ FString BuildManifestJson( Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("slot"), Arguments.Slot); + Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride); if (Arguments.bSurfaceRoughnessOverride) { @@ -3478,6 +3483,7 @@ FString BuildExploreJson( Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("slot"), Arguments.Slot); + Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride); if (Arguments.bSurfaceRoughnessOverride) { @@ -3494,10 +3500,11 @@ FString BuildExploreJson( Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory); Writer->WriteValue(TEXT("canonical_invocation"), FString::Printf( TEXT("UnrealEditor-Cmd VoxelM.uproject -run=VoxelForgeExplore -seed=%d -archetype=%s " - "-slot=%d -modes=%s -out=Saved/VoxelForge/Explore/"), + "-slot=%d -opstack=%d -modes=%s -out=Saved/VoxelForge/Explore/"), Arguments.Seed, ArchetypeName(Arguments.Archetype), Arguments.Slot, + Arguments.bUseOperatorStack ? 1 : 0, *Arguments.CanonicalModes())); Writer->WriteObjectEnd(); @@ -4123,6 +4130,102 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params) static_cast(DensityCalls), DensityTotalUs, DensityCalls > 0 ? DensityTotalUs / static_cast(DensityCalls) : 0.0); + const uint64 CacheTotalStaticBytes = Profile.TunnelCacheStaticBytes + + Profile.RoomGraphCacheStaticBytes; + const uint64 CacheTotalDynamicBytes = Profile.TunnelCacheDynamicBytes + + Profile.RoomGraphCacheDynamicBytes; + const uint64 CacheTotalBytes = CacheTotalStaticBytes + CacheTotalDynamicBytes; + const uint64 CacheWorkers = FMath::Max( + Profile.TunnelCacheWorkers, Profile.RoomGraphCacheWorkers); + const uint64 CacheValidEntries = Profile.TunnelCacheValidEntries + + Profile.RoomGraphCacheValidEntries; + const uint64 CacheEntryBytes = Profile.TunnelCacheEntryBytes + + Profile.RoomGraphCacheEntryBytes; + const uint64 CacheCapacityEntries = Profile.TunnelCacheCapacityEntries + + Profile.RoomGraphCacheCapacityEntries; + const uint64 CacheLargestEntryBytes = FMath::Max( + Profile.TunnelCacheLargestEntryBytes, + Profile.RoomGraphCacheLargestEntryBytes); + const uint64 CacheLargestWorkerBytes = FMath::Max( + Profile.TunnelCacheLargestWorkerBytes, + Profile.RoomGraphCacheLargestWorkerBytes); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCacheMemory] workers=%llu capacity_entries=%llu valid_entries=%llu " + "static_bytes=%llu dynamic_bytes=%llu total_bytes=%llu " + "avg_valid_entry_bytes=%.1f largest_entry_bytes=%llu " + "avg_worker_bytes=%.1f largest_worker_bytes=%llu " + "tunnel_avg_entry_bytes=%.1f tunnel_largest_entry_bytes=%llu " + "tunnel_largest_worker_valid=%llu " + "roomgraph_avg_entry_bytes=%.1f roomgraph_largest_entry_bytes=%llu " + "roomgraph_largest_worker_valid=%llu " + "tunnel_workers=%llu tunnel_capacity=%llu tunnel_valid=%llu " + "tunnel_static=%llu tunnel_dynamic=%llu roomgraph_workers=%llu " + "roomgraph_capacity=%llu roomgraph_valid=%llu roomgraph_static=%llu " + "roomgraph_dynamic=%llu"), + static_cast(CacheWorkers), + static_cast(CacheCapacityEntries), + static_cast(CacheValidEntries), + static_cast(CacheTotalStaticBytes), + static_cast(CacheTotalDynamicBytes), + static_cast(CacheTotalBytes), + CacheValidEntries > 0 + ? static_cast(CacheEntryBytes) / static_cast(CacheValidEntries) + : 0.0, + static_cast(CacheLargestEntryBytes), + CacheWorkers > 0 + ? static_cast(CacheTotalBytes) / static_cast(CacheWorkers) + : 0.0, + static_cast(CacheLargestWorkerBytes), + Profile.TunnelCacheValidEntries > 0 + ? static_cast(Profile.TunnelCacheEntryBytes) + / static_cast(Profile.TunnelCacheValidEntries) + : 0.0, + static_cast(Profile.TunnelCacheLargestEntryBytes), + static_cast(Profile.TunnelCacheLargestWorkerValidEntries), + Profile.RoomGraphCacheValidEntries > 0 + ? static_cast(Profile.RoomGraphCacheEntryBytes) + / static_cast(Profile.RoomGraphCacheValidEntries) + : 0.0, + static_cast(Profile.RoomGraphCacheLargestEntryBytes), + static_cast(Profile.RoomGraphCacheLargestWorkerValidEntries), + static_cast(Profile.TunnelCacheWorkers), + static_cast(Profile.TunnelCacheCapacityEntries), + static_cast(Profile.TunnelCacheValidEntries), + static_cast(Profile.TunnelCacheStaticBytes), + static_cast(Profile.TunnelCacheDynamicBytes), + static_cast(Profile.RoomGraphCacheWorkers), + static_cast(Profile.RoomGraphCacheCapacityEntries), + static_cast(Profile.RoomGraphCacheValidEntries), + static_cast(Profile.RoomGraphCacheStaticBytes), + static_cast(Profile.RoomGraphCacheDynamicBytes)); + + const VoxelDensityProfile::EBucket DensityPhases[] = { + VoxelDensityProfile::EBucket::DensityPrologue, + VoxelDensityProfile::EBucket::DensityCore, + VoxelDensityProfile::EBucket::DensityDisturbances, + VoxelDensityProfile::EBucket::DensityStructuralPosts, + VoxelDensityProfile::EBucket::DensityBoundarySeal, + VoxelDensityProfile::EBucket::DensityDiffLayer, + VoxelDensityProfile::EBucket::DensityTail, + }; + uint64 DensityAttributionCycles = 0; + uint64 DensityAttributionCalls = 0; + for (const VoxelDensityProfile::EBucket Phase : DensityPhases) + { + const int32 PhaseIndex = static_cast(Phase); + DensityAttributionCycles += Profile.Cycles[PhaseIndex]; + DensityAttributionCalls += Profile.Calls[PhaseIndex]; + } + const double DensityAttributionUs = + static_cast(DensityAttributionCycles) * CycleToMicroseconds; + const double DensityUnattributedUs = DensityTotalUs - DensityAttributionUs; + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeDensityProfile] attribution_sum_us=%.3f unattributed_us=%.3f " + "coverage=%.2f%% phase_calls=%llu"), + DensityAttributionUs, DensityUnattributedUs, + DensityTotalUs > 0.0 ? DensityAttributionUs * 100.0 / DensityTotalUs : 0.0, + static_cast(DensityAttributionCalls)); + for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index) { const VoxelDensityProfile::ECounter Counter = @@ -4151,6 +4254,37 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params) static_cast(Calls), TotalUs, TotalUs / static_cast(Calls)); } + + const VoxelDensityProfile::EBucket MesherBuckets[] = { + VoxelDensityProfile::EBucket::MesherDensityGrid, + VoxelDensityProfile::EBucket::MesherCellClassification, + VoxelDensityProfile::EBucket::MesherGradientNormals, + VoxelDensityProfile::EBucket::MesherVertexInterpolation, + VoxelDensityProfile::EBucket::MesherStreamBuilding, + VoxelDensityProfile::EBucket::MesherOther, + }; + uint64 MesherAttributionCycles = 0; + uint64 MesherAttributionCalls = 0; + for (const VoxelDensityProfile::EBucket Bucket : MesherBuckets) + { + const int32 BucketIndex = static_cast(Bucket); + MesherAttributionCycles += Profile.Cycles[BucketIndex]; + MesherAttributionCalls += Profile.Calls[BucketIndex]; + } + const int32 MesherTotalIndex = static_cast( + VoxelDensityProfile::EBucket::MesherGenerateMesh); + const double MesherTotalUs = static_cast( + Profile.Cycles[MesherTotalIndex]) * CycleToMicroseconds; + const double MesherAttributionUs = static_cast( + MesherAttributionCycles) * CycleToMicroseconds; + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeMesherProfile] total_calls=%llu total_us=%.3f " + "attribution_sum_us=%.3f unattributed_us=%.3f coverage=%.2f%% " + "phase_calls=%llu"), + static_cast(Profile.Calls[MesherTotalIndex]), + MesherTotalUs, MesherAttributionUs, MesherTotalUs - MesherAttributionUs, + MesherTotalUs > 0.0 ? MesherAttributionUs * 100.0 / MesherTotalUs : 0.0, + static_cast(MesherAttributionCalls)); VoxelDensityProfile::SetEnabled(false); } return bRequestedModeFailed ? 2 : 0;