diff --git a/CODEMAP.md b/CODEMAP.md index 9b88938..2fee138 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -302,7 +302,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error** | `ProcessPendingChunks` | 301 | Drains ProcessQueue under per-frame budget; reads the per-request cancellation token before removing its map entry, drops obsolete/aborted results, then applies each live result via `ApplyTileResult`. | | `ApplyTileResult` | — | **Shared game-thread apply** for one `FChunkResult` (async drain + sync carve): discards stale generation epochs and keys no longer desired, marks loaded, ingests capture, EMPTY releases the tile's existing component (a re-gen can flip content→empty on band change — old geometry must not linger), else `ApplyMeshToTile`. Returns true iff a visible mesh uploaded (counts the budget). Doesn't touch `PendingTiles` (caller's). | | `FVoxelStackSampler` / `FScopedVoxelStackRegistration` (`VoxelStackSampler.h/.cpp`) | — | **Diagnostic stack-sampling profiler (2026-09-13), off by default.** A sampler thread captures registered generation workers' stacks (`CaptureThreadStackBackTrace`) every N µs and tags each with the tile's LOD. It symbolises after the run (inline frames too) into CSV plus a summary under `Saved/`. Registration is per tile TASK in `LoadTile` (and the export's tasks), never per sample; disabled costs one relaxed atomic load. Game `-voxel.SampleStacks=`, commandlet `-samplestacks[=us]`. ~4-5% overhead at 1 ms, so trust large shares, not single-digit deltas. Report: `PERF-SAMPLED.md`. | -| Room-cache performance (2026-09-13): `VF_FindPlayerFitPointForRoomMemoized` / `GPlayerFitMemo` (`VoxelCaveMorphology.cpp`), `voxel.TileCacheWindow` + `VoxelGenLOD::TileOriginVoxels/TileCellsPerAxis` (`VoxelGenerator.h`), `FChunkSDFSpatialIndex` | — | **Player-fit memo:** exact 97-word key, process-wide, `voxel.PlayerFitMemo`. **Tile cache window:** the room cache covers the requesting tile's footprint plus a fixed collect margin. It is set via `TGuardValue` in `GenerateMesh`/classify, and is window-invariant by construction (the margin does not grow with the tile). **Spatial index:** ordered XY buckets over cache primitives, exact rejection. The `static_assert` in `VoxelStrateTypes.h` ties `VF_STRATE_PARAM_FIELDS` to `FStrateGenerationParams` (83 fields + 8 B padding). | +| Room-cache performance (2026-09-13): `VF_FindPlayerFitPointForRoomMemoized` / `GPlayerFitMemo` (`VoxelCaveMorphology.cpp`), `voxel.TileCacheWindow` + `VoxelGenLOD::TileOriginVoxels/TileCellsPerAxis` (`VoxelGenerator.h`), `FChunkSDFSpatialIndex` | — | **Player-fit memo:** exact 97-word key, process-wide, `voxel.PlayerFitMemo`. **Tile cache window:** the room cache covers the requesting tile's footprint plus a fixed collect margin. It is set via `TGuardValue` in `GenerateMesh`/classify, and is window-invariant by construction (the margin does not grow with the tile). **LOD-gated:** fused path at LOD ≥ `voxel.TileCacheWindowMinLOD` (3), op-stack at LOD ≥ `voxel.TileCacheWindowOpStackMinLOD` (4); lower LODs keep the 4-chunk worker window, because the tile window slowed LOD0 evaluation ~6%. Strates whose authored terrain ops exceed the proven 256-voxel extent (or are malformed/unloaded) fall back to the legacy window (`VF_ComputeTerrainOpEnvelope`). `voxel.SpatialIndex` is -1 auto (off at fused LOD0, on elsewhere), 0 or 1. **Spatial index:** ordered XY buckets over cache primitives, exact rejection. The `static_assert` in `VoxelStrateTypes.h` ties `VF_STRATE_PARAM_FIELDS` to `FStrateGenerationParams` (83 fields + 8 B padding). | | `GenerateTileResult` | — | **Shared worker-side gen** for one tile (async `LoadTile` task + sync `SyncRemeshTile`): [ClassifyTile (T1.d) + exact validation, only when `voxel.OuterClassifierMode=1`; **default 0 since 2026-09-13**] → `GenerateMesh`/`GenerateSheetMesh` → `BuildTileStreamSet`. Reads Generator/Mesher only → safe on a worker or the game thread; fills `FChunkResult`, no enqueue. | | `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread (`GenerateTileResult` + `ApplyTileResult` inline, Cells=CHUNK_SIZE/Step=1 + strate band, no capture). Used for the tile under the brush centre so a carve is visible THIS frame; one full-res gen on the game thread. | | `UpdateChunksAroundPosition` | 362 | Builds desired set, cancels pending keys that left it, sorts from the **true pawn position**, submits the occupied/support LOD0 tile and the next tile along the pawn heading as an absolute `BackgroundHigh` prefix, then loads/unloads and handles LOD changes. **Delta cull**: `BuildDesiredTiles` returns the LEAVERS (stamped `DesiredStamped` map, one sweep) — only those + `TransitionHold` are considered per crossing, not every loaded tile. `BuildDesiredTiles` also applies `RenderDistanceChunks`: the outermost shell widens to cover the distance — as level-MaxClipLevel MC tiles, or (F18 `bFarSheetRing`) as a SHEET ring at level MaxClipLevel+span (covered-check vs the MaxClipLevel box; `VF_OuterShell` shared with `IsTileInClipRange` so the cull sees the same horizon), dz pre-clamped to the vertical band. **§9.3 anchors:** also prunes dead `StreamingAnchors` + detects their chunk crossings → rebuilds the desired set when an anchor moves/(un)registers (`bAnchorsMoved`/`bForceDesiredRebuild`), same cadence as player movement. §8.10. | diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index 9c6193c..aa3f1f9 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -1538,12 +1538,13 @@ namespace template static int32 VF_ForEachSpatialCandidate( const FChunkSDFSpatialIndex& Index, int32 ItemCount, - float WorldX, float WorldY, FVisit&& Visit) + float WorldX, float WorldY, FVisit&& Visit, + bool bUseSpatialIndex = true) { int32 Begin = 0; int32 End = 0; int32 CandidateCount = 0; - if (Index.GetRange(WorldX, WorldY, Begin, End)) + if (bUseSpatialIndex && Index.GetRange(WorldX, WorldY, Begin, End)) { for (int32 Cursor = Begin; Cursor < End; ++Cursor) { @@ -1786,6 +1787,10 @@ struct FBuildRoomMouthRise struct FVFRoomGraphReach { float DirectRoomReach = 0.0f; + // Complete XY reach of a room body plus any authored terrain operation that can be selected + // for that room. This is also used by the STORE cull; leaving it out would drop a room whose + // pit/chimney/detail override extends beyond the base SDF body. + float TerrainOpReach = 0.0f; float FloorReliefEnvelope = 0.0f; float BlendEnvelope = 0.0f; float MaxTunnelLength = 0.0f; @@ -1808,8 +1813,324 @@ static float VF_RoomShapeReachUpperBound(float RadiusXY, float RadiusZ) return FMath::Max3(Ellipsoid, RoundedBox, Capsule); } +// Terrain operations are authored outside FStrateGenerationParams and are selected per room at +// cache-build time. The tile-window proof must therefore include both the transported params and +// every operation that the pool can select. The limit is deliberately finite: an authored feature +// beyond it uses the legacy worker window, while the fixed-margin proof remains valid for all +// assets that stay inside this envelope. +static constexpr float VF_MaxProvenTerrainOpExtent = 256.0f; + +static bool VF_ComputeTerrainOpEnvelope( + const FStrateGenerationParams& Params, + const TArray* TerrainOps, + float BaseRoomReach, + float AllRoomRadiusEnvelope, + float BlendEnvelope, + float& OutReach) +{ + float MaxReach = BaseRoomReach; + float MaxAuthoredExtent = 0.0f; + bool bHasActiveTerrainOperation = false; + + auto RecordAuthoredExtent = [&](float Extent) -> bool + { + if (!FMath::IsFinite(Extent) || Extent < 0.0f) + { + return false; + } + bHasActiveTerrainOperation = true; + MaxAuthoredExtent = FMath::Max(MaxAuthoredExtent, Extent); + return MaxAuthoredExtent <= VF_MaxProvenTerrainOpExtent; + }; + auto RecordReach = [&](float Reach) -> bool + { + if (!FMath::IsFinite(Reach) || Reach < 0.0f) + { + return false; + } + MaxReach = FMath::Max(MaxReach, Reach); + return FMath::IsFinite(MaxReach); + }; + auto RecordDetail = [&](float EffectRange) -> bool + { + return RecordAuthoredExtent(EffectRange) + && RecordReach(BaseRoomReach + EffectRange); + }; + auto RecordStructural = [&](float EffectExtent, float FeatureReach) -> bool + { + return RecordAuthoredExtent(EffectExtent) + && RecordReach(FeatureReach); + }; + + auto AccumulateParams = [&](const FStrateGenerationParams& OpParams) -> bool + { + const float OperationValues[] = { + OpParams.TerraceStepHeight, + OpParams.TerraceHardness, + OpParams.TerraceNoiseDisplacement, + OpParams.LayerLineSpacing, + OpParams.LayerLineDepth, + OpParams.OverhangStrength, + OpParams.OverhangDepth, + OpParams.OverhangFrequency, + OpParams.RibbingSpacing, + OpParams.RibbingDepth, + OpParams.CliffStrength, + OpParams.ScallopStrength, + OpParams.ScallopFrequency, + OpParams.ArchDensity, + OpParams.ArchMinRadius, + OpParams.ArchMaxRadius, + OpParams.ColumnDensity, + OpParams.ColumnMinRadius, + OpParams.ColumnMaxRadius, + OpParams.PitDensity, + OpParams.PitMinRadius, + OpParams.PitMaxRadius, + OpParams.PitDepth, + OpParams.ChimneyDensity, + OpParams.ChimneyMinRadius, + OpParams.ChimneyMaxRadius, + OpParams.ChimneyHeight, + OpParams.DomeDensity, + OpParams.DomeMinRadius, + OpParams.DomeMaxRadius, + OpParams.DomeHeightRatio, + OpParams.PinchDensity, + OpParams.PinchStrength, + OpParams.PinchLength}; + for (const float Value : OperationValues) + { + if (!FMath::IsFinite(Value)) + { + return false; + } + } + + if (OpParams.TerraceStepHeight > 0.0f) + { + if (OpParams.TerraceHardness < 0.0f || OpParams.TerraceHardness > 1.0f + || OpParams.TerraceNoiseDisplacement < 0.0f) + { + return false; + } + if (!RecordDetail( + OpParams.TerraceStepHeight * 3.0f + + OpParams.TerraceNoiseDisplacement + 4.0f)) + { + return false; + } + } + if (OpParams.LayerLineSpacing > 0.0f) + { + if (OpParams.LayerLineDepth < 0.0f + || !RecordDetail(OpParams.LayerLineSpacing + + OpParams.LayerLineDepth + 4.0f)) + { + return false; + } + } + if (OpParams.RibbingSpacing > 0.0f) + { + if (OpParams.RibbingDepth < 0.0f + || !RecordDetail(OpParams.RibbingSpacing + + OpParams.RibbingDepth + 4.0f)) + { + return false; + } + } + if (OpParams.CliffStrength > 0.0f) + { + if (!RecordDetail(OpParams.CliffStrength + 8.0f)) + { + return false; + } + } + if (OpParams.ScallopStrength > 0.0f) + { + if (OpParams.ScallopFrequency <= 0.0f + || !RecordDetail( + 4.0f / OpParams.ScallopFrequency + + OpParams.ScallopStrength + 4.0f)) + { + return false; + } + } + if (OpParams.OverhangStrength > 0.0f) + { + if (OpParams.OverhangDepth < 0.0f + || OpParams.OverhangFrequency <= 0.0f + || !RecordDetail(OpParams.OverhangDepth * 2.0f + 4.0f)) + { + return false; + } + } + if (OpParams.ArchDensity > 0.0f) + { + if (OpParams.ArchMinRadius < 0.0f || OpParams.ArchMaxRadius < 0.0f) + { + return false; + } + const float Radius = FMath::Max(OpParams.ArchMinRadius, OpParams.ArchMaxRadius); + if (!RecordStructural( + Radius + 4.0f, + AllRoomRadiusEnvelope * 1.2f + Radius + BlendEnvelope * 3.0f + 4.0f)) + { + return false; + } + } + if (OpParams.ColumnDensity > 0.0f) + { + if (OpParams.ColumnMinRadius < 0.0f || OpParams.ColumnMaxRadius < 0.0f) + { + return false; + } + const float Radius = FMath::Max(OpParams.ColumnMinRadius, OpParams.ColumnMaxRadius); + if (!RecordStructural( + Radius + 6.0f, + AllRoomRadiusEnvelope * 0.75f + Radius + 6.0f)) + { + return false; + } + } + if (OpParams.PitDensity > 0.0f) + { + if (OpParams.PitMinRadius < 0.0f || OpParams.PitMaxRadius < 0.0f + || OpParams.PitDepth < 0.0f) + { + return false; + } + const float Radius = FMath::Max(OpParams.PitMinRadius, OpParams.PitMaxRadius); + if (!RecordStructural( + 2.0f * Radius + OpParams.PitDepth + BlendEnvelope + 4.0f, + AllRoomRadiusEnvelope * 0.6f + 2.0f * Radius + + OpParams.PitDepth + BlendEnvelope + 4.0f)) + { + return false; + } + } + if (OpParams.ChimneyDensity > 0.0f) + { + if (OpParams.ChimneyMinRadius < 0.0f || OpParams.ChimneyMaxRadius < 0.0f + || OpParams.ChimneyHeight < 0.0f) + { + return false; + } + const float Radius = FMath::Max( + OpParams.ChimneyMinRadius, OpParams.ChimneyMaxRadius); + if (!RecordStructural( + 2.0f * Radius + OpParams.ChimneyHeight + BlendEnvelope + 4.0f, + AllRoomRadiusEnvelope * 0.6f + 2.0f * Radius + + OpParams.ChimneyHeight + BlendEnvelope + 4.0f)) + { + return false; + } + } + if (OpParams.DomeDensity > 0.0f) + { + if (OpParams.DomeMinRadius < 0.0f || OpParams.DomeMaxRadius < 0.0f + || OpParams.DomeHeightRatio < 0.0f) + { + return false; + } + const float Radius = FMath::Max(OpParams.DomeMinRadius, OpParams.DomeMaxRadius); + if (!RecordStructural( + Radius * (1.0f + OpParams.DomeHeightRatio) + 4.0f, + AllRoomRadiusEnvelope * 0.4f + Radius + * (1.0f + OpParams.DomeHeightRatio) + 4.0f)) + { + return false; + } + } + if (OpParams.PinchDensity > 0.0f) + { + if (OpParams.PinchStrength < 0.0f || OpParams.PinchLength < 0.0f) + { + return false; + } + const float Extent = FMath::Max(OpParams.PinchStrength, OpParams.PinchLength); + if (!RecordStructural( + Extent + 5.0f, + AllRoomRadiusEnvelope * 0.85f + Extent + 5.0f)) + { + return false; + } + } + return true; + }; + + if (!AccumulateParams(Params)) + { + return false; + } + + if (TerrainOps != nullptr) + { + for (const FStrateTerrainOpEntry& Entry : *TerrainOps) + { + if (!FMath::IsFinite(Entry.Probability) + || Entry.Probability < 0.0f || Entry.Probability > 1.0f + || !FMath::IsFinite(Entry.Weight) + || Entry.Weight < 0.0f || Entry.Weight > 3.0f) + { + return false; + } + if (Entry.Probability <= 0.0f || Entry.Weight <= 0.0f) + { + continue; + } + if (Entry.Operation.IsNull()) + { + continue; + } + const UVoxelTerrainOpDefinition* Operation = Entry.Operation.Get(); + if (Operation == nullptr) + { + // A referenced but unloaded asset is not a proof of absence. Keep the legacy + // window until the manager has a concrete definition to inspect. + return false; + } + switch (Operation->Type) + { + case EVoxelTerrainOpType::Terrace: + case EVoxelTerrainOpType::LayerLines: + case EVoxelTerrainOpType::Ribbing: + case EVoxelTerrainOpType::Cliff: + case EVoxelTerrainOpType::Scallop: + case EVoxelTerrainOpType::Overhang: + case EVoxelTerrainOpType::Arch: + case EVoxelTerrainOpType::Column: + case EVoxelTerrainOpType::Pit: + case EVoxelTerrainOpType::Chimney: + case EVoxelTerrainOpType::Dome: + case EVoxelTerrainOpType::Pinch: + break; + default: + return false; + } + + FStrateGenerationParams OperationParams{}; + Operation->ApplyTo(OperationParams, Entry.Weight); + if (!AccumulateParams(OperationParams)) + { + return false; + } + } + } + + // Keep the field at zero when the strate has no active terrain operation. The caller already + // has the base room envelope; using that same value as a second global room radius would make + // the legacy/no-op path store extra rooms and could change the canonical field. Active pools + // publish the wider envelope so the tile proof and STORE cull see pits/chimneys/detail reach. + OutReach = bHasActiveTerrainOperation ? MaxReach : 0.0f; + return FMath::IsFinite(OutReach) + && FMath::IsFinite(MaxAuthoredExtent) + && MaxAuthoredExtent <= VF_MaxProvenTerrainOpExtent; +} + static bool VF_ComputeRoomGraphReach( const FStrateGenerationParams& Params, + const TArray* TerrainOps, FVFRoomGraphReach& OutReach) { const float RelevantParams[] = { @@ -1892,9 +2213,17 @@ static bool VF_ComputeRoomGraphReach( FMath::Abs(Params.TunnelMaxRadius)) * 1.18f); const float MaxTunnelLength = FMath::Max( Params.MaxTunnelLength, 0.0f); - const float DirectRoomReach = VF_RoomShapeReachUpperBound( + const float BaseDirectRoomReach = VF_RoomShapeReachUpperBound( AllRoomRadiusEnvelope, AllRoomHeightEnvelope) + FloorReliefEnvelope + BlendEnvelope * 3.0f; + float TerrainOpReach = BaseDirectRoomReach; + if (!VF_ComputeTerrainOpEnvelope( + Params, TerrainOps, BaseDirectRoomReach, + AllRoomRadiusEnvelope, BlendEnvelope, TerrainOpReach)) + { + return false; + } + const float DirectRoomReach = FMath::Max(BaseDirectRoomReach, TerrainOpReach); const float TunnelInfluence = FMath::Abs(Params.TunnelWarpStrength) + TunnelRadiusEnvelope + BlendEnvelope * 3.0f; const float MaxInfluence = FMath::Max(DirectRoomReach, TunnelInfluence); @@ -1958,6 +2287,7 @@ static bool VF_ComputeRoomGraphReach( } OutReach.DirectRoomReach = DirectRoomReach; + OutReach.TerrainOpReach = TerrainOpReach; OutReach.FloorReliefEnvelope = FloorReliefEnvelope; OutReach.BlendEnvelope = BlendEnvelope; OutReach.MaxTunnelLength = MaxTunnelLength; @@ -2000,9 +2330,10 @@ static bool VF_MayHaveRoomGraphFeature( { return true; } - const float RoomReach = VF_RoomShapeReachUpperBound( + const float BaseRoomReach = VF_RoomShapeReachUpperBound( Room.RadiusXY, Room.RadiusZ) + Reach.FloorReliefEnvelope + Reach.BlendEnvelope * 3.0f; + const float RoomReach = FMath::Max(BaseRoomReach, Reach.TerrainOpReach); if (!FMath::IsFinite(RoomReach) || RoomReach < 0.0f) { return true; @@ -5474,7 +5805,8 @@ bool VoxelCaveMorphology::MayHaveFeatureInSearchBox( float SearchMaxX, float SearchMaxY, const FStrateGenerationParams& Params, uint32 Seed, int32 StrateIndex, - bool bUseZ, float SearchMinZ, float SearchMaxZ) + bool bUseZ, float SearchMinZ, float SearchMaxZ, + const TArray* TerrainOps) { if (!FMath::IsFinite(SearchMinX) || !FMath::IsFinite(SearchMinY) || !FMath::IsFinite(SearchMaxX) || !FMath::IsFinite(SearchMaxY) @@ -5496,7 +5828,7 @@ bool VoxelCaveMorphology::MayHaveFeatureInSearchBox( } FVFRoomGraphReach Reach; - if (!VF_ComputeRoomGraphReach(Params, Reach)) + if (!VF_ComputeRoomGraphReach(Params, TerrainOps, Reach)) { return true; } @@ -5639,10 +5971,11 @@ bool VoxelCaveMorphology::MayHaveFeatureInSearchBox( } bool VoxelCaveMorphology::IsRoomGraphWindowInvariant( - const FStrateGenerationParams& Params) + const FStrateGenerationParams& Params, + const TArray* TerrainOps) { FVFRoomGraphReach Reach; - return VF_ComputeRoomGraphReach(Params, Reach); + return VF_ComputeRoomGraphReach(Params, TerrainOps, Reach); } //============================================================================= @@ -5707,7 +6040,8 @@ void VoxelCaveMorphology::BuildChunkCache( // behavior below. The tile-window optimization is only a field-preserving optimization for // the finite parameter envelope accepted by VF_ComputeRoomGraphReach. FVFRoomGraphReach FeatureReach; - const bool bHasFeatureReach = VF_ComputeRoomGraphReach(Params, FeatureReach); + const bool bHasFeatureReach = VF_ComputeRoomGraphReach( + Params, TerrainOps, FeatureReach); //========================================================================= // INFLUENCE RADII @@ -5801,7 +6135,9 @@ void VoxelCaveMorphology::BuildChunkCache( auto RoomReachesSearchBox = [&](const FVector& C, float RadiusXY, float RadiusZ) -> bool { const float ShapeReach = VF_RoomShapeReachUpperBound(RadiusXY, RadiusZ); - const float Reach = ShapeReach + FloorReliefEnvelope + BlendEnvelope * 3.0f; + const float Reach = FMath::Max( + ShapeReach + FloorReliefEnvelope + BlendEnvelope * 3.0f, + bHasFeatureReach ? FeatureReach.TerrainOpReach : 0.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; @@ -6766,6 +7102,10 @@ void VoxelCaveMorphology::BuildChunkCache( FMath::Abs(CR.ShapeA.Z)); } MaxExtent = ShapeReach + FloorReliefEnvelope + BlendEnvelope * 3.0f; + if (bHasFeatureReach) + { + MaxExtent = FMath::Max(MaxExtent, FeatureReach.TerrainOpReach); + } CR.CullRadiusSq = MaxExtent * MaxExtent; } @@ -7004,7 +7344,8 @@ float VoxelCaveMorphology::EvaluateSDFCached( float WorldX, float WorldY, float WorldZ, const FChunkSDFCache& Cache, float SDFBlendRadius, - int32* OutNearestRoomIdx) + int32* OutNearestRoomIdx, + bool bUseSpatialIndex) { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::RoomGraphSdf); @@ -7117,7 +7458,8 @@ float VoxelCaveMorphology::EvaluateSDFCached( }; const int32 RoomCandidateCount = VF_ForEachSpatialCandidate( - Cache.RoomSpatialIndex, Cache.Rooms.Num(), WorldX, WorldY, EvaluateRoom); + Cache.RoomSpatialIndex, Cache.Rooms.Num(), WorldX, WorldY, EvaluateRoom, + bUseSpatialIndex); if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( @@ -7148,7 +7490,7 @@ float VoxelCaveMorphology::EvaluateSDFCached( }; VF_ForEachSpatialCandidate( Cache.RoomFloorJoinSpatialIndex, Cache.RoomFloorJoins.Num(), - WorldX, WorldY, EvaluateJoin); + WorldX, WorldY, EvaluateJoin, bUseSpatialIndex); //========================================================================= // Tunnel SDFs @@ -7188,7 +7530,8 @@ float VoxelCaveMorphology::EvaluateSDFCached( }; const int32 TunnelCandidateCount = VF_ForEachSpatialCandidate( - Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel); + Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel, + bUseSpatialIndex); if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( @@ -7207,7 +7550,8 @@ float VoxelCaveMorphology::EvaluateSDFCached( float VoxelCaveMorphology::EvaluateTunnelCoreSDF( float WorldX, float WorldY, float WorldZ, - const FChunkSDFCache& Cache) + const FChunkSDFCache& Cache, + bool bUseSpatialIndex) { const FVector Pos(WorldX, WorldY, WorldZ); float MinSDF = FLT_MAX; @@ -7237,7 +7581,8 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF( }; VF_ForEachSpatialCandidate( - Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel); + Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel, + bUseSpatialIndex); return MinSDF; } @@ -7245,7 +7590,8 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF( FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( float WorldX, float WorldY, float WorldZ, const FChunkSDFCache& Cache, - const FTunnelSupportFloorColumn* SupportColumn) + const FTunnelSupportFloorColumn* SupportColumn, + bool bUseSpatialIndex) { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCoreWorld); @@ -7358,7 +7704,8 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( }; const int32 TunnelCandidateCount = VF_ForEachSpatialCandidate( - Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel); + Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel, + bUseSpatialIndex); if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( @@ -7371,14 +7718,17 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( float WorldX, float WorldY, float WorldZ, - const FChunkSDFCache& Cache) + const FChunkSDFCache& Cache, + bool bUseSpatialIndex) { - return EvaluateTunnelCoreWorld(WorldX, WorldY, WorldZ, Cache).SDF; + return EvaluateTunnelCoreWorld( + WorldX, WorldY, WorldZ, Cache, nullptr, bUseSpatialIndex).SDF; } bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( float WorldX, float WorldY, float WorldZ, - const FChunkSDFCache& Cache) + const FChunkSDFCache& Cache, + bool bUseSpatialIndex) { const FVector Pos(WorldX, WorldY, WorldZ); @@ -7462,7 +7812,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( int32 Begin = 0; int32 End = 0; - if (Cache.TunnelSpatialIndex.GetRange(WorldX, WorldY, Begin, End)) + if (bUseSpatialIndex && Cache.TunnelSpatialIndex.GetRange(WorldX, WorldY, Begin, End)) { TestRange(Begin, End); } @@ -7494,7 +7844,8 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( void VoxelCaveMorphology::BuildTunnelSupportFloorColumn( float WorldX, float WorldY, const FChunkSDFCache& Cache, - FTunnelSupportFloorColumn& OutColumn) + FTunnelSupportFloorColumn& OutColumn, + bool bUseSpatialIndex) { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCoreSupport); @@ -7552,7 +7903,8 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn( } }; const int32 CandidateCount = VF_ForEachSpatialCandidate( - Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel); + Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel, + bUseSpatialIndex); if (VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index 1859010..983d33a 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -3822,6 +3822,21 @@ namespace * voxel PRÈS D'UNE SURFACE, pas partout) sans que onze opérateurs la refassent chacun. */ FStrateGenerationParams LocalParams; bool bLocalParamsValid = false; + + // Window invariance is a cache-policy proof, not a sample evaluator. Keep its result + // beside the worker-local source state so LOD0 does not pay the full reach walk once + // per voxel. + bool bTileCachePolicyValid = false; + bool bTileCachePolicyResult = false; + bool bTileCachePolicyHasContext = false; + int32 TileCachePolicySampleStep = 0; + FIntVector TileCachePolicyOrigin = FIntVector::ZeroValue; + int32 TileCachePolicyStep = 0; + int32 TileCachePolicyCells = 0; + uint32 TileCachePolicyFingerprint = 0xFFFFFFFFu; + uint32 TileCachePolicyLayout = 0xFFFFFFFFu; + uint64 TileCachePolicyManagerLifetimeId = 0; + const TArray* TileCachePolicyTerrainOps = nullptr; }; FState& State() const @@ -3861,7 +3876,8 @@ namespace FTunnelSupportFloorColumn& OutColumn) const { VoxelCaveMorphology::BuildTunnelSupportFloorColumn( - WorldX, WorldY, Cache, OutColumn); + WorldX, WorldY, Cache, OutColumn, + VoxelGenLOD::ShouldUseSpatialIndex(false)); } const FTunnelSupportFloorColumn* GetSupportColumn(const FVector& Position) const @@ -3917,7 +3933,8 @@ namespace const FVector& Position = S.LastWorldPosition; const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn(Position); S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( - Position.X, Position.Y, Position.Z, GetCache(), SupportColumn); + Position.X, Position.Y, Position.Z, GetCache(), SupportColumn, + VoxelGenLOD::ShouldUseSpatialIndex(false)); S.LastTunnelCoreEvaluationPosition = Position; S.bLastTunnelCoreWorldEvaluationValid = true; } @@ -3991,7 +4008,9 @@ namespace */ float ProbeSdfUnwarped(float X, float Y, float Z) const { - return VoxelCaveMorphology::EvaluateSDFCached(X, Y, Z, GetCache(), P.SDFBlendRadius); + return VoxelCaveMorphology::EvaluateSDFCached( + X, Y, Z, GetCache(), P.SDFBlendRadius, nullptr, + VoxelGenLOD::ShouldUseSpatialIndex(false)); } /** Le Z « effectif » : `VerticalScale` étire le monde AVANT le bruit. Pure fonction de Z et @@ -4126,10 +4145,47 @@ namespace FIntVector RequestedTileOrigin = FIntVector::ZeroValue; int32 RequestedTileStep = SampleStep; int32 RequestedTileCells = 0; - const bool bUseTileCacheWindow = VoxelGenLOD::IsTileCacheWindowEnabled() - && VoxelGenLOD::GetThreadTileCacheWindow( - RequestedTileOrigin, RequestedTileStep, RequestedTileCells) - && VoxelCaveMorphology::IsRoomGraphWindowInvariant(P); + const TArray* TerrainOps = nullptr; + if (LiveManager) + { + const int32 ChunkX = FMath::FloorToInt(WorldX / (float)CHUNK_SIZE); + const int32 ChunkY = FMath::FloorToInt(WorldY / (float)CHUNK_SIZE); + const int32 ChunkZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE); + UVoxelStrateDefinition* Def = LiveManager->GetStrateForChunk( + FIntVector(ChunkX, ChunkY, ChunkZ)); + if (Def) { TerrainOps = &Def->TerrainOperations; } + } + const bool bTileWindowEligible = VoxelGenLOD::IsTileCacheWindowEnabled(false); + const bool bHasTileWindowContext = VoxelGenLOD::GetThreadTileCacheWindow( + RequestedTileOrigin, RequestedTileStep, RequestedTileCells); + const bool bTilePolicyKeyChanged = + !S.bTileCachePolicyValid + || S.bTileCachePolicyHasContext + != (bTileWindowEligible && bHasTileWindowContext) + || S.TileCachePolicySampleStep != RequestedTileStep + || S.TileCachePolicyOrigin != RequestedTileOrigin + || S.TileCachePolicyStep != RequestedTileStep + || S.TileCachePolicyCells != RequestedTileCells + || S.TileCachePolicyFingerprint != ParamsFingerprint + || S.TileCachePolicyLayout != LayoutVersion + || S.TileCachePolicyManagerLifetimeId != ManagerLifetimeId + || S.TileCachePolicyTerrainOps != TerrainOps; + if (bTilePolicyKeyChanged) + { + S.bTileCachePolicyValid = true; + S.bTileCachePolicyHasContext = bTileWindowEligible && bHasTileWindowContext; + S.TileCachePolicySampleStep = RequestedTileStep; + S.TileCachePolicyOrigin = RequestedTileOrigin; + S.TileCachePolicyStep = RequestedTileStep; + S.TileCachePolicyCells = RequestedTileCells; + S.TileCachePolicyFingerprint = ParamsFingerprint; + S.TileCachePolicyLayout = LayoutVersion; + S.TileCachePolicyManagerLifetimeId = ManagerLifetimeId; + S.TileCachePolicyTerrainOps = TerrainOps; + S.bTileCachePolicyResult = S.bTileCachePolicyHasContext + && VoxelCaveMorphology::IsRoomGraphWindowInvariant(P, TerrainOps); + } + const bool bUseTileCacheWindow = S.bTileCachePolicyResult; int32 RegionSize = 0; int32 RegionMinX = 0; int32 RegionMinY = 0; @@ -4143,9 +4199,11 @@ namespace } else { - const int32 RegionChunks = SampleStep <= 1 - ? 1 - : (SampleStep >= 32 ? 4 : FMath::Max(4, (SampleStep + 3) / 4)); + // Fine requests retain the historical four-chunk worker window. The op-stack + // tile-sized window is gated to LOD4+ (the fused path starts at LOD3), so + // LOD0-LOD2 and the op-stack's LOD3 floor do not pay a full tile rebuild on their + // critical path while still reusing neighbouring chunks on the same worker. + const int32 RegionChunks = 4; RegionSize = CHUNK_SIZE * RegionChunks; const int32 RegionCellX = FMath::FloorToInt(WorldX / (float)RegionSize); const int32 RegionCellY = FMath::FloorToInt(WorldY / (float)RegionSize); @@ -4157,17 +4215,6 @@ namespace + (bUseTileCacheWindow ? static_cast(SampleStep) : 0.0f) + 2.0f; - const TArray* TerrainOps = nullptr; - if (LiveManager) - { - const int32 ChunkX = FMath::FloorToInt(WorldX / (float)CHUNK_SIZE); - const int32 ChunkY = FMath::FloorToInt(WorldY / (float)CHUNK_SIZE); - const int32 ChunkZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE); - UVoxelStrateDefinition* Def = LiveManager->GetStrateForChunk( - FIntVector(ChunkX, ChunkY, ChunkZ)); - if (Def) { TerrainOps = &Def->TerrainOperations; } - } - const FChunkSDFCache* PreviousActiveCache = S.ActiveCache; bool bRebuiltLocalCache = false; if (SampleStep > 1) @@ -4247,9 +4294,10 @@ namespace S.SupportColumns.Invalidate(); } + const bool bUseSpatialIndex = VoxelGenLOD::ShouldUseSpatialIndex(false); float CaveSDF = VoxelCaveMorphology::EvaluateSDFCached( WarpedX, WarpedY, WarpedZ, GetCache(), P.SDFBlendRadius, - &S.NearestRoom); + &S.NearestRoom, bUseSpatialIndex); //--------------------------------------------------------------- // PITS & CHEMINÉES — coordonnées RÉELLES, SmoothMin dans le même canal SDF @@ -4289,7 +4337,7 @@ namespace }; VF_ForEachChunkSDFSpatialCandidate( GetCache().PitSpatialIndex, GetCache().Pits.Num(), - WorldX, WorldY, EvaluatePit); + WorldX, WorldY, EvaluatePit, bUseSpatialIndex); auto EvaluateChimney = [&](int32 ChimneyIndex) { @@ -4320,7 +4368,7 @@ namespace }; VF_ForEachChunkSDFSpatialCandidate( GetCache().ChimneySpatialIndex, GetCache().Chimneys.Num(), - WorldX, WorldY, EvaluateChimney); + WorldX, WorldY, EvaluateChimney, bUseSpatialIndex); InOut.Sdf = CaveSDF; @@ -4332,7 +4380,8 @@ namespace const FTunnelSupportFloorColumn* SupportColumn = GetSupportColumn( S.LastWorldPosition); S.LastTunnelCoreWorldEvaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( - WorldX, WorldY, WorldZ, GetCache(), SupportColumn); + WorldX, WorldY, WorldZ, GetCache(), SupportColumn, + bUseSpatialIndex); S.LastTunnelCoreEvaluationPosition = S.LastWorldPosition; S.bLastTunnelCoreWorldEvaluationValid = true; if (S.LastTunnelCoreWorldEvaluation.bSupportFloor @@ -4782,13 +4831,49 @@ namespace return; } + int32 StrateIdx = 0; + const TArray* TerrainOps = nullptr; + if (LiveManager) + { + StrateIdx = LiveManager->GetStrateIndex( + ((float)CZ0 + 0.5f) * CHUNK_SIZE * VOXEL_SIZE); + for (int32 CZ = CZ0 + 1; CZ <= CZ1; ++CZ) + { + if (LiveManager->GetStrateIndex( + ((float)CZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE) != StrateIdx) + { + Unknown(); + return; + } + } + + // A room operation is part of the graph cache. Every Z slice covered by this + // proof must resolve to the same definition before one cache can represent it. + UVoxelStrateDefinition* Def0 = nullptr; + for (int32 CZ = CZ0; CZ <= CZ1; ++CZ) + { + UVoxelStrateDefinition* Def = + LiveManager->GetStrateForChunk(FIntVector(0, 0, CZ)); + if (CZ == CZ0) + { + Def0 = Def; + } + else if (Def != Def0) + { + Unknown(); + return; + } + } + if (Def0) { TerrainOps = &Def0->TerrainOperations; } + } + FIntVector RequestedTileOrigin = FIntVector::ZeroValue; int32 RequestedTileStep = FMath::Max(Ctx.Step, 1); int32 RequestedTileCells = 0; - const bool bUseTileCacheWindow = VoxelGenLOD::IsTileCacheWindowEnabled() + const bool bUseTileCacheWindow = VoxelGenLOD::IsTileCacheWindowEnabled(false) && VoxelGenLOD::GetThreadTileCacheWindow( RequestedTileOrigin, RequestedTileStep, RequestedTileCells) - && VoxelCaveMorphology::IsRoomGraphWindowInvariant(P); + && VoxelCaveMorphology::IsRoomGraphWindowInvariant(P, TerrainOps); const float RequestedTileExtent = static_cast( static_cast(RequestedTileStep) * static_cast(RequestedTileCells)); @@ -4849,46 +4934,6 @@ namespace return; } - int32 StrateIdx = 0; - const TArray* TerrainOps = nullptr; - if (LiveManager) - { - StrateIdx = LiveManager->GetStrateIndex( - ((float)CZ0 + 0.5f) * CHUNK_SIZE * VOXEL_SIZE); - for (int32 CZ = CZ0 + 1; CZ <= CZ1; ++CZ) - { - if (LiveManager->GetStrateIndex( - ((float)CZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE) != StrateIdx) - { - Unknown(); - return; - } - } - - // The room-op pool is part of the SDF: BuildChunkCache bakes pits, chimneys, and - // columns from it. Every chunk touched by this query must therefore resolve to the - // same definition before one cache can represent the box. - // GetStrateForChunk is a Z-layout lookup; the manager ignores - // X/Y. Check one representative per touched Z instead of - // repeating the same layout search for every coarse XY key. - UVoxelStrateDefinition* Def0 = nullptr; - for (int32 CZ = CZ0; CZ <= CZ1; ++CZ) - { - UVoxelStrateDefinition* Def = - LiveManager->GetStrateForChunk(FIntVector(0, 0, CZ)); - if (CZ == CZ0) - { - Def0 = Def; - } - else if (Def != Def0) - { - Unknown(); - return; - } - } - if (Def0) { TerrainOps = &Def0->TerrainOperations; } - } - const uint32 LV = Ctx.LayoutVersion; auto IsSharedCacheCurrent = [&]() -> bool { @@ -5085,7 +5130,8 @@ namespace CacheSearchMaxY, P, SeedU, StrateIdx, true, CacheSearchMinZ, - CacheSearchMaxZ); + CacheSearchMaxZ, + TerrainOps); const int64 MaxSharedChunkSpan = Ctx.Step >= 32 ? 32 : 16; bool bUsedSharedCache = false; const bool bTileWithinSharedBudget = bUseTileCacheWindow @@ -6747,13 +6793,15 @@ namespace { VoxelCaveMorphology::BuildTunnelSupportFloorColumn( static_cast(World.X), static_cast(World.Y), - GetBoxCache(), EmptySupportColumn); + GetBoxCache(), EmptySupportColumn, + VoxelGenLOD::ShouldUseSpatialIndex(false)); SupportColumn = &EmptySupportColumn; } const FTunnelCoreWorldEvaluation Evaluation = VoxelCaveMorphology::EvaluateTunnelCoreWorld( static_cast(World.X), static_cast(World.Y), - static_cast(World.Z), GetBoxCache(), SupportColumn); + static_cast(World.Z), GetBoxCache(), SupportColumn, + VoxelGenLOD::ShouldUseSpatialIndex(false)); ++B.ExactTailEvaluated; if (!FMath::IsFinite(Evaluation.SDF)) { @@ -8062,7 +8110,8 @@ namespace }; VF_ForEachChunkSDFSpatialCandidate( Rooms->GetCache().ColumnSpatialIndex, - Rooms->GetCache().Columns.Num(), WorldX, WorldY, EvaluateColumn); + Rooms->GetCache().Columns.Num(), WorldX, WorldY, EvaluateColumn, + VoxelGenLOD::ShouldUseSpatialIndex(false)); } /** The cached column list is the complete source of this op's work. A valid empty list is diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 54a7827..f2ca025 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -56,15 +56,34 @@ namespace } } - // A tile-sized cache window is the production default. Keep the switch in the generator - // module so native and op-stack sources make the same A/B decision, including when a - // headless harness applies command-line CVars after module startup. + // A tile-sized cache window is the production default on the coarse paths where it wins. + // Keep the switches in the generator module so native and op-stack sources make the same A/B + // decision, including when a headless harness applies command-line CVars after module startup. int32 GVoxelForgeTileCacheWindow = 1; FAutoConsoleVariableRef CVarVoxelForgeTileCacheWindow( TEXT("voxel.TileCacheWindow"), GVoxelForgeTileCacheWindow, TEXT("Use the requesting tile's aligned footprint plus the deterministic warp/halo margin for room caches.")); bool GVoxelForgeTileCacheWindowSwitchParsed = false; + // Refreshed profiles put the fused crossover at LOD3 and the op-stack crossover at LOD4. + // Separate defaults keep a path that loses at LOD3 from paying for a larger candidate set. + int32 GVoxelForgeTileCacheWindowMinLOD = 3; + FAutoConsoleVariableRef CVarVoxelForgeTileCacheWindowMinLOD( + TEXT("voxel.TileCacheWindowMinLOD"), + GVoxelForgeTileCacheWindowMinLOD, + TEXT("Minimum SampleStep LOD for the fused tile-sized room cache window.")); + int32 GVoxelForgeTileCacheWindowOpStackMinLOD = 4; + FAutoConsoleVariableRef CVarVoxelForgeTileCacheWindowOpStackMinLOD( + TEXT("voxel.TileCacheWindowOpStackMinLOD"), + GVoxelForgeTileCacheWindowOpStackMinLOD, + TEXT("Minimum SampleStep LOD for the op-stack tile-sized room cache window.")); + + int32 GVoxelForgeSpatialIndex = -1; + FAutoConsoleVariableRef CVarVoxelForgeSpatialIndex( + TEXT("voxel.SpatialIndex"), + GVoxelForgeSpatialIndex, + TEXT("Room-cache spatial broad phase: -1 measured auto policy, 0 off, 1 on.")); + bool GVoxelForgeSpatialIndexSwitchParsed = false; void VF_ParseTileCacheWindowSwitch() { @@ -80,6 +99,42 @@ namespace GVoxelForgeTileCacheWindow = CommandLineValue; } GVoxelForgeTileCacheWindow = GVoxelForgeTileCacheWindow != 0 ? 1 : 0; + + int32 MinLODCommandLineValue = GVoxelForgeTileCacheWindowMinLOD; + if (FParse::Value( + FCommandLine::Get(), TEXT("voxel.TileCacheWindowMinLOD="), + MinLODCommandLineValue)) + { + GVoxelForgeTileCacheWindowMinLOD = MinLODCommandLineValue; + } + GVoxelForgeTileCacheWindowMinLOD = FMath::Clamp( + GVoxelForgeTileCacheWindowMinLOD, 0, 5); + + int32 OpStackMinLODCommandLineValue = GVoxelForgeTileCacheWindowOpStackMinLOD; + if (FParse::Value( + FCommandLine::Get(), TEXT("voxel.TileCacheWindowOpStackMinLOD="), + OpStackMinLODCommandLineValue)) + { + GVoxelForgeTileCacheWindowOpStackMinLOD = OpStackMinLODCommandLineValue; + } + GVoxelForgeTileCacheWindowOpStackMinLOD = FMath::Clamp( + GVoxelForgeTileCacheWindowOpStackMinLOD, 0, 5); + } + + void VF_ParseSpatialIndexSwitch() + { + if (GVoxelForgeSpatialIndexSwitchParsed) + { + return; + } + GVoxelForgeSpatialIndexSwitchParsed = true; + int32 CommandLineValue = GVoxelForgeSpatialIndex; + if (FParse::Value( + FCommandLine::Get(), TEXT("voxel.SpatialIndex="), CommandLineValue)) + { + GVoxelForgeSpatialIndex = CommandLineValue; + } + GVoxelForgeSpatialIndex = FMath::Clamp(GVoxelForgeSpatialIndex, -1, 1); } } @@ -460,10 +515,18 @@ void VoxelGenLOD::SetThreadSampleStep(int32 Value) SampleStep = Value; } -bool VoxelGenLOD::IsTileCacheWindowEnabled() +bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath) { VF_ParseTileCacheWindowSwitch(); - return GVoxelForgeTileCacheWindow != 0; + if (GVoxelForgeTileCacheWindow == 0) + { + return false; + } + const int32 RequiredMinLOD = bFusedPath + ? GVoxelForgeTileCacheWindowMinLOD + : GVoxelForgeTileCacheWindowOpStackMinLOD; + const int32 RequiredStep = 1 << FMath::Clamp(RequiredMinLOD, 0, 5); + return FMath::Max(SampleStep, 1) >= RequiredStep; } bool VoxelGenLOD::GetThreadTileCacheWindow( @@ -485,6 +548,25 @@ bool VoxelGenLOD::GetThreadTileCacheWindow( && Extent <= static_cast(CHUNK_SIZE) * 64; } +bool VoxelGenLOD::ShouldUseSpatialIndex(bool bFusedPath) +{ + VF_ParseSpatialIndexSwitch(); + if (GVoxelForgeSpatialIndex >= 0) + { + return GVoxelForgeSpatialIndex != 0; + } + + // The full game sweep makes the fused LOD0 floor faster without the broad phase. Keep it + // enabled for LOD1+ where the candidate reduction pays for GetRange, and keep the measured + // op-stack policy enabled at every tested LOD. The explicit override remains useful for + // field/determinism A/B runs. + if (bFusedPath && FMath::Max(SampleStep, 1) <= 1) + { + return false; + } + return true; +} + // NOTE (T2.a): the fBm/Ridged bodies moved to VoxelNoise.h, where octaves are evaluated // 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, @@ -1244,7 +1326,8 @@ namespace static void BuildTunnelSupportColumnsForChunk( FChunkSDFCache& Cache, - const FIntVector& ChunkCoord); + const FIntVector& ChunkCoord, + bool bUseSpatialIndex); static const FTunnelSupportFloorColumn* FindTunnelSupportColumn( const FChunkSDFCache& Cache, @@ -1353,10 +1436,16 @@ namespace FIntVector RequestedTileOrigin = FIntVector::ZeroValue; int32 RequestedTileStep = 1; int32 RequestedTileCells = 0; - const bool bUseTileCacheWindow = VoxelGenLOD::IsTileCacheWindowEnabled() + const TArray* TerrainOps = nullptr; + if (UVoxelStrateDefinition* Def = Manager.GetStrateForChunk(ChunkCoord)) + { + TerrainOps = &Def->TerrainOperations; + } + const bool bUseTileCacheWindow = VoxelGenLOD::IsTileCacheWindowEnabled(true) && VoxelGenLOD::GetThreadTileCacheWindow( RequestedTileOrigin, RequestedTileStep, RequestedTileCells) - && VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params); + && VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params, TerrainOps); + const bool bUseSpatialIndex = VoxelGenLOD::ShouldUseSpatialIndex(true); const int32 StrateIndex = Manager.GetStrateIndex( (static_cast(ChunkCoord.Z) + 0.5f) * static_cast(CHUNK_SIZE) * VOXEL_SIZE); @@ -1382,7 +1471,8 @@ namespace // density cursor crosses a chunk; the room/tunnel graph itself is not rebuilt. if (OutState.SupportChunk != ChunkCoord) { - BuildTunnelSupportColumnsForChunk(OutState.Cache, ChunkCoord); + BuildTunnelSupportColumnsForChunk( + OutState.Cache, ChunkCoord, bUseSpatialIndex); OutState.SupportChunk = ChunkCoord; } OutState.Chunk = ChunkCoord; @@ -1421,7 +1511,7 @@ namespace StrateIndex, nullptr, ERoomGraphBuildSite::GeneratorTunnelCore); BuildTunnelSupportColumnsForChunk( - OutState.Cache, ChunkCoord); + OutState.Cache, ChunkCoord, bUseSpatialIndex); OutState.OwnerId = OwnerId; OutState.ManagerLifetimeId = ManagerLifetimeId; OutState.Chunk = ChunkCoord; @@ -1440,7 +1530,8 @@ namespace static void BuildTunnelSupportColumnsForChunk( FChunkSDFCache& Cache, - const FIntVector& ChunkCoord) + const FIntVector& ChunkCoord, + bool bUseSpatialIndex) { const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1; const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1; @@ -1463,7 +1554,8 @@ namespace static_cast(MinX + X), static_cast(MinY + Y), Cache, - Column); + Column, + bUseSpatialIndex); if (Column.Intervals.Num() == 0) { continue; @@ -1805,7 +1897,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co FIntVector RequestedTileOrigin = FIntVector::ZeroValue; int32 RequestedTileStep = 1; int32 RequestedTileCells = 0; - const bool bTileCacheContext = VoxelGenLOD::IsTileCacheWindowEnabled() + const bool bTileCacheContext = VoxelGenLOD::IsTileCacheWindowEnabled( + !bShareTunnelStackByZ) && VoxelGenLOD::GetThreadTileCacheWindow( RequestedTileOrigin, RequestedTileStep, RequestedTileCells); FVoxelOpStack* ActiveOpStack = &CP_OpStack; @@ -2352,7 +2445,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCorePosts); PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( - WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, SupportColumn); + WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, SupportColumn, + VoxelGenLOD::ShouldUseSpatialIndex( + CP_UseOpStack ? bUsedFusedEvaluator : true)); bHavePreDisturbanceTunnelCore = true; } const bool bProtectAuthoredTunnelFloor = bHavePreDisturbanceTunnelCore @@ -2499,7 +2594,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co VoxelDensityProfile::EBucket::TunnelCorePosts); TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, - SupportColumn); + SupportColumn, + VoxelGenLOD::ShouldUseSpatialIndex( + CP_UseOpStack ? bUsedFusedEvaluator : true)); bHaveTunnelCore = true; } if (bHaveTunnelCore) @@ -2831,15 +2928,27 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo thread_local int32 CachedTileCells = 0; thread_local uint64 CachedManagerLifetimeId = 0; thread_local const TArray* CachedTerrainOps = nullptr; + // The envelope proof is constant for a cache-policy key. Do not run the complete room/edge + // reach calculation for every density sample: that would turn a build-time guard into a + // per-sample cost on the LOD0 floor. + thread_local bool CachedTilePolicyValid = false; + thread_local bool CachedTilePolicyResult = false; + thread_local bool CachedTilePolicyHasContext = false; + thread_local bool CachedTilePolicyFusedPath = false; + thread_local int32 CachedTilePolicySampleStep = 0; + thread_local FIntVector CachedTilePolicyOrigin = FIntVector::ZeroValue; + thread_local int32 CachedTilePolicyStep = 0; + thread_local int32 CachedTilePolicyCells = 0; + thread_local uint32 CachedTilePolicyFingerprint = 0xFFFFFFFFu; + thread_local uint32 CachedTilePolicyLayout = 0xFFFFFFFFu; + thread_local uint64 CachedTilePolicyManagerLifetimeId = 0; + thread_local const TArray* CachedTilePolicyTerrainOps = nullptr; const bool bUseFusedCacheWindow = OutTunnelCore != nullptr; + const bool bUseSpatialIndex = VoxelGenLOD::ShouldUseSpatialIndex(bUseFusedCacheWindow); FIntVector RequestedTileOrigin = FIntVector::ZeroValue; int32 RequestedTileStep = 1; int32 RequestedTileCells = 0; - const bool bUseTileCacheWindow = VoxelGenLOD::IsTileCacheWindowEnabled() - && VoxelGenLOD::GetThreadTileCacheWindow( - RequestedTileOrigin, RequestedTileStep, RequestedTileCells) - && VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params); const uint64 CurrentManagerLifetimeId = StrateManager ? StrateManager->GetCacheLifetimeId() : 0; const int32 QueryChunkX = FMath::FloorToInt(WorldX / (float)CHUNK_SIZE); @@ -2865,6 +2974,39 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo QueryChunkZ)); if (Def) TerrainOps = &Def->TerrainOperations; } + const bool bTileWindowEligible = VoxelGenLOD::IsTileCacheWindowEnabled( + bUseFusedCacheWindow); + const bool bHasTileWindowContext = VoxelGenLOD::GetThreadTileCacheWindow( + RequestedTileOrigin, RequestedTileStep, RequestedTileCells); + const bool bTilePolicyKeyChanged = + !CachedTilePolicyValid + || CachedTilePolicyHasContext != (bTileWindowEligible && bHasTileWindowContext) + || CachedTilePolicyFusedPath != bUseFusedCacheWindow + || CachedTilePolicySampleStep != RequestedTileStep + || CachedTilePolicyOrigin != RequestedTileOrigin + || CachedTilePolicyStep != RequestedTileStep + || CachedTilePolicyCells != RequestedTileCells + || CachedTilePolicyFingerprint != ParamsFingerprint + || CachedTilePolicyLayout != LayoutVersion + || CachedTilePolicyManagerLifetimeId != CurrentManagerLifetimeId + || CachedTilePolicyTerrainOps != TerrainOps; + if (bTilePolicyKeyChanged) + { + CachedTilePolicyValid = true; + CachedTilePolicyHasContext = bTileWindowEligible && bHasTileWindowContext; + CachedTilePolicyFusedPath = bUseFusedCacheWindow; + CachedTilePolicySampleStep = RequestedTileStep; + CachedTilePolicyOrigin = RequestedTileOrigin; + CachedTilePolicyStep = RequestedTileStep; + CachedTilePolicyCells = RequestedTileCells; + CachedTilePolicyFingerprint = ParamsFingerprint; + CachedTilePolicyLayout = LayoutVersion; + CachedTilePolicyManagerLifetimeId = CurrentManagerLifetimeId; + CachedTilePolicyTerrainOps = TerrainOps; + CachedTilePolicyResult = CachedTilePolicyHasContext + && VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params, TerrainOps); + } + const bool bUseTileCacheWindow = CachedTilePolicyResult; // Index of the room with the smallest (most-inside) SDF for this voxel. // Written by EvaluateSDFCached, read by the terrain ops block to pick the @@ -2989,7 +3131,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo CaveSDF = VoxelCaveMorphology::EvaluateSDFCached( WarpedX, WarpedY, WarpedZ, SDFCache, Params.SDFBlendRadius, - &NearestRoomIdx + &NearestRoomIdx, + bUseSpatialIndex ); // ── PIT & CHIMNEY SDF INTEGRATION ── @@ -3039,7 +3182,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK); }; VF_ForEachChunkSDFSpatialCandidate( - SDFCache.PitSpatialIndex, SDFCache.Pits.Num(), WorldX, WorldY, EvaluatePit); + SDFCache.PitSpatialIndex, SDFCache.Pits.Num(), WorldX, WorldY, EvaluatePit, + bUseSpatialIndex); auto EvaluateChimney = [&](int32 ChimneyIndex) { @@ -3071,7 +3215,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo }; VF_ForEachChunkSDFSpatialCandidate( SDFCache.ChimneySpatialIndex, SDFCache.Chimneys.Num(), - WorldX, WorldY, EvaluateChimney); + WorldX, WorldY, EvaluateChimney, bUseSpatialIndex); // Convert SDF to density carving: // CaveSDF < 0 means we're inside a room/tunnel/pit → carve to air @@ -3102,7 +3246,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // The interpreted source has a lazy worker LRU for block evaluation; building that table // here would turn a cache-window change into hundreds of unnecessary column builds. *OutTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( - WorldX, WorldY, WorldZ, SDFCache, nullptr); + WorldX, WorldY, WorldZ, SDFCache, nullptr, bUseSpatialIndex); } //========================================================================= @@ -3328,9 +3472,11 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // We use the cached SDF so this costs two extra SDF evaluations per voxel, // only when near a surface — the common case is cheap (DistFromSurface > TerraceRange). float SDF_Zp1 = VoxelCaveMorphology::EvaluateSDFCached( - WorldX, WorldY, WorldZ + 1.0f, SDFCache, Params.SDFBlendRadius); + WorldX, WorldY, WorldZ + 1.0f, SDFCache, Params.SDFBlendRadius, + nullptr, bUseSpatialIndex); float SDF_Zm1 = VoxelCaveMorphology::EvaluateSDFCached( - WorldX, WorldY, WorldZ - 1.0f, SDFCache, Params.SDFBlendRadius); + WorldX, WorldY, WorldZ - 1.0f, SDFCache, Params.SDFBlendRadius, + nullptr, bUseSpatialIndex); float GZ = (SDF_Zp1 - SDF_Zm1) * 0.5f; // Normalized vertical component: 1 = perfectly horizontal surface (floor/ceiling) // 0 = perfectly vertical surface (wall) @@ -3620,7 +3766,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo } }; VF_ForEachChunkSDFSpatialCandidate( - SDFCache.ColumnSpatialIndex, SDFCache.Columns.Num(), WorldX, WorldY, EvaluateColumn); + SDFCache.ColumnSpatialIndex, SDFCache.Columns.Num(), WorldX, WorldY, EvaluateColumn, + bUseSpatialIndex); //========================================================================= // STEP 4g: DOMES (room-relative hemispherical ceilings) @@ -3881,7 +4028,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // 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); + WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex); // Keep the direct legacy entry point capability-safe for callers outside the canonical // operator-stack path; its support band follows the same relieved swept floor. if (bTunnelSupportFloor) @@ -3899,7 +4046,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo Density = FMath::Max(Density, StructuralSolidDensity); } const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f) - ? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(WorldX, WorldY, WorldZ, SDFCache) + ? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( + WorldX, WorldY, WorldZ, SDFCache, bUseSpatialIndex) : FLT_MAX; if (!bTunnelSupportFloor && TunnelCoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels) diff --git a/Source/VoxelForge/Private/VoxelStackSampler.cpp b/Source/VoxelForge/Private/VoxelStackSampler.cpp index e25caee..339c555 100644 --- a/Source/VoxelForge/Private/VoxelStackSampler.cpp +++ b/Source/VoxelForge/Private/VoxelStackSampler.cpp @@ -735,6 +735,8 @@ struct FStoredSample const FResolvedProgramCounter* LeafEntry = nullptr; const FResolvedSymbol* LeafFunction = nullptr; const FResolvedSymbol* LeafSource = nullptr; + uint64 LeafProgramCounter = 0; + bool bHasLeafProgramCounter = false; bool bSampleHasResolvedLeaf = false; bool bSampleHasWaitLike = false; @@ -755,8 +757,8 @@ struct FStoredSample } if (FrameIndex == 0) { - LeafEntry = Entry; - LeafFunction = FrameFunction; + LeafProgramCounter = ProgramCounter; + bHasLeafProgramCounter = true; bSampleHasResolvedLeaf = true; } } @@ -769,12 +771,8 @@ struct FStoredSample { if (FrameIndex == 0) { - LeafEntry = Entry; - if (LeafFunction == nullptr) - { - LeafFunction = FirstFunctionSymbol(*Entry); - } - LeafSource = FirstSourceSymbol(*Entry); + LeafProgramCounter = ProgramCounter; + bHasLeafProgramCounter = true; } for (const FResolvedSymbol& Symbol : Entry->Symbols) { @@ -792,9 +790,17 @@ struct FStoredSample } } - if (LeafEntry != nullptr && LeafSource == nullptr) + // SymbolCache is a TMap. FindOrAdd while resolving later frames may rehash it, so + // pointers captured during the loop are not stable. Re-find the leaf only after all + // PCs for this sample have been inserted. + if (bHasLeafProgramCounter) { - LeafSource = FirstSourceSymbol(*LeafEntry); + LeafEntry = SymbolCache.Find(LeafProgramCounter); + if (LeafEntry != nullptr) + { + LeafFunction = FirstFunctionSymbol(*LeafEntry); + LeafSource = FirstSourceSymbol(*LeafEntry); + } } auto AddToAggregate = [&](FProfileAggregateSet& Aggregate) diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 4040d51..01a870f 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -3849,9 +3849,21 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector return bMeasureTile && TileProfileEnd.Counters[Index] >= TileProfileStart.Counters[Index] ? TileProfileEnd.Counters[Index] - TileProfileStart.Counters[Index] : 0; }; + const auto CycleDelta = [&](VoxelDensityProfile::EBucket Bucket) -> uint64 + { + const int32 Index = static_cast(Bucket); + return bMeasureTile && TileProfileEnd.Cycles[Index] >= TileProfileStart.Cycles[Index] + ? TileProfileEnd.Cycles[Index] - TileProfileStart.Cycles[Index] : 0; + }; + const double TileSeconds = FPlatformTime::Seconds() - TileStartSeconds; + const double CacheBuildSeconds = FPlatformTime::ToSeconds64( + CycleDelta(VoxelDensityProfile::EBucket::RoomGraphBuild)); + const double EvaluationSeconds = FMath::Max( + 0.0, TileSeconds - CacheBuildSeconds); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d " "verdict=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f " + "cache_build=%.6f evaluation=%.6f " "refine=%u stack=%u core_samples=%u final_samples=%u core_hits=%u final_hits=%u " "core_leaves=%u final_leaves=%u tail_queries=%u tail_eval=%u " "stack_work=%.6f core_work=%.6f final_work=%.6f tail_work=%.6f " @@ -3868,7 +3880,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells, bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0, Result.bEmpty ? 1 : 0, ClassifyVerdict, ClassifySeconds, MeshSeconds, StreamSeconds, - FPlatformTime::Seconds() - TileStartSeconds, + TileSeconds, CacheBuildSeconds, EvaluationSeconds, ClassifierStats.RefineNodes, ClassifierStats.StackBoxCalls, ClassifierStats.ExactCoreSamples, ClassifierStats.ExactFinalSamples, ClassifierStats.ExactCoreCacheHits, ClassifierStats.ExactFinalCacheHits, diff --git a/Source/VoxelForge/Public/VoxelCaveMorphology.h b/Source/VoxelForge/Public/VoxelCaveMorphology.h index da3d388..60c8f6c 100644 --- a/Source/VoxelForge/Public/VoxelCaveMorphology.h +++ b/Source/VoxelForge/Public/VoxelCaveMorphology.h @@ -852,12 +852,13 @@ struct FChunkSDFSpatialIndex template FORCEINLINE int32 VF_ForEachChunkSDFSpatialCandidate( const FChunkSDFSpatialIndex& Index, int32 ItemCount, - float WorldX, float WorldY, FVisit&& Visit) + float WorldX, float WorldY, FVisit&& Visit, + bool bUseSpatialIndex = true) { int32 Begin = 0; int32 End = 0; int32 CandidateCount = 0; - if (Index.GetRange(WorldX, WorldY, Begin, End)) + if (bUseSpatialIndex && Index.GetRange(WorldX, WorldY, Begin, End)) { for (int32 Cursor = Begin; Cursor < End; ++Cursor) { @@ -1008,14 +1009,16 @@ namespace VoxelCaveMorphology const FStrateGenerationParams& Params, uint32 Seed, int32 StrateIndex, bool bUseZ = false, - float SearchMinZ = 0.0f, float SearchMaxZ = 0.0f + float SearchMinZ = 0.0f, float SearchMaxZ = 0.0f, + const TArray* TerrainOps = nullptr ); // True only for the finite parameter envelope for which BuildChunkCache's fixed collect // margin proves window invariance. Callers may widen the XY store window to a whole tile only // when this returns true; malformed or unbounded authored input must retain the legacy path. VOXELFORGE_API bool IsRoomGraphWindowInvariant( - const FStrateGenerationParams& Params + const FStrateGenerationParams& Params, + const TArray* TerrainOps = nullptr ); // PHASE 2: Evaluate the SDF at a single world position using cached data. @@ -1036,7 +1039,8 @@ namespace VoxelCaveMorphology float WorldX, float WorldY, float WorldZ, const FChunkSDFCache& Cache, float SDFBlendRadius, - int32* OutNearestRoomIdx = nullptr + int32* OutNearestRoomIdx = nullptr, + bool bUseSpatialIndex = true ); // Evaluate the raw union of cached graph-tunnel capsules. This is intentionally separate from @@ -1044,7 +1048,8 @@ namespace VoxelCaveMorphology // tunnel's walkable air core without reintroducing a connector or changing room ownership. VOXELFORGE_API float EvaluateTunnelCoreSDF( float WorldX, float WorldY, float WorldZ, - const FChunkSDFCache& Cache + const FChunkSDFCache& Cache, + bool bUseSpatialIndex = true ); // Evaluate the final structural air contract against the world-space wandering chain. Room @@ -1052,13 +1057,15 @@ namespace VoxelCaveMorphology // walkable floor follows the authored chain exactly. VOXELFORGE_API float EvaluateTunnelCoreWorldSDF( float WorldX, float WorldY, float WorldZ, - const FChunkSDFCache& Cache + const FChunkSDFCache& Cache, + bool bUseSpatialIndex = true ); VOXELFORGE_API FTunnelCoreWorldEvaluation EvaluateTunnelCoreWorld( float WorldX, float WorldY, float WorldZ, const FChunkSDFCache& Cache, - const FTunnelSupportFloorColumn* SupportColumn = nullptr + const FTunnelSupportFloorColumn* SupportColumn = nullptr, + bool bUseSpatialIndex = true ); // True for the finite support slab beneath a world-space graph tunnel. The native density @@ -1066,7 +1073,8 @@ namespace VoxelCaveMorphology // the rounded capsule's bottom from becoming an unsupported point. VOXELFORGE_API bool IsTunnelSupportFloorWorldPoint( float WorldX, float WorldY, float WorldZ, - const FChunkSDFCache& Cache + const FChunkSDFCache& Cache, + bool bUseSpatialIndex = true ); // Build/query the same support-floor predicate at column granularity. The column stores its @@ -1075,7 +1083,8 @@ namespace VoxelCaveMorphology VOXELFORGE_API void BuildTunnelSupportFloorColumn( float WorldX, float WorldY, const FChunkSDFCache& Cache, - FTunnelSupportFloorColumn& OutColumn + FTunnelSupportFloorColumn& OutColumn, + bool bUseSpatialIndex = true ); VOXELFORGE_API bool IsTunnelSupportFloorColumnZ( diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index ff183f7..cd5a0c4 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -63,7 +63,7 @@ namespace VoxelGenLOD VOXELFORGE_API void SetThreadOctaveBias(int32 Value); VOXELFORGE_API int32 GetThreadSampleStep(); VOXELFORGE_API void SetThreadSampleStep(int32 Value); - VOXELFORGE_API bool IsTileCacheWindowEnabled(); + VOXELFORGE_API bool IsTileCacheWindowEnabled(bool bFusedPath = true); VOXELFORGE_API bool GetThreadTileCacheWindow( FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis); @@ -426,6 +426,13 @@ private: const FBiomeContext& Ctx, FChunkBiomeCache& Cache) const; }; +namespace VoxelGenLOD +{ + // -1 uses the measured path/LOD policy; 0/1 force the spatial broad phase off/on for an A/B + // run. The policy affects candidate enumeration only, never cache contents or ordering. + VOXELFORGE_API bool ShouldUseSpatialIndex(bool bFusedPath); +} + /** * Optional diagnostics for one ClassifyTile call. This is populated only when the caller asks * for it (the streaming profiler does); it is never consulted by the proof and cannot affect a