perf(game): the path he actually walks -- meshed p95 4.91s -> 0.41s, RSS 19.5GB -> 1.7GB
The owner: "I want real performance gain for the ingame path... not in deletion, but
in optimization and tricks." Three rounds of ours had improved the commandlet export
he never runs. This one is the streaming path.
MEMORY -- it was retention, not the new lattice
19.5 GB was worker FChunkSDFCache arrays keeping capacity when keys were replaced
(946 MB across 15 workers in diagnostics), shared graph caches with no byte or
lifetime budget, and batch commandlets holding transient UObject worlds until exit.
BlockSkip is bounded to 8^3 bytes per tile; interval scratch is worker-local and
capped. Fixes: release storage on key replacement, LRU with a 256 MiB resident
budget, limit in-flight graph builds, drop oversized entries, CollectGarbage between
batch cases.
peak RSS 19.5 GB -> 1.643 GB (their runs) / 1.71 GB (mine, 3x128^3) -91%
a 3-case stress batch showed no accumulation
WHY SOME EMPTY TILES COST 5.6s AND OTHERS 3us -- a 1.9 MILLION x spread
Fast ones returned straight from the layout/edge proof (verdict AllAir/AllSolid,
refine=0, mesh=0). Slow ones returned Mixed, so the caller ran the FULL mesher grid
and only discovered emptiness by brute force -- logs show empty=1 after ~5 s of mesh
work. The interval machinery now reaches a verdict before a mesh job is enqueued.
IN GAME, 841 requests at streaming radius:
count p50 p95 max
empty 394 6.087 ms 12.113 ms 25.049 ms
meshed 447 114.748 ms 410.314 ms 844.043 ms
LOD0 empty 221 7.126 ms 11.901 ms 25.049 ms
LOD0 meshed 122 355.794 ms 600.945 ms 844.043 ms
against d38d4ca: meshed p50/p95/max 0.6085 / 4.9075 / 5.0199 s
LOD4 empty max 5.618 s
=> meshed p50 5.3x, p95 12.0x, and the multi-second empty outliers are gone.
REQUEST-TO-READY -- the number that answers "I can outrun the generation and fall"
all tiles p50 57.576 ms p95 429.052 ms max 905.633 ms
empty p50 30.943 ms p95 98.302 ms max 305.505 ms
meshed p50 128.521 ms p95 462.235 ms max 905.633 ms
At the harness walking speed of 6 m/s: p95 = 2.574 m, worst case = 5.434 m. A walking
player now outruns tile readiness by a couple of metres, not by a strate.
CAPABILITY PRESERVED, which is the point
The owner asked that speed never come from quietly simplifying the world. It did not:
geometry SHA-256 98eaad61..d3c4f is IDENTICAL to d38d4ca, 153,740 triangles, 82,516
vertices, across three runs. No resolution, sample-count, octave or seed reduction.
Unknown intervals still cost the skip. Prove and skip, never simplify and hope.
Export path, my own build, 3 runs: 1.088 / 1.102 / 1.100 s (was 1.171-1.258 s). ~9%,
below the 17% noise floor, so not claimed as a speed win -- the export was not the
target.
Honest remainder: LOD0 empties are ~7 ms p50 with a 25 ms tail, not the 2-5 ms of a
pure layout proof. Those are bounded, sound interval refinements that never invoke
meshing; going lower needs stronger proof bounds, not more skipping.
Verified by me: build 0 errors, binary timestamp checked against sources, peak RSS
measured, hashes compared. Nothing outside Plugins/VoxelForge; RMC untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
d38d4cabef
commit
412a7ad219
11 files changed
+4309
-687
No files matched your search
@@ -33,43 +33,8 @@
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01
|
||||
#include "VoxelStrateTypes.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FTask4BuildProbeScope
|
||||
{
|
||||
FChunkSDFCache& Cache;
|
||||
const float MinX;
|
||||
const float MinY;
|
||||
const float MaxX;
|
||||
const float MaxY;
|
||||
const uint64 StartCycles;
|
||||
const bool bEnabled;
|
||||
|
||||
FTask4BuildProbeScope(FChunkSDFCache& InCache, float InMinX, float InMinY,
|
||||
float InMaxX, float InMaxY)
|
||||
: Cache(InCache), MinX(InMinX), MinY(InMinY), MaxX(InMaxX), MaxY(InMaxY)
|
||||
, StartCycles(FPlatformTime::Cycles64())
|
||||
, bEnabled((InMaxX - InMinX) > 120.0f && (InMaxX - InMinX) < 220.0f
|
||||
&& (InMaxY - InMinY) > 120.0f && (InMaxY - InMinY) < 220.0f
|
||||
&& InMinX > -512.0f && InMaxX < 512.0f
|
||||
&& InMinY > -512.0f && InMaxY < 512.0f)
|
||||
{}
|
||||
|
||||
~FTask4BuildProbeScope()
|
||||
{
|
||||
if (bEnabled)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4BuildProbe] search=(%.0f,%.0f)-(%.0f,%.0f) rooms=%d joins=%d tunnels=%d pits=%d chimneys=%d ms=%.3f"),
|
||||
MinX, MinY, MaxX, MaxY, Cache.Rooms.Num(), Cache.RoomFloorJoins.Num(),
|
||||
Cache.Tunnels.Num(), Cache.Pits.Num(), Cache.Chimneys.Num(),
|
||||
FPlatformTime::ToMilliseconds64(
|
||||
FPlatformTime::Cycles64() - StartCycles));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
#include "VoxelNoise.h" // Pure FBM used by the slab landing query
|
||||
#include "VoxelPassageGeometry.h"
|
||||
@@ -109,6 +74,40 @@ namespace
|
||||
|
||||
}
|
||||
|
||||
void FChunkSDFCache::Reset()
|
||||
{
|
||||
// Keep the allocations: classifier windows can alternate between empty and
|
||||
// populated regions on adjacent requests.
|
||||
Rooms.Reset();
|
||||
RoomFloorJoins.Reset();
|
||||
Tunnels.Reset();
|
||||
Pits.Reset();
|
||||
Chimneys.Reset();
|
||||
Columns.Reset();
|
||||
SupportColumnMinX = 0;
|
||||
SupportColumnMinY = 0;
|
||||
SupportColumnCellsX = 0;
|
||||
SupportColumnCellsY = 0;
|
||||
SupportColumnEntries.Reset();
|
||||
SupportColumns.Reset();
|
||||
}
|
||||
|
||||
void FChunkSDFCache::Release()
|
||||
{
|
||||
Rooms.Empty();
|
||||
RoomFloorJoins.Empty();
|
||||
Tunnels.Empty();
|
||||
Pits.Empty();
|
||||
Chimneys.Empty();
|
||||
Columns.Empty();
|
||||
SupportColumnEntries.Empty();
|
||||
SupportColumns.Empty();
|
||||
SupportColumnMinX = 0;
|
||||
SupportColumnMinY = 0;
|
||||
SupportColumnCellsX = 0;
|
||||
SupportColumnCellsY = 0;
|
||||
}
|
||||
|
||||
SIZE_T FChunkSDFCache::GetAllocatedSize() const
|
||||
{
|
||||
return static_cast<SIZE_T>(GetAllocatedSizeBreakdown().TotalBytes());
|
||||
@@ -166,6 +165,473 @@ struct FBuildRoom
|
||||
bool bPlayerFitAttempted = false;
|
||||
};
|
||||
|
||||
struct FVFRoomGraphReach
|
||||
{
|
||||
float DirectRoomReach = 0.0f;
|
||||
float FloorReliefEnvelope = 0.0f;
|
||||
float BlendEnvelope = 0.0f;
|
||||
float MaxTunnelLength = 0.0f;
|
||||
float PairAabbReach = 0.0f;
|
||||
float PairZReach = 0.0f;
|
||||
float CollectMargin = 0.0f;
|
||||
};
|
||||
|
||||
static float VF_RoomShapeReachUpperBound(float RadiusXY, float RadiusZ)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
static bool VF_ComputeRoomGraphReach(
|
||||
const FStrateGenerationParams& Params,
|
||||
FVFRoomGraphReach& OutReach)
|
||||
{
|
||||
const float RelevantParams[] = {
|
||||
Params.RoomDensity,
|
||||
Params.RoomSpacing,
|
||||
Params.MinRoomRadius,
|
||||
Params.MaxRoomRadius,
|
||||
Params.RoomHeightRatio,
|
||||
Params.RoomFloorCutMin,
|
||||
Params.RoomFloorCutMax,
|
||||
Params.FloorReliefStrength,
|
||||
Params.FloorReliefFrequency,
|
||||
Params.OriginRoomRadius,
|
||||
Params.TunnelMinRadius,
|
||||
Params.TunnelMaxRadius,
|
||||
Params.TunnelDensity,
|
||||
Params.MaxTunnelLength,
|
||||
Params.TunnelWarpStrength,
|
||||
Params.TunnelHorizontalBias,
|
||||
Params.TunnelEndpointZOffset,
|
||||
Params.SDFBlendRadius,
|
||||
Params.CaveWarpStrength,
|
||||
Params.CaveWarpFrequency,
|
||||
Params.VerticalScale,
|
||||
Params.BoundarySealThickness,
|
||||
Params.StrateTopWorldZ,
|
||||
Params.StrateBottomWorldZ};
|
||||
for (const float Value : RelevantParams)
|
||||
{
|
||||
if (!FMath::IsFinite(Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Params.RoomSpacing <= 0.0f
|
||||
|| Params.RoomDensity < 0.0f
|
||||
|| Params.MinRoomRadius < 0.0f
|
||||
|| Params.MaxRoomRadius < 0.0f
|
||||
|| Params.RoomHeightRatio < 0.0f
|
||||
|| Params.OriginRoomRadius < 0.0f
|
||||
|| Params.TunnelMinRadius < 0.0f
|
||||
|| Params.TunnelMaxRadius < 0.0f
|
||||
|| Params.SDFBlendRadius < 0.0f
|
||||
|| FMath::Abs(Params.FloorReliefStrength) > 1000000.0f
|
||||
|| FMath::Abs(Params.CaveWarpStrength) > 1000000.0f
|
||||
|| FMath::Abs(Params.RoomFloorCutMin) > 1000000.0f
|
||||
|| FMath::Abs(Params.RoomFloorCutMax) > 1000000.0f
|
||||
|| FMath::Max(Params.MaxTunnelLength, 0.0f) > 1000000.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The cache's room centres and the world-space tunnel chain have separate
|
||||
// Z conventions when VerticalScale != 1. A proof that ignores that
|
||||
// conversion could under-estimate the chain length used for its lateral
|
||||
// wander. Let the normal cache path handle those authored variants.
|
||||
if (Params.VerticalScale > 0.0f
|
||||
&& FMath::Abs(Params.VerticalScale - 1.0f) > 1.0e-6f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const float RoomRadiusEnvelope = FMath::Max(
|
||||
FMath::Abs(Params.MinRoomRadius),
|
||||
FMath::Abs(Params.MaxRoomRadius));
|
||||
const float AllRoomRadiusEnvelope = FMath::Max(
|
||||
RoomRadiusEnvelope, FMath::Abs(Params.OriginRoomRadius));
|
||||
const float AllRoomHeightEnvelope = AllRoomRadiusEnvelope
|
||||
* FMath::Abs(Params.RoomHeightRatio);
|
||||
const float FloorReliefEnvelope = FMath::Abs(Params.FloorReliefStrength)
|
||||
* VOXEL_NOISE_SCALE * 1.5f;
|
||||
const float BlendEnvelope = FMath::Max(Params.SDFBlendRadius, 0.0f);
|
||||
const float TunnelRadiusEnvelope = FMath::Max(
|
||||
0.5f,
|
||||
FMath::Max(
|
||||
FMath::Abs(Params.TunnelMinRadius),
|
||||
FMath::Abs(Params.TunnelMaxRadius)) * 1.18f);
|
||||
const float MaxTunnelLength = FMath::Max(
|
||||
Params.MaxTunnelLength, 0.0f);
|
||||
const float MaxInfluence = FMath::Max(
|
||||
RoomRadiusEnvelope + FloorReliefEnvelope,
|
||||
FMath::Abs(Params.TunnelWarpStrength) + TunnelRadiusEnvelope)
|
||||
+ BlendEnvelope * 3.0f;
|
||||
const float DirectRoomReach = VF_RoomShapeReachUpperBound(
|
||||
AllRoomRadiusEnvelope, AllRoomHeightEnvelope)
|
||||
+ FloorReliefEnvelope + BlendEnvelope * 3.0f;
|
||||
|
||||
// A fitted tunnel mouth may move by one bounded cave-warp displacement in
|
||||
// each XY axis. The chain then wanders by at most 25.5% of its authored
|
||||
// length. Bound the endpoint's possible Z correction too, because that
|
||||
// length controls the lateral wander even though the preflight is XY-only.
|
||||
const float CaveWarpRaw = FMath::Abs(Params.CaveWarpStrength)
|
||||
* VOXEL_NOISE_SCALE;
|
||||
const float CaveWarpEnvelope = CaveWarpRaw * 1.5f;
|
||||
const float EndpointXY = FMath::Sqrt(2.0f)
|
||||
* CaveWarpEnvelope;
|
||||
const float FloorCutEnvelope = FMath::Max(
|
||||
1.0f,
|
||||
FMath::Max(
|
||||
FMath::Abs(Params.RoomFloorCutMin),
|
||||
FMath::Abs(Params.RoomFloorCutMax)));
|
||||
const float ReliefSearch = static_cast<float>(FMath::Clamp(
|
||||
FMath::CeilToInt(FMath::Abs(Params.FloorReliefStrength)
|
||||
* VOXEL_NOISE_SCALE + CaveWarpRaw) + 8,
|
||||
8,
|
||||
128));
|
||||
const float MaxStepHeight = FVoxelPlayerCapsuleConstants::MaxStepHeightMeters
|
||||
/ FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||
const float EndpointVertical = AllRoomHeightEnvelope * FloorCutEnvelope
|
||||
+ FloorReliefEnvelope + CaveWarpEnvelope + ReliefSearch
|
||||
+ MaxStepHeight + TunnelRadiusEnvelope + 2.0f;
|
||||
const float EndpointOffset = FMath::Sqrt(
|
||||
EndpointXY * EndpointXY
|
||||
+ EndpointVertical * EndpointVertical);
|
||||
const float TunnelLengthBound = MaxTunnelLength
|
||||
+ 2.0f * EndpointOffset;
|
||||
const float WorldFloorPad = FMath::Max(
|
||||
VoxelPassageGeometry::LandingFloorThicknessVoxels,
|
||||
BlendEnvelope);
|
||||
const float TunnelTubeReach = TunnelRadiusEnvelope
|
||||
+ WorldFloorPad + BlendEnvelope * 3.0f
|
||||
+ CaveWarpEnvelope + 2.0f;
|
||||
const float PairAabbReach = EndpointXY
|
||||
+ TunnelLengthBound * 0.255f
|
||||
+ FMath::Max(MaxInfluence, TunnelTubeReach);
|
||||
const float PairZReach = EndpointVertical + TunnelTubeReach;
|
||||
const float CollectMargin = 2.0f * MaxTunnelLength + MaxInfluence;
|
||||
|
||||
if (!FMath::IsFinite(DirectRoomReach)
|
||||
|| !FMath::IsFinite(PairAabbReach)
|
||||
|| !FMath::IsFinite(PairZReach)
|
||||
|| !FMath::IsFinite(CollectMargin)
|
||||
|| DirectRoomReach < 0.0f
|
||||
|| PairAabbReach < 0.0f
|
||||
|| PairZReach < 0.0f
|
||||
|| CollectMargin < 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutReach.DirectRoomReach = DirectRoomReach;
|
||||
OutReach.FloorReliefEnvelope = FloorReliefEnvelope;
|
||||
OutReach.BlendEnvelope = BlendEnvelope;
|
||||
OutReach.MaxTunnelLength = MaxTunnelLength;
|
||||
OutReach.PairAabbReach = PairAabbReach;
|
||||
OutReach.PairZReach = PairZReach;
|
||||
OutReach.CollectMargin = CollectMargin;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename AllocatorType>
|
||||
static bool VF_MayHaveRoomGraphFeature(
|
||||
const TArray<FBuildRoom, AllocatorType>& Rooms,
|
||||
float SearchMinX, float SearchMinY,
|
||||
float SearchMaxX, float SearchMaxY,
|
||||
const FStrateGenerationParams& Params,
|
||||
uint32 StrateSeed,
|
||||
const FVFRoomGraphReach& Reach,
|
||||
bool bUseZ = false,
|
||||
float SearchMinZ = 0.0f,
|
||||
float SearchMaxZ = 0.0f)
|
||||
{
|
||||
auto AabbDistanceSquared = [&](
|
||||
float X, float Y, float MinX, float MinY,
|
||||
float MaxX, float MaxY) -> float
|
||||
{
|
||||
const float DX = X < MinX ? MinX - X
|
||||
: (X > MaxX ? X - MaxX : 0.0f);
|
||||
const float DY = Y < MinY ? MinY - Y
|
||||
: (Y > MaxY ? Y - MaxY : 0.0f);
|
||||
return DX * DX + DY * DY;
|
||||
};
|
||||
|
||||
for (const FBuildRoom& Room : Rooms)
|
||||
{
|
||||
if (!FMath::IsFinite(Room.Center.X)
|
||||
|| !FMath::IsFinite(Room.Center.Y)
|
||||
|| !FMath::IsFinite(Room.Center.Z)
|
||||
|| !FMath::IsFinite(Room.RadiusXY)
|
||||
|| !FMath::IsFinite(Room.RadiusZ))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const float RoomReach = VF_RoomShapeReachUpperBound(
|
||||
Room.RadiusXY, Room.RadiusZ)
|
||||
+ Reach.FloorReliefEnvelope + Reach.BlendEnvelope * 3.0f;
|
||||
if (!FMath::IsFinite(RoomReach) || RoomReach < 0.0f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (bUseZ
|
||||
&& (static_cast<float>(Room.Center.Z) + RoomReach < SearchMinZ
|
||||
|| static_cast<float>(Room.Center.Z) - RoomReach > SearchMaxZ))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float DistanceSquared = AabbDistanceSquared(
|
||||
static_cast<float>(Room.Center.X),
|
||||
static_cast<float>(Room.Center.Y),
|
||||
SearchMinX - RoomReach,
|
||||
SearchMinY - RoomReach,
|
||||
SearchMaxX + RoomReach,
|
||||
SearchMaxY + RoomReach);
|
||||
if (!FMath::IsFinite(DistanceSquared))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (DistanceSquared <= KINDA_SMALL_NUMBER)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Reach.MaxTunnelLength <= 0.0f || Rooms.Num() < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// A pair whose broad centerline envelope can reach the query is enough to
|
||||
// keep the full graph path. This deliberately treats every geometrically
|
||||
// eligible pair as potential; the exact backbone/random decision remains
|
||||
// owned by BuildChunkCache. It keeps this preflight linear in the common
|
||||
// feature-present case and avoids player-fit work in the feature-free case.
|
||||
for (int32 First = 0; First < Rooms.Num(); ++First)
|
||||
{
|
||||
for (int32 Second = First + 1; Second < Rooms.Num(); ++Second)
|
||||
{
|
||||
const FVector& FirstCenter = Rooms[First].Center;
|
||||
const FVector& SecondCenter = Rooms[Second].Center;
|
||||
if (bUseZ
|
||||
&& (FMath::Max(
|
||||
static_cast<float>(FirstCenter.Z),
|
||||
static_cast<float>(SecondCenter.Z))
|
||||
+ Reach.PairZReach < SearchMinZ
|
||||
|| FMath::Min(
|
||||
static_cast<float>(FirstCenter.Z),
|
||||
static_cast<float>(SecondCenter.Z))
|
||||
- Reach.PairZReach > SearchMaxZ))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float EuclideanDistance = static_cast<float>(
|
||||
FVector::Dist(FirstCenter, SecondCenter));
|
||||
if (!FMath::IsFinite(EuclideanDistance))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (EuclideanDistance > Reach.MaxTunnelLength)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float PairMinX = FMath::Min(
|
||||
static_cast<float>(FirstCenter.X),
|
||||
static_cast<float>(SecondCenter.X))
|
||||
- Reach.PairAabbReach;
|
||||
const float PairMinY = FMath::Min(
|
||||
static_cast<float>(FirstCenter.Y),
|
||||
static_cast<float>(SecondCenter.Y))
|
||||
- Reach.PairAabbReach;
|
||||
const float PairMaxX = FMath::Max(
|
||||
static_cast<float>(FirstCenter.X),
|
||||
static_cast<float>(SecondCenter.X))
|
||||
+ Reach.PairAabbReach;
|
||||
const float PairMaxY = FMath::Max(
|
||||
static_cast<float>(FirstCenter.Y),
|
||||
static_cast<float>(SecondCenter.Y))
|
||||
+ Reach.PairAabbReach;
|
||||
if (!FMath::IsFinite(PairMinX) || !FMath::IsFinite(PairMinY)
|
||||
|| !FMath::IsFinite(PairMaxX) || !FMath::IsFinite(PairMaxY))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (PairMaxX >= SearchMinX && PairMaxY >= SearchMinY
|
||||
&& PairMinX <= SearchMaxX && PairMinY <= SearchMaxY)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
#if 0
|
||||
const float MaxTunnelLengthSquared =
|
||||
Reach.MaxTunnelLength * Reach.MaxTunnelLength;
|
||||
if (!FMath::IsFinite(MaxTunnelLengthSquared))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
TArray<int32, TInlineAllocator<128>> NearestNeighbor;
|
||||
NearestNeighbor.SetNumUninitialized(Rooms.Num());
|
||||
const bool bFlowToOrigin = Params.bTunnelsFlowTowardOrigin;
|
||||
auto OriginKeySquared = [&](int32 Index) -> float
|
||||
{
|
||||
const FVector& Center = Rooms[Index].Center;
|
||||
return static_cast<float>(
|
||||
Center.X * Center.X + Center.Y * Center.Y);
|
||||
};
|
||||
auto LinkMetric = [&](int32 First, int32 Second) -> float
|
||||
{
|
||||
const float Distance = static_cast<float>(
|
||||
FVector::Dist(Rooms[First].Center, Rooms[Second].Center));
|
||||
const float VerticalSeparation = FMath::Abs(
|
||||
static_cast<float>(
|
||||
Rooms[First].Center.Z - Rooms[Second].Center.Z));
|
||||
return Distance
|
||||
+ VerticalSeparation * Params.TunnelHorizontalBias * 5.0f;
|
||||
};
|
||||
auto PickNeighbor = [&](int32 Index) -> int32
|
||||
{
|
||||
const float MyKeySquared = OriginKeySquared(Index);
|
||||
float BestInward = FLT_MAX;
|
||||
int32 BestInwardIndex = -1;
|
||||
float BestAny = FLT_MAX;
|
||||
int32 BestAnyIndex = -1;
|
||||
for (int32 Other = 0; Other < Rooms.Num(); ++Other)
|
||||
{
|
||||
if (Other == Index)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float DistanceSquared = static_cast<float>(
|
||||
FVector::DistSquared(
|
||||
Rooms[Index].Center, Rooms[Other].Center));
|
||||
if (!FMath::IsFinite(DistanceSquared))
|
||||
{
|
||||
return -2;
|
||||
}
|
||||
if (DistanceSquared > MaxTunnelLengthSquared)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float Metric = LinkMetric(Index, Other);
|
||||
if (!FMath::IsFinite(Metric))
|
||||
{
|
||||
return -2;
|
||||
}
|
||||
if (Metric < BestAny)
|
||||
{
|
||||
BestAny = Metric;
|
||||
BestAnyIndex = Other;
|
||||
}
|
||||
if (bFlowToOrigin
|
||||
&& OriginKeySquared(Other) < MyKeySquared
|
||||
&& Metric < BestInward)
|
||||
{
|
||||
BestInward = Metric;
|
||||
BestInwardIndex = Other;
|
||||
}
|
||||
}
|
||||
return (bFlowToOrigin && BestInwardIndex != -1)
|
||||
? BestInwardIndex : BestAnyIndex;
|
||||
};
|
||||
for (int32 Index = 0; Index < Rooms.Num(); ++Index)
|
||||
{
|
||||
NearestNeighbor[Index] = PickNeighbor(Index);
|
||||
if (NearestNeighbor[Index] == -2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int32 First = 0; First < Rooms.Num(); ++First)
|
||||
{
|
||||
for (int32 Second = First + 1; Second < Rooms.Num(); ++Second)
|
||||
{
|
||||
const float EuclideanDistance = static_cast<float>(
|
||||
FVector::Dist(Rooms[First].Center, Rooms[Second].Center));
|
||||
if (!FMath::IsFinite(EuclideanDistance))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool bBackbone = NearestNeighbor[First] == Second
|
||||
|| NearestNeighbor[Second] == First;
|
||||
float CheckDistance = EuclideanDistance;
|
||||
if (!bBackbone && Params.TunnelHorizontalBias > 0.0f)
|
||||
{
|
||||
const float VerticalSeparation = FMath::Abs(
|
||||
static_cast<float>(
|
||||
Rooms[First].Center.Z - Rooms[Second].Center.Z));
|
||||
CheckDistance += VerticalSeparation
|
||||
* Params.TunnelHorizontalBias * 5.0f;
|
||||
}
|
||||
if (!FMath::IsFinite(CheckDistance)
|
||||
|| CheckDistance > Reach.MaxTunnelLength)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!bBackbone)
|
||||
{
|
||||
if (Params.TunnelDensity <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const uint32 PairHash = VoxelHash::Pair(
|
||||
Rooms[First].CellX, Rooms[First].CellY,
|
||||
Rooms[Second].CellX, Rooms[Second].CellY,
|
||||
StrateSeed);
|
||||
if (VoxelHash::ToFloat01(PairHash) >= Params.TunnelDensity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const float PairMinX = FMath::Min(
|
||||
static_cast<float>(Rooms[First].Center.X),
|
||||
static_cast<float>(Rooms[Second].Center.X))
|
||||
- Reach.PairAabbReach;
|
||||
const float PairMinY = FMath::Min(
|
||||
static_cast<float>(Rooms[First].Center.Y),
|
||||
static_cast<float>(Rooms[Second].Center.Y))
|
||||
- Reach.PairAabbReach;
|
||||
const float PairMaxX = FMath::Max(
|
||||
static_cast<float>(Rooms[First].Center.X),
|
||||
static_cast<float>(Rooms[Second].Center.X))
|
||||
+ Reach.PairAabbReach;
|
||||
const float PairMaxY = FMath::Max(
|
||||
static_cast<float>(Rooms[First].Center.Y),
|
||||
static_cast<float>(Rooms[Second].Center.Y))
|
||||
+ Reach.PairAabbReach;
|
||||
if (!FMath::IsFinite(PairMinX) || !FMath::IsFinite(PairMinY)
|
||||
|| !FMath::IsFinite(PairMaxX) || !FMath::IsFinite(PairMaxY))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (PairMaxX >= SearchMinX && PairMaxY >= SearchMinY
|
||||
&& PairMinX <= SearchMaxX && PairMinY <= SearchMaxY)
|
||||
{
|
||||
// A backbone link is considered potential even if the origin
|
||||
// connection cap later downgrades it. That only sacrifices a
|
||||
// skip; it can never turn a real tunnel into an empty proof.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// INTERNAL: Shared hash-placement skeleton for the per-room baked features
|
||||
// (pits / chimneys / columns). Squelette commun de placement par hash — les
|
||||
@@ -2848,6 +3314,175 @@ bool VF_SuggestLandingPoint(
|
||||
}
|
||||
}
|
||||
|
||||
bool VoxelCaveMorphology::MayHaveFeatureInSearchBox(
|
||||
float SearchMinX, float SearchMinY,
|
||||
float SearchMaxX, float SearchMaxY,
|
||||
const FStrateGenerationParams& Params,
|
||||
uint32 Seed, int32 StrateIndex,
|
||||
bool bUseZ, float SearchMinZ, float SearchMaxZ)
|
||||
{
|
||||
if (!FMath::IsFinite(SearchMinX) || !FMath::IsFinite(SearchMinY)
|
||||
|| !FMath::IsFinite(SearchMaxX) || !FMath::IsFinite(SearchMaxY)
|
||||
|| SearchMinX > SearchMaxX || SearchMinY > SearchMaxY)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (bUseZ
|
||||
&& (!FMath::IsFinite(SearchMinZ) || !FMath::IsFinite(SearchMaxZ)
|
||||
|| SearchMinZ > SearchMaxZ))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (Params.RoomSpacing <= 0.0f)
|
||||
{
|
||||
// BuildChunkCache returns an empty cache before creating the origin
|
||||
// room when spacing is non-positive.
|
||||
return false;
|
||||
}
|
||||
|
||||
FVFRoomGraphReach Reach;
|
||||
if (!VF_ComputeRoomGraphReach(Params, Reach))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const float CollectMinX = SearchMinX - Reach.CollectMargin;
|
||||
const float CollectMinY = SearchMinY - Reach.CollectMargin;
|
||||
const float CollectMaxX = SearchMaxX + Reach.CollectMargin;
|
||||
const float CollectMaxY = SearchMaxY + Reach.CollectMargin;
|
||||
if (!FMath::IsFinite(CollectMinX) || !FMath::IsFinite(CollectMinY)
|
||||
|| !FMath::IsFinite(CollectMaxX) || !FMath::IsFinite(CollectMaxY))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
auto FloorCell = [](double Value, double CellSize, int32& OutCell) -> bool
|
||||
{
|
||||
const double Cell = FMath::FloorToDouble(Value / CellSize);
|
||||
if (!FMath::IsFinite(Cell)
|
||||
|| Cell < static_cast<double>(MIN_int32) + 2.0
|
||||
|| Cell > static_cast<double>(MAX_int32) - 2.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutCell = static_cast<int32>(Cell);
|
||||
return true;
|
||||
};
|
||||
int32 CellMinX = 0;
|
||||
int32 CellMaxX = 0;
|
||||
int32 CellMinY = 0;
|
||||
int32 CellMaxY = 0;
|
||||
if (!FloorCell(
|
||||
static_cast<double>(CollectMinX),
|
||||
static_cast<double>(Params.RoomSpacing), CellMinX)
|
||||
|| !FloorCell(
|
||||
static_cast<double>(CollectMaxX),
|
||||
static_cast<double>(Params.RoomSpacing), CellMaxX)
|
||||
|| !FloorCell(
|
||||
static_cast<double>(CollectMinY),
|
||||
static_cast<double>(Params.RoomSpacing), CellMinY)
|
||||
|| !FloorCell(
|
||||
static_cast<double>(CollectMaxY),
|
||||
static_cast<double>(Params.RoomSpacing), CellMaxY))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const int64 SpanX = static_cast<int64>(CellMaxX)
|
||||
- static_cast<int64>(CellMinX) + 1;
|
||||
const int64 SpanY = static_cast<int64>(CellMaxY)
|
||||
- static_cast<int64>(CellMinY) + 1;
|
||||
// This is deliberately a bounded proof pass. If an authored asset asks
|
||||
// BuildChunkCache to hash a pathological number of cells, keep the old
|
||||
// conservative path instead of making classification unbounded.
|
||||
if (SpanX <= 0 || SpanY <= 0 || SpanX > 4096 || SpanY > 4096
|
||||
|| SpanX > 262144 / FMath::Max<int64>(SpanY, 1))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const float RoomRadiusEnvelope = FMath::Max(
|
||||
FMath::Abs(Params.MinRoomRadius),
|
||||
FMath::Abs(Params.MaxRoomRadius));
|
||||
const float RoomZBuffer = RoomRadiusEnvelope * Params.RoomHeightRatio;
|
||||
const float StrateMinZ = Params.StrateBottomWorldZ
|
||||
+ Params.BoundarySealThickness + RoomZBuffer;
|
||||
const float StrateMaxZ = Params.StrateTopWorldZ
|
||||
- Params.BoundarySealThickness - RoomZBuffer;
|
||||
const float StrateRangeZ = StrateMaxZ - StrateMinZ;
|
||||
const float StrateCenterZ = (StrateMinZ + StrateMaxZ) * 0.5f;
|
||||
if (!FMath::IsFinite(StrateMinZ) || !FMath::IsFinite(StrateMaxZ)
|
||||
|| !FMath::IsFinite(StrateRangeZ)
|
||||
|| !FMath::IsFinite(StrateCenterZ))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
TArray<FBuildRoom, TInlineAllocator<128>> Rooms;
|
||||
if (Params.OriginRoomRadius > 0.0f)
|
||||
{
|
||||
FBuildRoom OriginRoom{};
|
||||
OriginRoom.Center = FVector(0.0f, 0.0f, StrateCenterZ);
|
||||
OriginRoom.RadiusXY = Params.OriginRoomRadius;
|
||||
OriginRoom.RadiusZ = Params.OriginRoomRadius
|
||||
* Params.RoomHeightRatio;
|
||||
OriginRoom.CellX = INT32_MAX;
|
||||
OriginRoom.CellY = INT32_MAX;
|
||||
OriginRoom.Hash = VoxelHash::Cell(
|
||||
0, 0, VoxelCaveMorphology::MakeStrateSeed(Seed, StrateIndex)
|
||||
^ 0x0A161Cu);
|
||||
OriginRoom.bIsOrigin = true;
|
||||
OriginRoom.bStore = true;
|
||||
Rooms.Add(OriginRoom);
|
||||
}
|
||||
|
||||
const uint32 StrateSeed = VoxelCaveMorphology::MakeStrateSeed(
|
||||
Seed, StrateIndex);
|
||||
if (Params.RoomDensity > 0.0f)
|
||||
{
|
||||
for (int32 CY = CellMinY; CY <= CellMaxY; ++CY)
|
||||
{
|
||||
for (int32 CX = CellMinX; CX <= CellMaxX; ++CX)
|
||||
{
|
||||
const uint32 CellHash = VoxelHash::Cell(
|
||||
CX, CY, StrateSeed);
|
||||
if (VoxelHash::ToFloat01(CellHash) >= Params.RoomDensity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float JitterX = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(CellHash ^ 0x12345678u));
|
||||
const float JitterY = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(CellHash ^ 0x9ABCDEF0u));
|
||||
const float JitterZ = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(CellHash ^ 0x55AA55AAu));
|
||||
FBuildRoom Room{};
|
||||
Room.CellX = CX;
|
||||
Room.CellY = CY;
|
||||
Room.Hash = CellHash;
|
||||
Room.Center.X = (static_cast<float>(CX) + 0.15f
|
||||
+ JitterX * 0.7f) * Params.RoomSpacing;
|
||||
Room.Center.Y = (static_cast<float>(CY) + 0.15f
|
||||
+ JitterY * 0.7f) * Params.RoomSpacing;
|
||||
Room.Center.Z = StrateMinZ + JitterZ
|
||||
* FMath::Max(StrateRangeZ, 1.0f);
|
||||
const float SizeFactor = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(CellHash ^ 0xFEDCBA98u));
|
||||
Room.RadiusXY = FMath::Lerp(
|
||||
Params.MinRoomRadius, Params.MaxRoomRadius, SizeFactor);
|
||||
Room.RadiusZ = Room.RadiusXY * Params.RoomHeightRatio;
|
||||
Room.bStore = true;
|
||||
Rooms.Add(Room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return VF_MayHaveRoomGraphFeature(
|
||||
Rooms, SearchMinX, SearchMinY, SearchMaxX, SearchMaxY,
|
||||
Params, StrateSeed, Reach, bUseZ, SearchMinZ, SearchMaxZ);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// PHASE 1: BUILD CHUNK CACHE
|
||||
//=============================================================================
|
||||
@@ -2866,21 +3501,8 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
uint32 Seed, int32 StrateIndex,
|
||||
const TArray<FStrateTerrainOpEntry>* TerrainOps)
|
||||
{
|
||||
FTask4BuildProbeScope Task4BuildProbe(OutCache, SearchMinX, SearchMinY, SearchMaxX, SearchMaxY);
|
||||
|
||||
// Clear previous data (arrays keep their allocation for reuse)
|
||||
OutCache.Rooms.Reset();
|
||||
OutCache.RoomFloorJoins.Reset();
|
||||
OutCache.Tunnels.Reset();
|
||||
OutCache.Pits.Reset();
|
||||
OutCache.Chimneys.Reset();
|
||||
OutCache.Columns.Reset();
|
||||
OutCache.SupportColumnMinX = 0;
|
||||
OutCache.SupportColumnMinY = 0;
|
||||
OutCache.SupportColumnCellsX = 0;
|
||||
OutCache.SupportColumnCellsY = 0;
|
||||
OutCache.SupportColumnEntries.Reset();
|
||||
OutCache.SupportColumns.Reset();
|
||||
OutCache.Reset();
|
||||
|
||||
// Combine world seed with strate index so each strate gets unique caves
|
||||
const uint32 StrateSeed = VoxelCaveMorphology::MakeStrateSeed(Seed, StrateIndex);
|
||||
@@ -2970,23 +3592,9 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
// 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 ShapeReach = RoomShapeReachUpperBound(RadiusXY, RadiusZ);
|
||||
const float ShapeReach = VF_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));
|
||||
@@ -3002,6 +3610,60 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
return (dx * dx + dy * dy) <= RSq;
|
||||
};
|
||||
|
||||
// A connected edge can be discarded before the expensive player-fit bake when even a
|
||||
// conservative AABB around its entire possible chain misses the search window. The endpoint
|
||||
// fit stays inside 75% of its room radius; each cave-warped endpoint/control point can move by
|
||||
// at most two proven warp envelopes (fit inversion plus the final map); the interior wander is
|
||||
// bounded by TunnelWarpStrength. This is only a reject gate: non-finite inputs retain the old
|
||||
// full build, and a touching envelope still takes the exact path below.
|
||||
const float CaveWarpBound = FMath::Abs(Params.CaveWarpStrength)
|
||||
* VOXEL_NOISE_SCALE * 1.5f;
|
||||
auto PossibleTunnelTouchesSearchXY = [&](const FBuildRoom& RoomA,
|
||||
const FBuildRoom& RoomB) -> bool
|
||||
{
|
||||
const float RadiusA = FMath::Abs(RoomA.RadiusXY);
|
||||
const float RadiusB = FMath::Abs(RoomB.RadiusXY);
|
||||
const float RoomEndpointShift = FMath::Max(
|
||||
2.0f, 0.75f * FMath::Max(RadiusA, RadiusB)) + CaveWarpBound;
|
||||
const float TunnelRadius = FMath::Max(
|
||||
0.5f,
|
||||
FMath::Max(FMath::Abs(Params.TunnelMinRadius),
|
||||
FMath::Abs(Params.TunnelMaxRadius)) * 1.18f);
|
||||
const float ChainReach = RoomEndpointShift
|
||||
+ FMath::Abs(Params.TunnelWarpStrength)
|
||||
+ TunnelRadius
|
||||
+ FMath::Max(
|
||||
FMath::Max(Params.SDFBlendRadius, 0.0f) * 3.0f,
|
||||
VoxelPassageGeometry::LandingFloorThicknessVoxels);
|
||||
if (!FMath::IsFinite(CaveWarpBound)
|
||||
|| !FMath::IsFinite(ChainReach)
|
||||
|| ChainReach < 0.0f
|
||||
|| !FMath::IsFinite((float)RoomA.Center.X)
|
||||
|| !FMath::IsFinite((float)RoomA.Center.Y)
|
||||
|| !FMath::IsFinite((float)RoomB.Center.X)
|
||||
|| !FMath::IsFinite((float)RoomB.Center.Y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const float MinX = FMath::Min(
|
||||
static_cast<float>(RoomA.Center.X), static_cast<float>(RoomB.Center.X))
|
||||
- ChainReach;
|
||||
const float MinY = FMath::Min(
|
||||
static_cast<float>(RoomA.Center.Y), static_cast<float>(RoomB.Center.Y))
|
||||
- ChainReach;
|
||||
const float MaxX = FMath::Max(
|
||||
static_cast<float>(RoomA.Center.X), static_cast<float>(RoomB.Center.X))
|
||||
+ ChainReach;
|
||||
const float MaxY = FMath::Max(
|
||||
static_cast<float>(RoomA.Center.Y), static_cast<float>(RoomB.Center.Y))
|
||||
+ ChainReach;
|
||||
return FMath::IsFinite(MinX) && FMath::IsFinite(MinY)
|
||||
&& FMath::IsFinite(MaxX) && FMath::IsFinite(MaxY)
|
||||
&& MaxX >= SearchMinX && MaxY >= SearchMinY
|
||||
&& MinX <= SearchMaxX && MinY <= SearchMaxY;
|
||||
};
|
||||
|
||||
// Temporary array with cell coordinates for tunnel connection decisions
|
||||
TArray<FBuildRoom, TInlineAllocator<64>> BuildRooms;
|
||||
|
||||
@@ -3071,6 +3733,21 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
}
|
||||
}
|
||||
|
||||
// The collect window is intentionally much wider than the stored window:
|
||||
// it is needed to make the nearest-neighbor graph seam-invariant. Prove
|
||||
// the genuinely feature-free case before the O(N^2) graph and player-fit
|
||||
// bake. The helper repeats only the deterministic graph decisions that can
|
||||
// affect whether a room/tunnel bound reaches this search window.
|
||||
FVFRoomGraphReach FeatureReach;
|
||||
if (VF_ComputeRoomGraphReach(Params, FeatureReach)
|
||||
&& !VF_MayHaveRoomGraphFeature(
|
||||
BuildRooms, SearchMinX, SearchMinY, SearchMaxX, SearchMaxY,
|
||||
Params, StrateSeed, FeatureReach))
|
||||
{
|
||||
VF_FinalizeCacheBroadPhaseBounds(OutCache, BlendK);
|
||||
return;
|
||||
}
|
||||
|
||||
const int32 NumRooms = BuildRooms.Num();
|
||||
if (NumRooms == 0)
|
||||
{
|
||||
@@ -3298,6 +3975,14 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
RoomConnected[I] = true;
|
||||
RoomConnected[J] = true;
|
||||
|
||||
if (!PossibleTunnelTouchesSearchXY(RoomA, RoomB))
|
||||
{
|
||||
// The graph decision is retained above; only the search-window geometry is
|
||||
// omitted. This preserves connectivity, joins, and all neighbouring-window
|
||||
// decisions while avoiding player-fit work for edges that cannot be stored.
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- TUNNEL HASH (for deriving all tunnel properties) ---
|
||||
const uint32 TunnelHash = VoxelHash::Pair(
|
||||
RoomA.CellX, RoomA.CellY,
|
||||
@@ -3343,8 +4028,9 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
FVector WorldEndB = VF_UnwarpCavePoint(EndB, Params, Seed);
|
||||
if (!RoomA.bIsOrigin)
|
||||
{
|
||||
if (ResolvePlayerFitPoint(
|
||||
BuildRooms[I], Params, Seed))
|
||||
const bool bHasPlayerFit = ResolvePlayerFitPoint(
|
||||
BuildRooms[I], Params, Seed);
|
||||
if (bHasPlayerFit)
|
||||
{
|
||||
WorldEndA = FVector(
|
||||
RoomA.PlayerFitPoint.X,
|
||||
@@ -3355,8 +4041,9 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
}
|
||||
if (!RoomB.bIsOrigin)
|
||||
{
|
||||
if (ResolvePlayerFitPoint(
|
||||
BuildRooms[J], Params, Seed))
|
||||
const bool bHasPlayerFit = ResolvePlayerFitPoint(
|
||||
BuildRooms[J], Params, Seed);
|
||||
if (bHasPlayerFit)
|
||||
{
|
||||
WorldEndB = FVector(
|
||||
RoomB.PlayerFitPoint.X,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -19,6 +19,7 @@
|
||||
#include "VoxelStats.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "VoxelStrateComposer.h"
|
||||
@@ -3506,76 +3507,218 @@ static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack,
|
||||
const FBox& VoxelBox,
|
||||
const FVoxelOpContext& Context,
|
||||
const UVoxelGenerator* Generator,
|
||||
const UVoxelStrateManager* StructuralManager)
|
||||
const UVoxelStrateManager* StructuralManager,
|
||||
float StructuralBaseDensity = 8.0f,
|
||||
FVoxelTileClassificationStats* Stats = nullptr)
|
||||
{
|
||||
// The ordinary proof stays shallow. The finite-domain warp retry is paid only by a
|
||||
// tile that already failed the cheap proof, and can afford one more split level because
|
||||
// its purpose is to discharge the LOD0 false-Mixed cases without sampling the field.
|
||||
const int32 MaxRefinementDepth = Context.bTightenWarpProof ? 5 : 2;
|
||||
const bool bTask4TimingProbe = Context.bUseLatticeProof
|
||||
&& Context.LatticeOriginVoxels == FIntVector(32, 0, -32)
|
||||
&& Context.Step == 1;
|
||||
int32 RefineNodeCount = 0;
|
||||
int32 ExactLeafCount = 0;
|
||||
int32 ExactSampleCount = 0;
|
||||
uint64 StackClassifyCycles = 0;
|
||||
uint64 ExactSampleCycles = 0;
|
||||
const bool bTask4ExactProbe = Context.bUseLatticeProof
|
||||
&& (Context.LatticeOriginVoxels == FIntVector(-64, -64, -64)
|
||||
|| Context.LatticeOriginVoxels == FIntVector(-64, -96, -64))
|
||||
&& Context.Step == 1;
|
||||
auto ClassifyExactLatticeLeaf = [&](const FBox& Box, bool bUseFinalField) -> EVoxelTileClass
|
||||
{
|
||||
if (!Context.bUseLatticeProof || Context.Step <= 0)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
|
||||
// Exact certificates are still the mesher's lattice, never a reduced-resolution shortcut.
|
||||
// Keep the samples local to this one classifier call. The old recursive fallback evaluated
|
||||
// the same shared lattice vertices once per depth-3 leaf (up to 64,000 calls for one LOD0
|
||||
// tile); besides being wasteful, that made an empty tile look like a brute-force generation.
|
||||
// A byte validity plane is intentional: it is transient, bounded by one tile, and must not
|
||||
// become another process-wide cache.
|
||||
int32 ExactCacheX0 = 0, ExactCacheY0 = 0, ExactCacheZ0 = 0;
|
||||
int32 ExactCacheNX = 0, ExactCacheNY = 0, ExactCacheNZ = 0;
|
||||
TArray<float> ExactCoreValues;
|
||||
TArray<float> ExactFinalValues;
|
||||
TArray<uint8> ExactCoreValid;
|
||||
TArray<uint8> ExactFinalValid;
|
||||
// A negative structural result is monotone over nested boxes. Compute it once for the root
|
||||
// lattice and carry that absence through the refinement tree; otherwise every AllSolid child
|
||||
// repeats the same 33³ post-air scan even though a false root result already excludes all of
|
||||
// its descendants. A positive result is not cached as absent and keeps the old conservative
|
||||
// per-child checks.
|
||||
bool bRootPassageAirKnownAbsent = false;
|
||||
bool bRootOriginAirKnownAbsent = false;
|
||||
const bool bUseExactCache = Context.bUseLatticeProof && Context.Step > 0
|
||||
&& [&]()
|
||||
{
|
||||
const float Step = static_cast<float>(Context.Step);
|
||||
const FVector Origin(static_cast<float>(Context.LatticeOriginVoxels.X),
|
||||
static_cast<float>(Context.LatticeOriginVoxels.Y),
|
||||
static_cast<float>(Context.LatticeOriginVoxels.Z));
|
||||
const int32 IX0 = FMath::CeilToInt((static_cast<float>(Box.Min.X) - Origin.X) / Step - 1.0e-4f);
|
||||
const int32 IY0 = FMath::CeilToInt((static_cast<float>(Box.Min.Y) - Origin.Y) / Step - 1.0e-4f);
|
||||
const int32 IZ0 = FMath::CeilToInt((static_cast<float>(Box.Min.Z) - Origin.Z) / Step - 1.0e-4f);
|
||||
const int32 IX1 = FMath::FloorToInt((static_cast<float>(Box.Max.X) - Origin.X) / Step + 1.0e-4f);
|
||||
const int32 IY1 = FMath::FloorToInt((static_cast<float>(Box.Max.Y) - Origin.Y) / Step + 1.0e-4f);
|
||||
const int32 IZ1 = FMath::FloorToInt((static_cast<float>(Box.Max.Z) - Origin.Z) / Step + 1.0e-4f);
|
||||
const int64 Count = ((int64)IX1 - IX0 + 1)
|
||||
* ((int64)IY1 - IY0 + 1)
|
||||
* ((int64)IZ1 - IZ0 + 1);
|
||||
if (IX1 < IX0 || IY1 < IY0 || IZ1 < IZ0 || Count <= 0 || Count > 256)
|
||||
ExactCacheX0 = FMath::CeilToInt((static_cast<float>(VoxelBox.Min.X) - Origin.X)
|
||||
/ Step - 1.0e-4f);
|
||||
ExactCacheY0 = FMath::CeilToInt((static_cast<float>(VoxelBox.Min.Y) - Origin.Y)
|
||||
/ Step - 1.0e-4f);
|
||||
ExactCacheZ0 = FMath::CeilToInt((static_cast<float>(VoxelBox.Min.Z) - Origin.Z)
|
||||
/ Step - 1.0e-4f);
|
||||
const int32 X1 = FMath::FloorToInt((static_cast<float>(VoxelBox.Max.X) - Origin.X)
|
||||
/ Step + 1.0e-4f);
|
||||
const int32 Y1 = FMath::FloorToInt((static_cast<float>(VoxelBox.Max.Y) - Origin.Y)
|
||||
/ Step + 1.0e-4f);
|
||||
const int32 Z1 = FMath::FloorToInt((static_cast<float>(VoxelBox.Max.Z) - Origin.Z)
|
||||
/ Step + 1.0e-4f);
|
||||
ExactCacheNX = X1 - ExactCacheX0 + 1;
|
||||
ExactCacheNY = Y1 - ExactCacheY0 + 1;
|
||||
ExactCacheNZ = Z1 - ExactCacheZ0 + 1;
|
||||
const int64 Count = static_cast<int64>(ExactCacheNX)
|
||||
* static_cast<int64>(ExactCacheNY)
|
||||
* static_cast<int64>(ExactCacheNZ);
|
||||
if (ExactCacheNX <= 0 || ExactCacheNY <= 0 || ExactCacheNZ <= 0
|
||||
|| Count <= 0 || Count > 100000)
|
||||
{
|
||||
ExactCacheNX = ExactCacheNY = ExactCacheNZ = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 NumVertices = static_cast<int32>(Count);
|
||||
ExactCoreValues.SetNumUninitialized(NumVertices);
|
||||
ExactFinalValues.SetNumUninitialized(NumVertices);
|
||||
ExactCoreValid.Init(0, NumVertices);
|
||||
ExactFinalValid.Init(0, NumVertices);
|
||||
return true;
|
||||
}();
|
||||
|
||||
auto GetLatticeBounds = [&](const FBox& Box,
|
||||
int32& OutX0, int32& OutY0, int32& OutZ0,
|
||||
int32& OutX1, int32& OutY1, int32& OutZ1,
|
||||
int64& OutCount) -> bool
|
||||
{
|
||||
if (!Context.bUseLatticeProof || Context.Step <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const float Step = static_cast<float>(Context.Step);
|
||||
const FVector Origin(static_cast<float>(Context.LatticeOriginVoxels.X),
|
||||
static_cast<float>(Context.LatticeOriginVoxels.Y),
|
||||
static_cast<float>(Context.LatticeOriginVoxels.Z));
|
||||
OutX0 = FMath::CeilToInt((static_cast<float>(Box.Min.X) - Origin.X)
|
||||
/ Step - 1.0e-4f);
|
||||
OutY0 = FMath::CeilToInt((static_cast<float>(Box.Min.Y) - Origin.Y)
|
||||
/ Step - 1.0e-4f);
|
||||
OutZ0 = FMath::CeilToInt((static_cast<float>(Box.Min.Z) - Origin.Z)
|
||||
/ Step - 1.0e-4f);
|
||||
OutX1 = FMath::FloorToInt((static_cast<float>(Box.Max.X) - Origin.X)
|
||||
/ Step + 1.0e-4f);
|
||||
OutY1 = FMath::FloorToInt((static_cast<float>(Box.Max.Y) - Origin.Y)
|
||||
/ Step + 1.0e-4f);
|
||||
OutZ1 = FMath::FloorToInt((static_cast<float>(Box.Max.Z) - Origin.Z)
|
||||
/ Step + 1.0e-4f);
|
||||
OutCount = static_cast<int64>(OutX1 - OutX0 + 1)
|
||||
* static_cast<int64>(OutY1 - OutY0 + 1)
|
||||
* static_cast<int64>(OutZ1 - OutZ0 + 1);
|
||||
return OutX1 >= OutX0 && OutY1 >= OutY0 && OutZ1 >= OutZ0 && OutCount > 0;
|
||||
};
|
||||
|
||||
auto EvaluateExactLatticeValue = [&](int32 IX, int32 IY, int32 IZ,
|
||||
bool bUseFinalField) -> float
|
||||
{
|
||||
const float Step = static_cast<float>(Context.Step);
|
||||
const FVector Origin(static_cast<float>(Context.LatticeOriginVoxels.X),
|
||||
static_cast<float>(Context.LatticeOriginVoxels.Y),
|
||||
static_cast<float>(Context.LatticeOriginVoxels.Z));
|
||||
TArray<float>& Values = bUseFinalField ? ExactFinalValues : ExactCoreValues;
|
||||
TArray<uint8>& Valid = bUseFinalField ? ExactFinalValid : ExactCoreValid;
|
||||
int32 CacheIndex = INDEX_NONE;
|
||||
if (bUseExactCache
|
||||
&& IX >= ExactCacheX0 && IX < ExactCacheX0 + ExactCacheNX
|
||||
&& IY >= ExactCacheY0 && IY < ExactCacheY0 + ExactCacheNY
|
||||
&& IZ >= ExactCacheZ0 && IZ < ExactCacheZ0 + ExactCacheNZ)
|
||||
{
|
||||
CacheIndex = (IZ - ExactCacheZ0) * ExactCacheNY * ExactCacheNX
|
||||
+ (IY - ExactCacheY0) * ExactCacheNX
|
||||
+ (IX - ExactCacheX0);
|
||||
if (Valid[CacheIndex] != 0)
|
||||
{
|
||||
if (Stats)
|
||||
{
|
||||
if (bUseFinalField) { ++Stats->ExactFinalCacheHits; }
|
||||
else { ++Stats->ExactCoreCacheHits; }
|
||||
}
|
||||
return Values[CacheIndex];
|
||||
}
|
||||
}
|
||||
|
||||
const float WorldX = Origin.X + static_cast<float>(IX) * Step;
|
||||
const float WorldY = Origin.Y + static_cast<float>(IY) * Step;
|
||||
const float WorldZ = Origin.Z + static_cast<float>(IZ) * Step;
|
||||
// The operator stack is the cheap proof field. A passage/landing post is applied after
|
||||
// that stack by GetDensityAt; for a child intersecting one of those modifiers, certify
|
||||
// the final MC-facing field rather than trusting the conservative core proof.
|
||||
const float Density = bUseFinalField
|
||||
? -Generator->GetDensityAt(WorldX, WorldY, WorldZ)
|
||||
: Stack.EvalInternal(WorldX, WorldY, WorldZ);
|
||||
if (Stats)
|
||||
{
|
||||
if (bUseFinalField) { ++Stats->ExactFinalSamples; }
|
||||
else { ++Stats->ExactCoreSamples; }
|
||||
}
|
||||
if (CacheIndex != INDEX_NONE)
|
||||
{
|
||||
Values[CacheIndex] = Density;
|
||||
Valid[CacheIndex] = 1;
|
||||
}
|
||||
return Density;
|
||||
};
|
||||
|
||||
auto CornersHaveOneSign = [&](const FBox& Box, bool bUseFinalField) -> bool
|
||||
{
|
||||
int32 IX0 = 0, IY0 = 0, IZ0 = 0, IX1 = 0, IY1 = 0, IZ1 = 0;
|
||||
int64 Count = 0;
|
||||
if (!GetLatticeBounds(Box, IX0, IY0, IZ0, IX1, IY1, IZ1, Count))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool bHaveSign = false;
|
||||
bool bPositive = false;
|
||||
const int32 Xs[2] = { IX0, IX1 };
|
||||
const int32 Ys[2] = { IY0, IY1 };
|
||||
const int32 Zs[2] = { IZ0, IZ1 };
|
||||
for (const int32 IZ : Zs)
|
||||
for (const int32 IY : Ys)
|
||||
for (const int32 IX : Xs)
|
||||
{
|
||||
const float Density = EvaluateExactLatticeValue(IX, IY, IZ, bUseFinalField);
|
||||
if (!FMath::IsFinite(Density) || Density == 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const bool bThisPositive = Density > 0.0f;
|
||||
if (!bHaveSign)
|
||||
{
|
||||
bHaveSign = true;
|
||||
bPositive = bThisPositive;
|
||||
}
|
||||
else if (bPositive != bThisPositive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return bHaveSign;
|
||||
};
|
||||
|
||||
auto ClassifyExactLatticeLeaf = [&](const FBox& Box, bool bUseFinalField) -> EVoxelTileClass
|
||||
{
|
||||
const uint64 ExactStartCycles = Stats ? FPlatformTime::Cycles64() : 0;
|
||||
int32 IX0 = 0, IY0 = 0, IZ0 = 0, IX1 = 0, IY1 = 0, IZ1 = 0;
|
||||
int64 Count = 0;
|
||||
if (!GetLatticeBounds(Box, IX0, IY0, IZ0, IX1, IY1, IZ1, Count)
|
||||
|| Count > 8192)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
if (Stats)
|
||||
{
|
||||
if (bUseFinalField) { ++Stats->ExactFinalLeaves; }
|
||||
else { ++Stats->ExactCoreLeaves; }
|
||||
}
|
||||
|
||||
bool bAllSolid = true;
|
||||
bool bAllAir = true;
|
||||
int32 PositiveCount = 0;
|
||||
int32 NegativeCount = 0;
|
||||
int32 ZeroOrInvalidCount = 0;
|
||||
float MinDensity = FLT_MAX;
|
||||
float MaxDensity = -FLT_MAX;
|
||||
for (int32 IZ = IZ0; IZ <= IZ1; ++IZ)
|
||||
for (int32 IY = IY0; IY <= IY1; ++IY)
|
||||
for (int32 IX = IX0; IX <= IX1; ++IX)
|
||||
{
|
||||
const float WorldX = Origin.X + static_cast<float>(IX) * Step;
|
||||
const float WorldY = Origin.Y + static_cast<float>(IY) * Step;
|
||||
const float WorldZ = Origin.Z + static_cast<float>(IZ) * Step;
|
||||
// The operator stack is the cheap proof field. A passage/landing post is applied
|
||||
// after that stack by GetDensityAt; for a small child that intersects one of those
|
||||
// exact lattice modifiers, certify the final MC-facing field instead of trusting a
|
||||
// core-stack verdict that is deliberately conservative at the post boundary.
|
||||
const uint64 SampleStartCycles = bTask4TimingProbe ? FPlatformTime::Cycles64() : 0;
|
||||
const float Density = bUseFinalField
|
||||
? -Generator->GetDensityAt(WorldX, WorldY, WorldZ)
|
||||
: Stack.EvalInternal(WorldX, WorldY, WorldZ);
|
||||
if (bTask4TimingProbe)
|
||||
{
|
||||
ExactSampleCycles += FPlatformTime::Cycles64() - SampleStartCycles;
|
||||
++ExactSampleCount;
|
||||
}
|
||||
const float Density = EvaluateExactLatticeValue(IX, IY, IZ, bUseFinalField);
|
||||
if (!FMath::IsFinite(Density) || Density == 0.0f)
|
||||
{
|
||||
++ZeroOrInvalidCount;
|
||||
@@ -3583,8 +3726,6 @@ static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack,
|
||||
IY = IY1;
|
||||
break;
|
||||
}
|
||||
MinDensity = FMath::Min(MinDensity, Density);
|
||||
MaxDensity = FMath::Max(MaxDensity, Density);
|
||||
if (Density > 0.0f) { ++PositiveCount; bAllAir = false; }
|
||||
else { ++NegativeCount; bAllSolid = false; }
|
||||
if (!bAllSolid && !bAllAir)
|
||||
@@ -3597,56 +3738,156 @@ static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack,
|
||||
const EVoxelTileClass Verdict = ZeroOrInvalidCount > 0 || (PositiveCount > 0 && NegativeCount > 0)
|
||||
? EVoxelTileClass::Mixed
|
||||
: (bAllSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir);
|
||||
if (bTask4ExactProbe)
|
||||
if (Stats)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4ExactProbe] depth_box=(%.0f,%.0f,%.0f)-(%.0f,%.0f,%.0f) count=%lld pos=%d neg=%d zero=%d min=%.6g max=%.6g verdict=%d"),
|
||||
(float)Box.Min.X, (float)Box.Min.Y, (float)Box.Min.Z,
|
||||
(float)Box.Max.X, (float)Box.Max.Y, (float)Box.Max.Z,
|
||||
(long long)Count, PositiveCount, NegativeCount, ZeroOrInvalidCount,
|
||||
MinDensity, MaxDensity, static_cast<int32>(Verdict));
|
||||
const uint64 Elapsed = FPlatformTime::Cycles64() - ExactStartCycles;
|
||||
if (bUseFinalField) { Stats->ExactFinalCycles += Elapsed; }
|
||||
else { Stats->ExactCoreCycles += Elapsed; }
|
||||
}
|
||||
return Verdict;
|
||||
};
|
||||
auto Refine = [&](auto&& Self, const FBox& Box, int32 Depth) -> EVoxelTileClass
|
||||
{
|
||||
++RefineNodeCount;
|
||||
const uint64 StackStartCycles = bTask4TimingProbe ? FPlatformTime::Cycles64() : 0;
|
||||
if (Stats) { ++Stats->RefineNodes; }
|
||||
const uint64 StackStartCycles = Stats ? FPlatformTime::Cycles64() : 0;
|
||||
const EVoxelTileClass Whole = Stack.ClassifyBox(Box, Context);
|
||||
if (bTask4TimingProbe)
|
||||
if (Stats)
|
||||
{
|
||||
StackClassifyCycles += FPlatformTime::Cycles64() - StackStartCycles;
|
||||
++Stats->StackBoxCalls;
|
||||
Stats->StackBoxCycles += FPlatformTime::Cycles64() - StackStartCycles;
|
||||
switch (Whole)
|
||||
{
|
||||
case EVoxelTileClass::Mixed: ++Stats->WholeMixedNodes; break;
|
||||
case EVoxelTileClass::AllSolid: ++Stats->WholeSolidNodes; break;
|
||||
case EVoxelTileClass::AllAir: ++Stats->WholeAirNodes; break;
|
||||
default: break;
|
||||
}
|
||||
const bool bHasStructuralCandidate = StructuralManager != nullptr
|
||||
&& (StructuralManager->AnyPassageNearLattice(
|
||||
Box, Context.LatticeOriginVoxels, Context.Step)
|
||||
|| StructuralManager->AnyPassageLandingFloorNearLattice(
|
||||
Box, Context.LatticeOriginVoxels, Context.Step)
|
||||
|| StructuralManager->AnyOriginLandingNearLattice(
|
||||
Box, Context.LatticeOriginVoxels, Context.Step)
|
||||
|| StructuralManager->AnyOriginLandingFloorNearLattice(
|
||||
Stats->MaxRefinementDepth = FMath::Max(
|
||||
Stats->MaxRefinementDepth, static_cast<uint32>(Depth));
|
||||
}
|
||||
// The graph stack is not the complete MC field: GetDensityAt reasserts the walkable
|
||||
// tunnel core after the stack and after the disturbance/landing posts. The source's
|
||||
// box audit tells us whether that post can have a tunnel candidate in this exact box.
|
||||
// Keep this test local to room-graph stacks; the diagnostic is thread-local and would be
|
||||
// stale for a slab/maze stack. A candidate only widens the work, never grants a skip.
|
||||
if (Depth == 0 && Context.bUseLatticeProof && StructuralManager != nullptr)
|
||||
{
|
||||
bRootPassageAirKnownAbsent = !StructuralManager->AnyPassageAirPostNearLattice(
|
||||
VoxelBox, Context.LatticeOriginVoxels, Context.Step,
|
||||
StructuralBaseDensity, Context.EdgeSealThickness);
|
||||
bRootOriginAirKnownAbsent = !StructuralManager->AnyOriginLandingAirNearLattice(
|
||||
VoxelBox, Context.LatticeOriginVoxels, Context.Step,
|
||||
StructuralBaseDensity, Context.EdgeSealThickness);
|
||||
}
|
||||
const bool bHasRoomGeometry = Stack.ProvidesResource(VoxelOpResources::RoomGeometry);
|
||||
const VoxelDensityOps::FRoomBoxDiagnostic RoomBox = bHasRoomGeometry
|
||||
? VoxelDensityOps::GetLastRoomBoxDiagnostic()
|
||||
: VoxelDensityOps::FRoomBoxDiagnostic();
|
||||
if (Stats && bHasRoomGeometry)
|
||||
{
|
||||
Stats->RoomTailQueries += RoomBox.ExactTailQueries;
|
||||
Stats->RoomTailEvaluated += RoomBox.ExactTailEvaluated;
|
||||
Stats->RoomTailCycles += RoomBox.ExactTailCycles;
|
||||
Stats->RoomPropagateCycles += RoomBox.PropagateCycles;
|
||||
Stats->RoomExactPrimitiveCycles += RoomBox.ExactPrimitiveCycles;
|
||||
Stats->RoomCacheWindowCycles += RoomBox.CacheWindowCycles;
|
||||
Stats->RoomNumRooms = RoomBox.NumRooms;
|
||||
Stats->RoomNumTunnels = RoomBox.NumTunnels;
|
||||
Stats->RoomNumRoomFloorJoins = RoomBox.NumRoomFloorJoins;
|
||||
Stats->RoomNumPits = RoomBox.NumPits;
|
||||
Stats->RoomNumChimneys = RoomBox.NumChimneys;
|
||||
}
|
||||
const bool bHasTunnelAirCandidate = bHasRoomGeometry
|
||||
&& RoomBox.bMayHaveTunnelCoreAir;
|
||||
const bool bHasTunnelSupportCandidate = bHasRoomGeometry
|
||||
&& RoomBox.bMayHaveTunnelSupportFloor;
|
||||
// The tail has two polarities. A solid-only tail cannot turn a proven solid child into
|
||||
// geometry, and an air-only tail cannot turn a proven air child into geometry. Keep the
|
||||
// uncertain Mixed parent conservative by descending; at the resolved children only the
|
||||
// polarity that can change the answer is allowed to request a final-field certificate.
|
||||
const bool bHasTunnelCoreCandidate = Whole == EVoxelTileClass::AllSolid
|
||||
? bHasTunnelAirCandidate
|
||||
: Whole == EVoxelTileClass::AllAir
|
||||
? bHasTunnelSupportCandidate
|
||||
: false;
|
||||
// FPassageCarveOp and FOriginSpine are already in the stack. Reusing their broad spatial
|
||||
// candidates here was the LOD0 bug: a passage that the stack had already proved to miss
|
||||
// the sampled sign still forced a final-field refinement of every mixed parent. Only the
|
||||
// writers that run after the stack remain candidates in this section: post-disturbance
|
||||
// air, and support floors that can add solid. Unknown/non-finite input remains a
|
||||
// candidate in each exact helper, so this separation cannot authorize a false skip.
|
||||
// A Mixed core is not a verdict yet. Descend until the interval proof resolves its
|
||||
// polarity; an air post matters to an AllSolid child, while a solid floor/support post
|
||||
// matters to an AllAir child. Checking both writers on every Mixed ancestor was the
|
||||
// remaining LOD0 classifier tax and could not improve the answer before the child split.
|
||||
const bool bNeedsPostAirCandidate = Whole == EVoxelTileClass::AllSolid;
|
||||
const bool bPassageAirCandidate = bNeedsPostAirCandidate
|
||||
&& !bRootPassageAirKnownAbsent
|
||||
&& StructuralManager != nullptr
|
||||
&& StructuralManager->AnyPassageAirPostNearLattice(
|
||||
Box, Context.LatticeOriginVoxels, Context.Step,
|
||||
StructuralBaseDensity, Context.EdgeSealThickness);
|
||||
const bool bOriginAirCandidate = bNeedsPostAirCandidate
|
||||
&& !bRootOriginAirKnownAbsent
|
||||
&& StructuralManager != nullptr
|
||||
&& StructuralManager->AnyOriginLandingAirNearLattice(
|
||||
Box, Context.LatticeOriginVoxels, Context.Step,
|
||||
StructuralBaseDensity, Context.EdgeSealThickness);
|
||||
// Floors only add solid. They matter when this core box is already AllAir; on a Mixed
|
||||
// parent, descending to its children preserves the proof while avoiding a padded floor
|
||||
// AABB forcing a final-field walk over every mixed ancestor. Ask the exact lattice floor
|
||||
// predicate only at an AllAir child. It is conservative on malformed data and never
|
||||
// grants a skip.
|
||||
const bool bHasCarveCandidate = bPassageAirCandidate || bOriginAirCandidate;
|
||||
const bool bHasStructuralCandidate = Whole == EVoxelTileClass::AllSolid
|
||||
? bHasCarveCandidate
|
||||
: Whole == EVoxelTileClass::AllAir
|
||||
&& (StructuralManager != nullptr
|
||||
&& StructuralManager->AnyLandingFloorAtLattice(
|
||||
Box, Context.LatticeOriginVoxels, Context.Step));
|
||||
// A resolved core box already includes the conservative passage carve bound. Only a
|
||||
// Mixed core box needs the post-disturbance/floor evaluator; applying it to every resolved
|
||||
// child would turn one narrow landing into hundreds of tiny GetDensityAt walks.
|
||||
// A resolved core box still needs a finite-lattice certificate when a common MC tail can
|
||||
// rewrite it (passage/landing posts or the graph tunnel-core backstop). Without this,
|
||||
// the interval proves the stack's "all solid" field and silently skips a core tunnel
|
||||
// that GetDensityAt opens afterwards. Boxes with no tail candidate retain the cheap
|
||||
// interval path.
|
||||
const bool bNeedsFinalField = Generator != nullptr
|
||||
&& bHasStructuralCandidate
|
||||
&& Whole == EVoxelTileClass::Mixed;
|
||||
const bool bTask4RefineProbe = Context.bUseLatticeProof
|
||||
&& (Context.LatticeOriginVoxels == FIntVector(-96, 96, -96)
|
||||
|| Context.LatticeOriginVoxels == FIntVector(-32, 64, -64)
|
||||
|| Context.LatticeOriginVoxels == FIntVector(-64, -64, -64)
|
||||
|| Context.LatticeOriginVoxels == FIntVector(-64, -96, -64))
|
||||
&& Context.Step == 1;
|
||||
if (bTask4RefineProbe)
|
||||
&& (bHasStructuralCandidate || bHasTunnelCoreCandidate);
|
||||
if (Stats && bNeedsFinalField)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4RefineProbe] depth=%d box=(%.0f,%.0f,%.0f)-(%.0f,%.0f,%.0f) verdict=%d"),
|
||||
Depth, (float)Box.Min.X, (float)Box.Min.Y, (float)Box.Min.Z,
|
||||
(float)Box.Max.X, (float)Box.Max.Y, (float)Box.Max.Z,
|
||||
static_cast<int32>(Whole));
|
||||
++Stats->NeedsFinalFieldNodes;
|
||||
}
|
||||
if (Whole != EVoxelTileClass::Mixed)
|
||||
// A final-field certificate is exact on the same MC lattice as GenerateMesh. Use it for
|
||||
// the small block-sized tail-risk boxes, including an already-resolved AllSolid/AllAir
|
||||
// box. The common streamed tile is deliberately not a final-field sampling job: a
|
||||
// coarse tile can contain thousands of MC vertices, and its proof must stay interval-only.
|
||||
// Larger tail-risk boxes remain conservative Mixed until the mesher's normal sampling.
|
||||
bool bAttemptedExact = false;
|
||||
const float FinalFieldBoxExtent = FMath::Max3(
|
||||
static_cast<float>(Box.Max.X - Box.Min.X),
|
||||
static_cast<float>(Box.Max.Y - Box.Min.Y),
|
||||
static_cast<float>(Box.Max.Z - Box.Min.Z));
|
||||
const bool bSmallFinalFieldBox = bNeedsFinalField
|
||||
&& FinalFieldBoxExtent <= 8.0f;
|
||||
if (bSmallFinalFieldBox)
|
||||
{
|
||||
int32 ExactIX0 = 0, ExactIY0 = 0, ExactIZ0 = 0;
|
||||
int32 ExactIX1 = 0, ExactIY1 = 0, ExactIZ1 = 0;
|
||||
int64 ExactCount = 0;
|
||||
if (GetLatticeBounds(Box, ExactIX0, ExactIY0, ExactIZ0,
|
||||
ExactIX1, ExactIY1, ExactIZ1, ExactCount)
|
||||
&& ExactCount <= 8192)
|
||||
{
|
||||
bAttemptedExact = true;
|
||||
const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, true);
|
||||
if (Exact != EVoxelTileClass::Mixed)
|
||||
{
|
||||
return Exact;
|
||||
}
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
}
|
||||
|
||||
if (Whole != EVoxelTileClass::Mixed && !bNeedsFinalField)
|
||||
{
|
||||
return Whole;
|
||||
}
|
||||
@@ -3655,10 +3896,41 @@ static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack,
|
||||
// it is not a lower-resolution fallback and never changes GenerateMesh's sample count.
|
||||
// This discharges the last conservative SDF interval misses without walking another
|
||||
// refinement level or rebuilding the morphology bounds for every half-box.
|
||||
if (Depth >= 3)
|
||||
// The corner check is only a work gate. It is not a proof: matching corners still pay the
|
||||
// complete exact lattice certificate, so a thin cave or a zero/invalid sample cannot be
|
||||
// skipped accidentally. For a genuinely uniform LOD0 empty box this collapses the old
|
||||
// 585-node/64k-sample fallback to at most the 33^3 unique lattice vertices.
|
||||
// A depth-1 box still contains 17^3 MC vertices; probing its corners and then certifying
|
||||
// the whole child is more expensive than the interval refinement it was meant to avoid.
|
||||
// Wait until the depth-3 tail (at most 9^3 vertices) before paying an exact core walk.
|
||||
const bool bCornersHaveOneSign = !bNeedsFinalField && Depth >= 3
|
||||
&& CornersHaveOneSign(Box, false);
|
||||
if (bCornersHaveOneSign)
|
||||
{
|
||||
++ExactLeafCount;
|
||||
const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, bNeedsFinalField);
|
||||
bAttemptedExact = true;
|
||||
const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, false);
|
||||
if (Exact != EVoxelTileClass::Mixed)
|
||||
{
|
||||
return Exact;
|
||||
}
|
||||
}
|
||||
|
||||
// A two-voxel unresolved tail is still cheap to certify on the final MC lattice (3^3
|
||||
// points). Doing this before the last split avoids eight more interval folds for the
|
||||
// localized passage/post boundary that caused the original LOD0 false-Mixed bail.
|
||||
if (!bAttemptedExact && Whole == EVoxelTileClass::Mixed && !bNeedsFinalField
|
||||
&& Context.bUseLatticeProof && Depth >= 4 && FinalFieldBoxExtent <= 2.0f)
|
||||
{
|
||||
const EVoxelTileClass ExactFinal = ClassifyExactLatticeLeaf(Box, true);
|
||||
if (ExactFinal != EVoxelTileClass::Mixed)
|
||||
{
|
||||
return ExactFinal;
|
||||
}
|
||||
bAttemptedExact = true;
|
||||
}
|
||||
if (!bAttemptedExact && Depth >= 3 && (!bNeedsFinalField || bSmallFinalFieldBox))
|
||||
{
|
||||
const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, bSmallFinalFieldBox);
|
||||
if (Exact != EVoxelTileClass::Mixed)
|
||||
{
|
||||
return Exact;
|
||||
@@ -3674,10 +3946,24 @@ static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack,
|
||||
}
|
||||
if (Depth >= MaxRefinementDepth)
|
||||
{
|
||||
// The operator interval can remain Mixed at a one-voxel tail even though the final
|
||||
// MC-facing field is uniform after a structural post. Certify that tiny tail on the
|
||||
// exact lattice before conceding the whole tile to the mesher. This is a proof over
|
||||
// all corners the mesher would consume; zero/non-finite values remain Mixed.
|
||||
if (Whole == EVoxelTileClass::Mixed && !bNeedsFinalField
|
||||
&& Context.bUseLatticeProof && FinalFieldBoxExtent <= 1.0f)
|
||||
{
|
||||
const EVoxelTileClass ExactFinal = ClassifyExactLatticeLeaf(Box, true);
|
||||
if (ExactFinal != EVoxelTileClass::Mixed)
|
||||
{
|
||||
return ExactFinal;
|
||||
}
|
||||
}
|
||||
return bNeedsFinalField ? EVoxelTileClass::Mixed : Whole;
|
||||
}
|
||||
|
||||
const FVector Mid = (Box.Min + Box.Max) * 0.5f;
|
||||
if (Stats) { ++Stats->SplitNodes; }
|
||||
bool bSawSolid = false;
|
||||
bool bSawAir = false;
|
||||
for (int32 Z = 0; Z < 2; ++Z)
|
||||
@@ -3707,17 +3993,6 @@ static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack,
|
||||
return bSawSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
|
||||
};
|
||||
const EVoxelTileClass Result = Refine(Refine, VoxelBox, 0);
|
||||
if (bTask4TimingProbe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4TimingProbe] origin=(%d,%d,%d) step=%d nodes=%d exact_leaves=%d samples=%d stack_ms=%.3f sample_ms=%.3f result=%d"),
|
||||
Context.LatticeOriginVoxels.X, Context.LatticeOriginVoxels.Y,
|
||||
Context.LatticeOriginVoxels.Z, Context.Step,
|
||||
RefineNodeCount, ExactLeafCount, ExactSampleCount,
|
||||
FPlatformTime::ToMilliseconds64(StackClassifyCycles),
|
||||
FPlatformTime::ToMilliseconds64(ExactSampleCycles),
|
||||
static_cast<int32>(Result));
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -3728,10 +4003,12 @@ static EVoxelTileClass VF_ClassifyBoxWithWarpRetry(const FVoxelOpStack& Stack,
|
||||
const FBox& VoxelBox,
|
||||
const FVoxelOpContext& Context,
|
||||
const UVoxelGenerator* Generator = nullptr,
|
||||
const UVoxelStrateManager* StructuralManager = nullptr)
|
||||
const UVoxelStrateManager* StructuralManager = nullptr,
|
||||
float StructuralBaseDensity = 8.0f,
|
||||
FVoxelTileClassificationStats* Stats = nullptr)
|
||||
{
|
||||
const EVoxelTileClass Initial = VF_ClassifyBoxRefined(
|
||||
Stack, VoxelBox, Context, Generator, StructuralManager);
|
||||
Stack, VoxelBox, Context, Generator, StructuralManager, StructuralBaseDensity, Stats);
|
||||
// A Mixed fine tile is not evidence of geometry: it can be the conservative result of a
|
||||
// coarse operator interval. Keep the bounded retry for every LOD; skipping it recreates
|
||||
// the false-Mixed empty tiles that send the mesher through the expensive path.
|
||||
@@ -3744,11 +4021,20 @@ static EVoxelTileClass VF_ClassifyBoxWithWarpRetry(const FVoxelOpStack& Stack,
|
||||
FVoxelOpContext TightContext = Context;
|
||||
TightContext.bTightenWarpProof = true;
|
||||
return VF_ClassifyBoxRefined(
|
||||
Stack, VoxelBox, TightContext, Generator, StructuralManager);
|
||||
Stack, VoxelBox, TightContext, Generator, StructuralManager, StructuralBaseDensity, Stats);
|
||||
}
|
||||
|
||||
EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const
|
||||
EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels,
|
||||
int32 Step, int32 CellsPerAxis) const
|
||||
{
|
||||
return ClassifyTile(OriginVoxels, Step, CellsPerAxis, nullptr);
|
||||
}
|
||||
|
||||
EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step,
|
||||
int32 CellsPerAxis,
|
||||
FVoxelTileClassificationStats* OutStats) const
|
||||
{
|
||||
if (OutStats) { *OutStats = FVoxelTileClassificationStats(); }
|
||||
// Mêmes clamps que GenerateMesh — le verdict doit couvrir le treillis réellement échantillonné.
|
||||
Step = FMath::Max(1, Step);
|
||||
// ClassifyTile runs before GenerateMesh installs its LOD TLS. The room-graph source uses this
|
||||
@@ -3767,15 +4053,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
const int32 MinX = OriginVoxels.X, MaxX = OriginVoxels.X + CPA * Step;
|
||||
const int32 MinY = OriginVoxels.Y, MaxY = OriginVoxels.Y + CPA * Step;
|
||||
const int32 MinZ = OriginVoxels.Z, MaxZ = OriginVoxels.Z + CPA * Step;
|
||||
const bool bTask4Probe =
|
||||
(OriginVoxels == FIntVector(-96, 96, -96) && Step == 1)
|
||||
|| (OriginVoxels == FIntVector(-32, 64, -64) && Step == 1)
|
||||
|| (OriginVoxels == FIntVector(-64, -64, -64) && Step == 1)
|
||||
|| (OriginVoxels == FIntVector(-64, -96, -64) && Step == 1)
|
||||
|| (OriginVoxels == FIntVector(0, 32, -96) && Step == 2)
|
||||
|| (OriginVoxels == FIntVector(-32, -32, -32) && Step == 8)
|
||||
|| (OriginVoxels == FIntVector(-96, -32, -32) && Step == 16);
|
||||
|
||||
// Division entière PLANCHER (les coords négatives tronquent vers 0 en C++ — pas floor).
|
||||
auto FloorDivC = [](int32 A, int32 B) -> int32
|
||||
{
|
||||
@@ -3893,7 +4170,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
RecipeContext.LatticeOriginVoxels = OriginVoxels;
|
||||
RecipeStack.PrepareChunk(RecipeContext);
|
||||
const EVoxelTileClass RecipeVerdict =
|
||||
VF_ClassifyBoxWithWarpRetry(RecipeStack, TileVoxelBox, RecipeContext);
|
||||
VF_ClassifyBoxWithWarpRetry(RecipeStack, TileVoxelBox, RecipeContext,
|
||||
nullptr, nullptr, 8.0f, OutStats);
|
||||
if (RecipeVerdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
}
|
||||
return RecipeVerdict;
|
||||
}
|
||||
|
||||
@@ -3986,7 +4267,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
RegionContext.LatticeOriginVoxels = OriginVoxels;
|
||||
RegionStack.PrepareChunk(RegionContext);
|
||||
const EVoxelTileClass RegionVerdict =
|
||||
VF_ClassifyBoxWithWarpRetry(RegionStack, TileVoxelBox, RegionContext);
|
||||
VF_ClassifyBoxWithWarpRetry(RegionStack, TileVoxelBox, RegionContext,
|
||||
nullptr, nullptr, 8.0f, OutStats);
|
||||
if (RegionVerdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
}
|
||||
return RegionVerdict;
|
||||
}
|
||||
|
||||
@@ -4100,7 +4385,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
{
|
||||
// Retrouve (ou résout) le slot de strate — l'identité vient des bornes chunk-Z du layout.
|
||||
int32 TopCZ = 0, BotCZ = 0;
|
||||
if (!StrateManager->GetStrateChunkZBounds(ChunkZ, TopCZ, BotCZ)) return EVoxelTileClass::Mixed;
|
||||
if (!StrateManager->GetStrateChunkZBounds(ChunkZ, TopCZ, BotCZ))
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
MemoSlotIdx = -1;
|
||||
for (int32 s = 0; s < NumSlots; ++s)
|
||||
{
|
||||
@@ -4108,7 +4396,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
}
|
||||
if (MemoSlotIdx < 0)
|
||||
{
|
||||
if (NumSlots >= 2) return EVoxelTileClass::Mixed; // >2 strates surface dans une tuile : improbable
|
||||
if (NumSlots >= 2)
|
||||
{
|
||||
return EVoxelTileClass::Mixed; // >2 strates surface dans une tuile : improbable
|
||||
}
|
||||
MemoSlotIdx = NumSlots++;
|
||||
FSurfSlot& S = Slots[MemoSlotIdx];
|
||||
S.BotChunkZ = BotCZ;
|
||||
@@ -4232,17 +4523,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
if (bInBand) { bCanAir = false; }
|
||||
else { S.InteriorZ.Add(Z); }
|
||||
}
|
||||
if (!bCanSolid && !bCanAir) { return EVoxelTileClass::Mixed; }
|
||||
}
|
||||
|
||||
if (bTask4Probe)
|
||||
if (!bCanSolid && !bCanAir)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] phase=z origin=(%d,%d,%d) step=%d solid=%d air=%d cave=%d gap=%d surface=%d out=%d cave_z=(%d,%d)"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
bCanSolid ? 1 : 0, bCanAir ? 1 : 0, bAnyCave ? 1 : 0,
|
||||
bAnyGap ? 1 : 0, bAnySurface ? 1 : 0, bAnyOutOfLayout ? 1 : 0,
|
||||
CaveMinZ, CaveMaxZ);
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
}
|
||||
|
||||
// Out-of-layout is a constant-air field. Passage tubes and origin rooms can only carve that
|
||||
@@ -4284,13 +4568,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
{
|
||||
bCanAir = false;
|
||||
}
|
||||
if (bTask4Probe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] phase=guards origin=(%d,%d,%d) step=%d solid=%d air=%d"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
|
||||
}
|
||||
// ── LE PLIAGE DE LA PILE D'OPÉRATEURS, POUR LES ARCHÉTYPES DE CAVE ──
|
||||
//=========================================================================
|
||||
// ⚠️ ERREUR ICI = TROU, PAS RÉGRESSION. Un verdict non-Mixed fait SAUTER `GenerateMesh` : pas
|
||||
@@ -4484,65 +4761,12 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
TileStack.PrepareChunk(OpCtx);
|
||||
|
||||
const FBox CaveBox(FVector((float)MinX, (float)MinY, (float)CaveMinZ),
|
||||
FVector((float)MaxX, (float)MaxY, (float)CaveMaxZ));
|
||||
const EVoxelTileClass StackVerdict = VF_ClassifyBoxWithWarpRetry(
|
||||
TileStack, CaveBox, OpCtx, this, StrateManager);
|
||||
if (bTask4Probe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] phase=stack origin=(%d,%d,%d) step=%d verdict=%d solid=%d air=%d"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
static_cast<int32>(StackVerdict), bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
|
||||
}
|
||||
TileStack, CaveBox, OpCtx, this, StrateManager, TileTunnel.BaseDensity, OutStats);
|
||||
if (StackVerdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
if (bTask4Probe)
|
||||
{
|
||||
int32 SolidKiller = INDEX_NONE;
|
||||
int32 AirKiller = INDEX_NONE;
|
||||
const EVoxelTileClass Attributed = TileStack.ClassifyBoxAttributed(
|
||||
CaveBox, OpCtx, SolidKiller, AirKiller);
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] stack_attributed origin=(%d,%d,%d) step=%d verdict=%d solid_killer=%d:%s air_killer=%d:%s"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
static_cast<int32>(Attributed), SolidKiller,
|
||||
TileStack.GetOpDebugName(SolidKiller), AirKiller,
|
||||
TileStack.GetOpDebugName(AirKiller));
|
||||
|
||||
int32 FinalPositive = 0;
|
||||
int32 FinalNegative = 0;
|
||||
int32 FinalZeroOrInvalid = 0;
|
||||
float FinalMin = FLT_MAX;
|
||||
float FinalMax = -FLT_MAX;
|
||||
for (int32 Z = MinZ; Z <= MaxZ; Z += Step)
|
||||
for (int32 Y = MinY; Y <= MaxY; Y += Step)
|
||||
for (int32 X = MinX; X <= MaxX; X += Step)
|
||||
{
|
||||
const float FinalDensity = GetDensityAt((float)X, (float)Y, (float)Z);
|
||||
if (!FMath::IsFinite(FinalDensity) || FinalDensity == 0.0f)
|
||||
{
|
||||
++FinalZeroOrInvalid;
|
||||
}
|
||||
else if (FinalDensity > 0.0f)
|
||||
{
|
||||
++FinalPositive;
|
||||
FinalMin = FMath::Min(FinalMin, FinalDensity);
|
||||
FinalMax = FMath::Max(FinalMax, FinalDensity);
|
||||
}
|
||||
else
|
||||
{
|
||||
++FinalNegative;
|
||||
FinalMin = FMath::Min(FinalMin, FinalDensity);
|
||||
FinalMax = FMath::Max(FinalMax, FinalDensity);
|
||||
}
|
||||
}
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] final_samples origin=(%d,%d,%d) step=%d pos=%d neg=%d zero=%d min=%.6g max=%.6g"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
FinalPositive, FinalNegative, FinalZeroOrInvalid, FinalMin, FinalMax);
|
||||
}
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailStackVerdict);
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
@@ -4657,13 +4881,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
// sans point intérieur ont tué AllAir via gap/seal). Égalité = prudence → Mixed.
|
||||
if (bCanSolid == bCanAir)
|
||||
{
|
||||
if (bTask4Probe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] phase=final origin=(%d,%d,%d) step=%d verdict=0 solid=%d air=%d"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
|
||||
}
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
if (bAnyCave)
|
||||
@@ -4675,13 +4892,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
if (bCanSolid) { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); }
|
||||
else { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir); }
|
||||
}
|
||||
if (bTask4Probe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4Probe] phase=final origin=(%d,%d,%d) step=%d verdict=%d solid=%d air=%d"),
|
||||
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
|
||||
bCanSolid ? 1 : 2, bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
|
||||
}
|
||||
return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
|
||||
}
|
||||
|
||||
|
||||
@@ -237,7 +237,13 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
// resolution are removed from a tile that reaches the mesher.
|
||||
TArray<uint8> BlockSkip;
|
||||
int32 BlocksPerAxis = 0;
|
||||
if (bUseBlockEarlyOut && !OutCaptureGrid && Step == 1)
|
||||
// LOD2's 16-cell tile is the remaining false-Mixed empty case in the streamed game path.
|
||||
// Its step-8 lattice can use the same proof-only 8-cell block partition as LOD0. Keep the
|
||||
// gate off for steps 2/16/32: their meshing work is already smaller than repeating a full
|
||||
// tile classifier per block, and this preserves the established coarse-LOD latency profile.
|
||||
const bool bUseProofBlocks = bUseBlockEarlyOut && !OutCaptureGrid
|
||||
&& (Step == 1 || Step == 8);
|
||||
if (bUseProofBlocks)
|
||||
{
|
||||
constexpr int32 BlockCells = 8;
|
||||
BlocksPerAxis = (CellsPerAxis + BlockCells - 1) / BlockCells;
|
||||
@@ -246,6 +252,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
EVoxelTileClass FirstUniformVerdict = EVoxelTileClass::Mixed;
|
||||
bool bAllRelevantBlocksUniform = true;
|
||||
bool bAllRelevantBlocksSame = true;
|
||||
TArray<int32> MixedBlockIndices;
|
||||
|
||||
for (int32 BlockZ = 0; BlockZ < BlocksPerAxis; ++BlockZ)
|
||||
{
|
||||
@@ -271,17 +278,22 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
}
|
||||
const EVoxelTileClass Verdict = Generator->ClassifyTile(
|
||||
BlockOrigin, Step, BlockCells);
|
||||
if (Verdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
MixedBlockIndices.Add(
|
||||
((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX);
|
||||
}
|
||||
if (Verdict == EVoxelTileClass::AllSolid
|
||||
|| Verdict == EVoxelTileClass::AllAir)
|
||||
{
|
||||
BlockSkip[((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX] = 1;
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
Verdict == EVoxelTileClass::AllSolid
|
||||
? VoxelDensityProfile::ECounter::MesherBlockAllSolid
|
||||
: VoxelDensityProfile::ECounter::MesherBlockAllAir);
|
||||
}
|
||||
// Keep the classifier result as a candidate until the exact density grid
|
||||
// used by this mesher has been filled. ClassifyTile is conservative by
|
||||
// contract, but its proof and the mesher share worker-local caches; a
|
||||
// cache replacement must never turn that contract into a cell skip by
|
||||
// itself. The post-grid sign check below is the final proof consumed by
|
||||
// the cell loop and clears any candidate whose actual MC vertices disagree.
|
||||
BlockSkip[((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX] =
|
||||
Verdict == EVoxelTileClass::AllSolid ? 1 : 2;
|
||||
if (!bHaveUniformBlock)
|
||||
{
|
||||
FirstUniformVerdict = Verdict;
|
||||
@@ -300,10 +312,84 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
}
|
||||
}
|
||||
|
||||
if (bHaveUniformBlock && bAllRelevantBlocksUniform && bAllRelevantBlocksSame)
|
||||
// A conservative whole-tile proof can leave a small block unresolved even when every
|
||||
// other block has the same verdict. Resolve that tail on the exact MC lattice. This uses
|
||||
// the final GetDensityAt field, checks every cell corner, treats zero/non-finite values
|
||||
// as unknown, and never changes the grid resolution or skips an uncertified block.
|
||||
if (bHaveUniformBlock && bAllRelevantBlocksSame && !bAllRelevantBlocksUniform
|
||||
&& MixedBlockIndices.Num() <= 4)
|
||||
{
|
||||
int32 RemainingMixedBlocks = MixedBlockIndices.Num();
|
||||
for (const int32 MixedBlockIndex : MixedBlockIndices)
|
||||
{
|
||||
const int32 BlockX = MixedBlockIndex % BlocksPerAxis;
|
||||
const int32 BlockY = (MixedBlockIndex / BlocksPerAxis) % BlocksPerAxis;
|
||||
const int32 BlockZ = MixedBlockIndex / (BlocksPerAxis * BlocksPerAxis);
|
||||
const FIntVector BlockOrigin = OriginVoxels + FIntVector(
|
||||
BlockX * BlockCells * Step,
|
||||
BlockY * BlockCells * Step,
|
||||
BlockZ * BlockCells * Step);
|
||||
|
||||
bool bAllSolid = true;
|
||||
bool bAllAir = true;
|
||||
for (int32 Z = 0; Z <= BlockCells && (bAllSolid || bAllAir); ++Z)
|
||||
{
|
||||
for (int32 Y = 0; Y <= BlockCells && (bAllSolid || bAllAir); ++Y)
|
||||
{
|
||||
for (int32 X = 0; X <= BlockCells; ++X)
|
||||
{
|
||||
const float Density = Generator->GetDensityAt(
|
||||
BlockOrigin.X + X * Step,
|
||||
BlockOrigin.Y + Y * Step,
|
||||
BlockOrigin.Z + Z * Step);
|
||||
if (!FMath::IsFinite(Density) || Density == 0.0f)
|
||||
{
|
||||
bAllSolid = false;
|
||||
bAllAir = false;
|
||||
break;
|
||||
}
|
||||
bAllSolid &= Density < 0.0f;
|
||||
bAllAir &= Density > 0.0f;
|
||||
if (!bAllSolid && !bAllAir)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const EVoxelTileClass ExactVerdict = bAllSolid
|
||||
? EVoxelTileClass::AllSolid
|
||||
: (bAllAir ? EVoxelTileClass::AllAir : EVoxelTileClass::Mixed);
|
||||
if (ExactVerdict == EVoxelTileClass::AllSolid
|
||||
|| ExactVerdict == EVoxelTileClass::AllAir)
|
||||
{
|
||||
BlockSkip[MixedBlockIndex] =
|
||||
ExactVerdict == EVoxelTileClass::AllSolid ? 1 : 2;
|
||||
--RemainingMixedBlocks;
|
||||
if (ExactVerdict != FirstUniformVerdict)
|
||||
{
|
||||
bAllRelevantBlocksSame = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
bAllRelevantBlocksUniform = RemainingMixedBlocks == 0;
|
||||
}
|
||||
|
||||
// Every relevant block has independently proved the same sign. This is a complete
|
||||
// certificate for the tile's MC lattice: the block boxes share their boundary vertices,
|
||||
// so equal signs on both sides cannot hide a surface between blocks. The whole-tile
|
||||
// ClassifyTile path already relies on this same non-Mixed proof contract. Return before
|
||||
// allocating the density grid; no samples are skipped for a tile that can contain
|
||||
// geometry, and the final geometry remains byte-identical because this path emits none.
|
||||
if (bHaveUniformBlock && bAllRelevantBlocksSame && bAllRelevantBlocksUniform)
|
||||
{
|
||||
return MeshData;
|
||||
}
|
||||
|
||||
// A mixed or differently signed block set still needs the exact mesher grid. Classifier
|
||||
// results remain candidates only in that case; the grid validation below keeps the cell
|
||||
// skip independent of worker-local cache state and preserves geometry byte-for-byte.
|
||||
}
|
||||
|
||||
// Réutilise le tampon entre tuiles (thread_local) : SetNumUninitialized garde la
|
||||
@@ -364,6 +450,69 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
}
|
||||
MesherDensityGridTimer.End();
|
||||
|
||||
if (bUseProofBlocks)
|
||||
{
|
||||
// Finalize every block candidate against the exact density samples that the marching-
|
||||
// cubes loop will consume. This is intentionally redundant with ClassifyTile: it is a
|
||||
// cheap soundness fence around the proof/cache hand-off. Unknown, zero, or opposite-sign
|
||||
// vertices clear the candidate, so an unproven block pays the ordinary cell path.
|
||||
constexpr int32 BlockCells = 8;
|
||||
for (int32 BlockZ = 0; BlockZ < BlocksPerAxis; ++BlockZ)
|
||||
{
|
||||
const int32 BlockMinZ = BlockZ * BlockCells;
|
||||
const int32 BlockMaxZ = FMath::Min(CellsPerAxis - 1,
|
||||
BlockMinZ + BlockCells - 1);
|
||||
if (BlockMaxZ < CzLo || BlockMinZ > CzHi)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int32 BlockY = 0; BlockY < BlocksPerAxis; ++BlockY)
|
||||
for (int32 BlockX = 0; BlockX < BlocksPerAxis; ++BlockX)
|
||||
{
|
||||
const int32 BlockIndex =
|
||||
((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX;
|
||||
const uint8 Candidate = BlockSkip[BlockIndex];
|
||||
if (Candidate == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const int32 BlockMaxX = FMath::Min(CellsPerAxis - 1,
|
||||
BlockX * BlockCells + BlockCells - 1);
|
||||
const int32 BlockMaxY = FMath::Min(CellsPerAxis - 1,
|
||||
BlockY * BlockCells + BlockCells - 1);
|
||||
bool bUniform = true;
|
||||
for (int32 Z = BlockMinZ; Z <= BlockMaxZ + 1 && bUniform; ++Z)
|
||||
for (int32 Y = BlockY * BlockCells; Y <= BlockMaxY + 1 && bUniform; ++Y)
|
||||
for (int32 X = BlockX * BlockCells; X <= BlockMaxX + 1; ++X)
|
||||
{
|
||||
const float Density = DensityGrid[
|
||||
((Z + 1) * MDim + (Y + 1)) * MDim + (X + 1)];
|
||||
if (!FMath::IsFinite(Density)
|
||||
|| Density == 0.0f
|
||||
|| (Candidate == 1 ? Density >= 0.0f : Density <= 0.0f))
|
||||
{
|
||||
bUniform = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bUniform)
|
||||
{
|
||||
BlockSkip[BlockIndex] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
Candidate == 1
|
||||
? VoxelDensityProfile::ECounter::MesherBlockAllSolid
|
||||
: VoxelDensityProfile::ECounter::MesherBlockAllAir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── CAPTURE-DURING-MESHING ──
|
||||
// Si demandé et que la tuile est pleine résolution (CellsPerAxis==CHUNK_SIZE ⇒ Step==1<<Level,
|
||||
// donc chaque point de grille = exactement une cellule du clipmap de densité), on recopie les
|
||||
@@ -423,7 +572,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 cx = 0; cx < CellsPerAxis; cx++)
|
||||
{
|
||||
if (bUseBlockEarlyOut && Step == 1
|
||||
if (bUseProofBlocks
|
||||
&& BlockSkip[((cz / 8) * BlocksPerAxis + (cy / 8)) * BlocksPerAxis + (cx / 8)] != 0)
|
||||
{
|
||||
continue;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -11,6 +11,7 @@
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelContentManager.h"
|
||||
#include "VoxelDensityVolume.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelStats.h"
|
||||
// IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc
|
||||
// APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController était
|
||||
@@ -984,6 +985,7 @@ void AVoxelWorld::BeginPlay()
|
||||
Mesher->bGenerateSkirts = Settings->bGenerateSkirts;
|
||||
Mesher->SkirtCells = Settings->SkirtCells;
|
||||
Mesher->LODOctaveDrop = Settings->LODOctaveDrop; // T2.b — 0 = off
|
||||
Mesher->bUseBlockEarlyOut = true;
|
||||
|
||||
// Système de strates — a cooked season takes precedence; otherwise the authored pool path is
|
||||
// exactly the legacy one.
|
||||
@@ -2123,17 +2125,43 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
double MeshSeconds = 0.0;
|
||||
double StreamSeconds = 0.0;
|
||||
int32 ClassifyVerdict = -1; // Mixed = 0, AllSolid = 1, AllAir = 2
|
||||
FVoxelTileClassificationStats ClassifierStats;
|
||||
auto EmitTileProfile = [&]()
|
||||
{
|
||||
if (bProfileTile)
|
||||
{
|
||||
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"),
|
||||
"verdict=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.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 "
|
||||
"room_work=%.6f room_exact=%.6f cache_work=%.6f "
|
||||
"rooms=%d tunnels=%d joins=%d pits=%d chimneys=%d "
|
||||
"whole_mixed=%u whole_solid=%u whole_air=%u final_nodes=%u "
|
||||
"split_nodes=%u max_depth=%u"),
|
||||
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);
|
||||
FPlatformTime::Seconds() - TileStartSeconds,
|
||||
ClassifierStats.RefineNodes, ClassifierStats.StackBoxCalls,
|
||||
ClassifierStats.ExactCoreSamples, ClassifierStats.ExactFinalSamples,
|
||||
ClassifierStats.ExactCoreCacheHits, ClassifierStats.ExactFinalCacheHits,
|
||||
ClassifierStats.ExactCoreLeaves, ClassifierStats.ExactFinalLeaves,
|
||||
ClassifierStats.RoomTailQueries, ClassifierStats.RoomTailEvaluated,
|
||||
FPlatformTime::ToSeconds64(ClassifierStats.StackBoxCycles),
|
||||
FPlatformTime::ToSeconds64(ClassifierStats.ExactCoreCycles),
|
||||
FPlatformTime::ToSeconds64(ClassifierStats.ExactFinalCycles),
|
||||
FPlatformTime::ToSeconds64(ClassifierStats.RoomTailCycles),
|
||||
FPlatformTime::ToSeconds64(ClassifierStats.RoomPropagateCycles),
|
||||
FPlatformTime::ToSeconds64(ClassifierStats.RoomExactPrimitiveCycles),
|
||||
FPlatformTime::ToSeconds64(ClassifierStats.RoomCacheWindowCycles),
|
||||
ClassifierStats.RoomNumRooms, ClassifierStats.RoomNumTunnels,
|
||||
ClassifierStats.RoomNumRoomFloorJoins, ClassifierStats.RoomNumPits,
|
||||
ClassifierStats.RoomNumChimneys,
|
||||
ClassifierStats.WholeMixedNodes, ClassifierStats.WholeSolidNodes,
|
||||
ClassifierStats.WholeAirNodes, ClassifierStats.NeedsFinalFieldNodes,
|
||||
ClassifierStats.SplitNodes, ClassifierStats.MaxRefinementDepth);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2175,7 +2203,8 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
|
||||
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
|
||||
const double ClassifyStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0;
|
||||
const EVoxelTileClass Verdict = Generator->ClassifyTile(OriginVoxels, Step, Cells);
|
||||
const EVoxelTileClass Verdict = Generator->ClassifyTile(
|
||||
OriginVoxels, Step, Cells, bProfileTile ? &ClassifierStats : nullptr);
|
||||
ClassifySeconds = bProfileTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0;
|
||||
ClassifyVerdict = static_cast<int32>(Verdict);
|
||||
if (ShouldAbortWork())
|
||||
|
||||
@@ -768,6 +768,14 @@ struct FChunkSDFCache
|
||||
// Allocator-backed bytes owned by this cache, including the nested tunnel chains. The returned
|
||||
// value excludes sizeof(FChunkSDFCache) itself so callers can add the enclosing entry's inline
|
||||
// storage exactly once.
|
||||
// Reset keeps the allocated backing storage and clears the immutable contents.
|
||||
// It is used when a classifier proves that the current search window has no
|
||||
// possible room/tunnel feature.
|
||||
void Reset();
|
||||
// Release all backing storage. Used when a worker has moved from a populated
|
||||
// classifier window to a feature-free one and the retained capacity is no
|
||||
// longer useful; this is intentionally separate from hot-path Reset().
|
||||
void Release();
|
||||
SIZE_T GetAllocatedSize() const;
|
||||
VoxelDensityProfile::FCacheMemoryBreakdown GetAllocatedSizeBreakdown() const;
|
||||
};
|
||||
@@ -830,6 +838,20 @@ namespace VoxelCaveMorphology
|
||||
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr
|
||||
);
|
||||
|
||||
// Conservative, geometry-free preflight for BuildChunkCache. It recreates
|
||||
// the deterministic room candidates and edge decisions, but does not build
|
||||
// player-fit points or tunnel control chains. False means that the same
|
||||
// cache window is guaranteed to contain no room, join, decoration, or
|
||||
// connected tunnel. Any malformed or unbounded input returns true.
|
||||
VOXELFORGE_API bool MayHaveFeatureInSearchBox(
|
||||
float SearchMinX, float SearchMinY,
|
||||
float SearchMaxX, float SearchMaxY,
|
||||
const FStrateGenerationParams& Params,
|
||||
uint32 Seed, int32 StrateIndex,
|
||||
bool bUseZ = false,
|
||||
float SearchMinZ = 0.0f, float SearchMaxZ = 0.0f
|
||||
);
|
||||
|
||||
// PHASE 2: Evaluate the SDF at a single world position using cached data.
|
||||
// Loops through cached rooms and tunnels, computes SDF primitives, applies
|
||||
// SmoothMin. Includes per-room/tunnel distance culling to skip primitives
|
||||
|
||||
@@ -130,6 +130,23 @@ public:
|
||||
|
||||
int32 Num() const { return Ops.Num(); }
|
||||
|
||||
/** True when this stack publishes the requested immutable geometry resource. */
|
||||
bool ProvidesResource(EVoxelOpResourceMask Resource) const
|
||||
{
|
||||
if (Resource == VoxelOpResources::None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const FOpEntry& Entry : Ops)
|
||||
{
|
||||
if ((Entry.ProvidedResources & Resource) == Resource)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocator-backed bytes owned by the stack, including each concrete operator allocation.
|
||||
// The enclosing cache entry adds sizeof(FVoxelOpStack) separately.
|
||||
SIZE_T GetAllocatedSize() const;
|
||||
@@ -186,25 +203,10 @@ public:
|
||||
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
|
||||
{
|
||||
FVoxelBoxHypotheses H;
|
||||
const bool bTask4OpProbe = Ctx.bUseLatticeProof
|
||||
&& Ctx.LatticeOriginVoxels == FIntVector(32, 0, -32)
|
||||
&& Ctx.Step == 1;
|
||||
for (const FOpEntry& Entry : Ops)
|
||||
{
|
||||
const uint64 Task4OpStartCycles = bTask4OpProbe
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx,
|
||||
(Entry.Writes & VoxelOpChannels::Sdf) != 0);
|
||||
if (bTask4OpProbe)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("[Task4OpProbe] box=(%.0f,%.0f,%.0f)-(%.0f,%.0f,%.0f) op=%s ms=%.3f"),
|
||||
(float)VoxelBox.Min.X, (float)VoxelBox.Min.Y, (float)VoxelBox.Min.Z,
|
||||
(float)VoxelBox.Max.X, (float)VoxelBox.Max.Y, (float)VoxelBox.Max.Z,
|
||||
Entry.Op->DebugName(),
|
||||
FPlatformTime::ToMilliseconds64(
|
||||
FPlatformTime::Cycles64() - Task4OpStartCycles));
|
||||
}
|
||||
// Do not early-out on a dead hypothesis: a later forcing structural post may
|
||||
// deliberately overwrite it (the XY edge seal is appended after passage tube/landing
|
||||
// carving and support).
|
||||
@@ -502,6 +504,20 @@ namespace VoxelDensityOps
|
||||
* resserrement autour du mauvais terme. */
|
||||
int32 HitRoomsNoWarp = 0, HitTunnelsNoWarp = 0;
|
||||
float WarpDilation = 0.0f;
|
||||
|
||||
/** Conservative candidate for the world-space tunnel-core post that runs after EvalMC.
|
||||
* This is derived from the final post's own world-chain bounds, not from the warped SDF
|
||||
* source's broader threshold hit count. */
|
||||
bool bMayHaveTunnelCoreAir = false;
|
||||
bool bMayHaveTunnelSupportFloor = false;
|
||||
bool bMayHaveTunnelCoreTail = false;
|
||||
uint32 ExactTailQueries = 0;
|
||||
uint32 ExactTailEvaluated = 0;
|
||||
uint64 ExactTailCycles = 0;
|
||||
uint64 PropagateCycles = 0;
|
||||
uint64 ExactPrimitiveCycles = 0;
|
||||
uint64 CacheWindowCycles = 0;
|
||||
int32 NumRoomFloorJoins = 0;
|
||||
};
|
||||
|
||||
VOXELFORGE_API FRoomBoxDiagnostic GetLastRoomBoxDiagnostic();
|
||||
@@ -509,6 +525,12 @@ namespace VoxelDensityOps
|
||||
/** Current worker-local room-graph cache footprint; profiling/diagnostic use only. */
|
||||
VOXELFORGE_API void ReportWorkerRoomGraphCacheFootprint();
|
||||
|
||||
/** Current process-wide immutable room-graph proof-cache footprint; diagnostic use only. */
|
||||
VOXELFORGE_API void ReportSharedRoomGraphCacheFootprint();
|
||||
|
||||
/** Drop completed proof caches from older manager lifetimes, or all of them when zero. */
|
||||
VOXELFORGE_API void TrimSharedRoomGraphCache(uint64 KeepManagerLifetimeId = 0);
|
||||
|
||||
/**
|
||||
* FloatingIslands — 8 ops, et **la pile tourne à l'ENVERS** :
|
||||
* ConstantVoid → IslandBlob → SdfRoughness → SdfFill → [structural post ×4]
|
||||
|
||||
@@ -340,7 +340,11 @@ public:
|
||||
* des gardes conservatrices. Worker-safe (lecture seule + caches thread_local partagés avec
|
||||
* GetDensityAt — un verdict Mixed laisse les colonnes chaudes pour la génération qui suit).
|
||||
*/
|
||||
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const;
|
||||
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step,
|
||||
int32 CellsPerAxis) const;
|
||||
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step,
|
||||
int32 CellsPerAxis,
|
||||
struct FVoxelTileClassificationStats* OutStats) const;
|
||||
|
||||
private:
|
||||
/** Identité process-unique du propriétaire des caches `CP_*` thread_local.
|
||||
@@ -380,3 +384,41 @@ private:
|
||||
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
|
||||
const FBiomeContext& Ctx, FChunkBiomeCache& Cache) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* verdict. Keeping the counters beside the classifier lets the game profile distinguish cache,
|
||||
* interval refinement, and exact-lattice work without sampling a different code path.
|
||||
*/
|
||||
struct FVoxelTileClassificationStats
|
||||
{
|
||||
uint32 RefineNodes = 0;
|
||||
uint32 StackBoxCalls = 0;
|
||||
uint32 WholeMixedNodes = 0;
|
||||
uint32 WholeSolidNodes = 0;
|
||||
uint32 WholeAirNodes = 0;
|
||||
uint32 NeedsFinalFieldNodes = 0;
|
||||
uint32 SplitNodes = 0;
|
||||
uint32 MaxRefinementDepth = 0;
|
||||
uint32 ExactCoreSamples = 0;
|
||||
uint32 ExactFinalSamples = 0;
|
||||
uint32 ExactCoreCacheHits = 0;
|
||||
uint32 ExactFinalCacheHits = 0;
|
||||
uint32 ExactCoreLeaves = 0;
|
||||
uint32 ExactFinalLeaves = 0;
|
||||
uint64 StackBoxCycles = 0;
|
||||
uint64 ExactCoreCycles = 0;
|
||||
uint64 ExactFinalCycles = 0;
|
||||
uint32 RoomTailQueries = 0;
|
||||
uint32 RoomTailEvaluated = 0;
|
||||
uint64 RoomTailCycles = 0;
|
||||
uint64 RoomPropagateCycles = 0;
|
||||
uint64 RoomExactPrimitiveCycles = 0;
|
||||
uint64 RoomCacheWindowCycles = 0;
|
||||
int32 RoomNumRooms = 0;
|
||||
int32 RoomNumTunnels = 0;
|
||||
int32 RoomNumRoomFloorJoins = 0;
|
||||
int32 RoomNumPits = 0;
|
||||
int32 RoomNumChimneys = 0;
|
||||
};
|
||||
@@ -427,8 +427,16 @@ public:
|
||||
* carve reaches a sampled point. Invalid geometry returns 1 conservatively. */
|
||||
float MaxPassageCarveFactorNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step) const;
|
||||
/** Exact conservative candidate for the post-stack passage air writers on the MC lattice.
|
||||
* This deliberately excludes the passage modifier already folded by FPassageCarveOp. */
|
||||
bool AnyPassageAirPostNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step,
|
||||
float BaseDensity, float SealThickness) const;
|
||||
bool AnyPassageLandingFloorNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step) const;
|
||||
/** Exact presence test for a structural floor write on the MC lattice. */
|
||||
bool AnyLandingFloorAtLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step) const;
|
||||
|
||||
/** Conservative box guards for the per-strate (0,0) landing rooms and their floor slabs. */
|
||||
bool AnyOriginLandingNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
|
||||
@@ -436,6 +444,10 @@ public:
|
||||
|
||||
bool AnyOriginLandingNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step) const;
|
||||
/** Exact conservative candidate for the post-disturbance origin-room air reassertion. */
|
||||
bool AnyOriginLandingAirNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step,
|
||||
float BaseDensity, float SealThickness) const;
|
||||
bool AnyOriginLandingFloorNearLattice(const FBox& VoxelBox,
|
||||
const FIntVector& LatticeOrigin, int32 Step) const;
|
||||
|
||||
|
||||
@@ -4940,6 +4940,10 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
|
||||
return 1;
|
||||
}
|
||||
|
||||
// A commandlet can run several fresh manager lifetimes in one process. Release completed
|
||||
// immutable proof caches from the preceding case before constructing this one.
|
||||
VoxelDensityOps::TrimSharedRoomGraphCache();
|
||||
|
||||
// Every case starts from a disabled, empty diagnostic state. The world/cache objects are
|
||||
// local to this invocation and their owner/lifetime keys prevent stale worker caches from
|
||||
// serving the next case's generation.
|
||||
@@ -5529,6 +5533,8 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
|
||||
static_cast<unsigned long long>(MesherAttributionCalls));
|
||||
VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Disabled);
|
||||
}
|
||||
VoxelDensityOps::ReportSharedRoomGraphCacheFootprint();
|
||||
VoxelDensityOps::TrimSharedRoomGraphCache();
|
||||
// Per-case isolation boundary. The UObject world dies with this function; worker-local
|
||||
// caches are keyed by the fresh generator/manager lifetime and the diagnostics are reset before
|
||||
// the next case. No case may inherit profiler counters or a live profiler mode.
|
||||
@@ -5861,6 +5867,12 @@ int32 RunBatch(const FString& Params)
|
||||
{
|
||||
++FailedCases;
|
||||
}
|
||||
|
||||
// RunExploreCase owns a complete transient UObject world. Release that case's strong
|
||||
// roots before starting the next one and collect now; otherwise a batch retains every
|
||||
// completed mesh/settings/manager graph until the commandlet exits, making peak RSS grow
|
||||
// with the number of cases even though no report needs those objects afterward.
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> BatchReport = MakeShared<FJsonObject>();
|
||||
|
||||
Reference in new issue
Block a user