perf(apply): stutter is NOT the game thread -- and the flat floor cannot be relaxed
Two things the owner reported from playing412a7ad. One is answered, one is a null result that Codex flagged itself rather than dressing up. THE FLATTENED TUNNEL FLOORS -- capability-safe, and worth almost nothing He has now flagged these twice: every tunnel carries a smooth flat strip running in and out through otherwise craggy rock. The idea was to make the floor CONDITIONAL -- assert it only where the natural rock fails walkability, leave the rock alone elsewhere. Cheaper and better looking, in theory. The floor is now skipped only when the exact no-floor field passes the same player-fit validator at foot offsets 0, -1, +1. Unknown or failed keeps the slab. natural-floor skips accepted 1,539 of 1,529,241 tests = 0.10% player-fit cells 20,153 -> 20,153 reachable cells 10,483 -> 10,483 arrival -> departure connected, both dihedral p50/p90 2.5429 / 33.8499 deg, IDENTICAL mesh time (their batch) 1.757135 -> 1.757140 s Capability preserved exactly. Visual complaint NOT solved, and no speed gained. ⭐ The useful part is what it PROVED by trying to go further. Every broader policy lost walkable space: analytic-only fit -217, reachable -195 tightened analytic fit -76, reachable -76 exact +/-4 foot fit -16, reachable -16 ⇒ The floor cannot be relaxed without changing the walkability contract or adding a route-aware global proof. That is a real constraint, now measured rather than assumed. Codex's own words, and it was right to lead with them: "the conservative floor change is capability-safe, but it does not visibly solve the flattened-strip complaint." THE STUTTER -- measured, and it is not where we thought Game-thread apply, meshed tiles, under -nullrhi: total apply p50 0.107 ms p95 0.153 ms max 0.273 ms stream upload/RMC p50 0.040 p95 0.088 max 0.124 component creation p50 0.019 p95 0.021 max 0.035 registration p50 0.010 p95 0.011 max 0.020 collision config p50 0.005 p95 0.007 max 0.019 A quarter of a millisecond at worst. The game-thread drain is NOT the hitch. ⇒ Remaining visible stutter is most likely render-thread/RHI work or asynchronous collision cooking behind RealtimeMeshComponent, which is off-limits for edits. Added anyway as a cheap safety valve: MaxMeshApplyMilliseconds, default 2 ms of drain budget per frame. Over 729 apply frames: drain p50/p95/max 0.111/0.182/0.313 ms, budget hits 0, at most 2 visible applies in a frame. It costs nothing today and caps a future burst. Request-to-ready, now timed after game-thread application: all p50 57.1 ms p95 432.9 ms max 919.4 ms -> 2.60 m at 6 m/s meshed p50 134.1 ms p95 457.4 ms max 919.4 ms ⚠️ My own verification: 1.197 / 1.131 / 1.118 s vs 1.088-1.102 s on412a7ad. Mean +4.7% -- below the 17% noise floor, but slower in all three runs, and the validator runs 1.5M times to accept 1,539 skips. Geometry moved by 4 triangles (153,740 -> 153,744), which is allowed. If the next round cannot make that validator earn its place, it should come out. Build 0 errors. Peak RSS 1.67 GB export / 1.90 GB game. Nothing outside Plugins/VoxelForge; RealtimeMeshComponent 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
412a7ad219
commit
383ec86c37
11 files changed
+508
-43
No files matched your search
@@ -35,6 +35,17 @@
|
|||||||
#include "VoxelStrateTypes.h"
|
#include "VoxelStrateTypes.h"
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
using FVFPlayerDensitySampler = FTunnelSupportFloorDensitySampler;
|
||||||
|
|
||||||
|
bool VF_ValidatePlayerFitPose(
|
||||||
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
|
const FVector& CandidateFeetPoint,
|
||||||
|
float StrateTopZ,
|
||||||
|
float StrateBottomZ,
|
||||||
|
float BoundarySealThickness,
|
||||||
|
FVFPlayerDensitySampler SampleDensity,
|
||||||
|
FVector& OutPoint);
|
||||||
|
|
||||||
}
|
}
|
||||||
#include "VoxelNoise.h" // Pure FBM used by the slab landing query
|
#include "VoxelNoise.h" // Pure FBM used by the slab landing query
|
||||||
#include "VoxelPassageGeometry.h"
|
#include "VoxelPassageGeometry.h"
|
||||||
@@ -72,6 +83,173 @@ namespace
|
|||||||
return DX * DX + DY * DY + DZ * DZ;
|
return DX * DX + DY * DY + DZ * DZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A graph tunnel's rounded capsule is a candidate natural floor only in the part of the tube
|
||||||
|
// where the player's complete support footprint remains inside a sufficiently wide,
|
||||||
|
// sufficiently flat cross-section. This analytic prefilter only saves density probes; it is
|
||||||
|
// never the decision by itself. The exact no-floor field is passed through the same player-fit
|
||||||
|
// validator used by the capability measurement, and failure retains the old support slab.
|
||||||
|
bool VF_IsNaturalTunnelFloorWalkable(
|
||||||
|
const FCachedTunnel& Tunnel,
|
||||||
|
float WorldX,
|
||||||
|
float WorldY,
|
||||||
|
float FloorZ,
|
||||||
|
float SupportRadius,
|
||||||
|
int32 SegmentIndex,
|
||||||
|
const FTunnelSupportFloorDensitySampler* NaturalSampler,
|
||||||
|
float StrateTopZ,
|
||||||
|
float StrateBottomZ,
|
||||||
|
float BoundarySealThickness)
|
||||||
|
{
|
||||||
|
if (SegmentIndex < 0
|
||||||
|
|| SegmentIndex + 1 >= Tunnel.WorldControlPoints.Num()
|
||||||
|
|| Tunnel.WorldControlRadii.Num() != Tunnel.WorldControlPoints.Num())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector& Start = Tunnel.WorldControlPoints[SegmentIndex];
|
||||||
|
const FVector& End = Tunnel.WorldControlPoints[SegmentIndex + 1];
|
||||||
|
const float StartRadius = FMath::Abs(Tunnel.WorldControlRadii[SegmentIndex]);
|
||||||
|
const float EndRadius = FMath::Abs(Tunnel.WorldControlRadii[SegmentIndex + 1]);
|
||||||
|
const FVector2D Delta(End.X - Start.X, End.Y - Start.Y);
|
||||||
|
const float LengthSquared = Delta.SizeSquared();
|
||||||
|
if (!(LengthSquared > KINDA_SMALL_NUMBER)
|
||||||
|
|| !FMath::IsFinite(StartRadius)
|
||||||
|
|| !FMath::IsFinite(EndRadius)
|
||||||
|
|| !FMath::IsFinite(FloorZ)
|
||||||
|
|| !FMath::IsFinite(SupportRadius))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector2D Query(WorldX, WorldY);
|
||||||
|
const float T = FMath::Clamp(
|
||||||
|
FVector2D::DotProduct(Query - FVector2D(Start.X, Start.Y), Delta)
|
||||||
|
/ LengthSquared,
|
||||||
|
0.0f, 1.0f);
|
||||||
|
const FVector2D Closest = FVector2D(Start.X, Start.Y) + Delta * T;
|
||||||
|
const float CentrelineDistance = (Query - Closest).Size();
|
||||||
|
const float TubeRadius = FMath::Lerp(StartRadius, EndRadius, T);
|
||||||
|
constexpr float CapsuleSafetyMargin = 0.25f;
|
||||||
|
// The first candidate was still too permissive: the analytic tube was walkable, but
|
||||||
|
// roughness outside the structural core could remove the last supporting voxels. Only
|
||||||
|
// the broad, nearly-centreline part of the widest tubes is a certificate strong enough to
|
||||||
|
// replace the authored floor; all marginal geometry keeps the slab.
|
||||||
|
constexpr float NaturalMinimumTubeRadius = 7.5f;
|
||||||
|
constexpr float NaturalCentrelineTolerance = 0.5f;
|
||||||
|
constexpr float NaturalWallMargin = 2.0f;
|
||||||
|
constexpr float NaturalMaximumAxialGradient = 0.12f;
|
||||||
|
const float PlayerRadius = VoxelPassageGeometry::PlayerRadiusVoxels;
|
||||||
|
const float FootprintRadius = PlayerRadius + CapsuleSafetyMargin;
|
||||||
|
if (TubeRadius < NaturalMinimumTubeRadius
|
||||||
|
|| CentrelineDistance > NaturalCentrelineTolerance
|
||||||
|
|| !(TubeRadius > 0.0f)
|
||||||
|
|| CentrelineDistance + FootprintRadius
|
||||||
|
> TubeRadius - NaturalWallMargin
|
||||||
|
|| SupportRadius + KINDA_SMALL_NUMBER < PlayerRadius)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float FarthestSupportRadius = CentrelineDistance + FootprintRadius;
|
||||||
|
const float CrossSectionHalfWidth = FMath::Sqrt(FMath::Max(
|
||||||
|
TubeRadius * TubeRadius
|
||||||
|
- FarthestSupportRadius * FarthestSupportRadius,
|
||||||
|
0.0f));
|
||||||
|
if (2.0f * CrossSectionHalfWidth
|
||||||
|
< VoxelPassageGeometry::PlayerHeightVoxels
|
||||||
|
+ 2.0f * CapsuleSafetyMargin)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The round floor's steepest support-patch slope is attained at its outer edge. Keep it
|
||||||
|
// no steeper than the authored 15-degree tunnel contract, rather than using a generous
|
||||||
|
// engine walkability ceiling merely to make the optimization fire more often.
|
||||||
|
const float RadialDenominator = FMath::Sqrt(FMath::Max(
|
||||||
|
TubeRadius * TubeRadius
|
||||||
|
- FarthestSupportRadius * FarthestSupportRadius,
|
||||||
|
KINDA_SMALL_NUMBER));
|
||||||
|
const float RadialGradient = FarthestSupportRadius / RadialDenominator;
|
||||||
|
const float AxialGradient = VoxelPassageGeometry::TunnelFloorGradient(
|
||||||
|
Start, StartRadius, End, EndRadius);
|
||||||
|
if (!FMath::IsFinite(RadialGradient)
|
||||||
|
|| !FMath::IsFinite(AxialGradient)
|
||||||
|
|| RadialGradient > VoxelPassageGeometry::WalkableTunnelMaxGradient
|
||||||
|
|| AxialGradient > NaturalMaximumAxialGradient)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject bends and segment endpoints where the projected support plane changes abruptly.
|
||||||
|
// The flat-floor path remains responsible for those transition columns.
|
||||||
|
constexpr float ProbeDistance = 1.0f;
|
||||||
|
const FVector ProbePositions[] = {
|
||||||
|
FVector(WorldX - ProbeDistance, WorldY, 0.0f),
|
||||||
|
FVector(WorldX + ProbeDistance, WorldY, 0.0f),
|
||||||
|
FVector(WorldX, WorldY - ProbeDistance, 0.0f),
|
||||||
|
FVector(WorldX, WorldY + ProbeDistance, 0.0f)
|
||||||
|
};
|
||||||
|
for (const FVector& Probe : ProbePositions)
|
||||||
|
{
|
||||||
|
float ProbeFloorZ = 0.0f;
|
||||||
|
float ProbeSupportRadius = 0.0f;
|
||||||
|
int32 ProbeSegmentIndex = INDEX_NONE;
|
||||||
|
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
|
||||||
|
Tunnel.WorldControlPoints,
|
||||||
|
Tunnel.WorldControlRadii,
|
||||||
|
Probe,
|
||||||
|
ProbeFloorZ,
|
||||||
|
ProbeSupportRadius,
|
||||||
|
&ProbeSegmentIndex)
|
||||||
|
|| ProbeSegmentIndex != SegmentIndex
|
||||||
|
|| FMath::Abs(ProbeFloorZ - FloorZ)
|
||||||
|
> VoxelPassageGeometry::WalkableTunnelMaxGradient
|
||||||
|
* ProbeDistance + CapsuleSafetyMargin)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NaturalSampler == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStrateMeasureSettings FitSettings;
|
||||||
|
const float MaxStepHeightVoxels =
|
||||||
|
FVoxelPlayerCapsuleConstants::MaxStepHeightMeters
|
||||||
|
/ FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||||
|
// The projected graph floor is the centreline's authored support level, but natural rock
|
||||||
|
// may leave a legal footing a little above or below it. Search a small fixed offset set so
|
||||||
|
// that the conditional path does not mistake a vertically displaced natural ledge for a
|
||||||
|
// failed walkability test. The validator still enforces the actual max-step and capsule
|
||||||
|
// clearance rules for every trial; no geometry is accepted by this search alone.
|
||||||
|
constexpr int32 FeetOffsetCount = 3;
|
||||||
|
const float FeetOffsets[FeetOffsetCount] = {
|
||||||
|
0.0f, -1.0f, 1.0f
|
||||||
|
};
|
||||||
|
for (int32 OffsetIndex = 0; OffsetIndex < FeetOffsetCount; ++OffsetIndex)
|
||||||
|
{
|
||||||
|
const FVector CandidateFeet(
|
||||||
|
WorldX, WorldY,
|
||||||
|
FloorZ + MaxStepHeightVoxels + 0.5f + FeetOffsets[OffsetIndex]);
|
||||||
|
FVector FitPoint = FVector::ZeroVector;
|
||||||
|
if (VF_ValidatePlayerFitPose(
|
||||||
|
FitSettings,
|
||||||
|
CandidateFeet,
|
||||||
|
StrateTopZ,
|
||||||
|
StrateBottomZ,
|
||||||
|
BoundarySealThickness,
|
||||||
|
*NaturalSampler,
|
||||||
|
FitPoint))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FChunkSDFCache::Reset()
|
void FChunkSDFCache::Reset()
|
||||||
@@ -903,8 +1081,6 @@ namespace
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
using FVFPlayerDensitySampler = TFunctionRef<float(float, float, float)>;
|
|
||||||
|
|
||||||
bool VF_GetPlayerFitInteriorBounds(
|
bool VF_GetPlayerFitInteriorBounds(
|
||||||
const FVoxelStrateMeasureSettings& Settings,
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
float StrateTopZ,
|
float StrateTopZ,
|
||||||
@@ -5114,7 +5290,8 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
|||||||
Tunnel.WorldControlRadii,
|
Tunnel.WorldControlRadii,
|
||||||
Pos,
|
Pos,
|
||||||
FloorZ,
|
FloorZ,
|
||||||
SupportRadius)
|
SupportRadius,
|
||||||
|
nullptr)
|
||||||
|| WorldZ > FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
|
|| WorldZ > FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
|
||||||
|| WorldZ < FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels)
|
|| WorldZ < FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels)
|
||||||
{
|
{
|
||||||
@@ -5150,7 +5327,11 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
|||||||
void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
||||||
float WorldX, float WorldY,
|
float WorldX, float WorldY,
|
||||||
const FChunkSDFCache& Cache,
|
const FChunkSDFCache& Cache,
|
||||||
FTunnelSupportFloorColumn& OutColumn)
|
FTunnelSupportFloorColumn& OutColumn,
|
||||||
|
const FTunnelSupportFloorDensitySampler* NaturalSampler,
|
||||||
|
float NaturalStrateTopZ,
|
||||||
|
float NaturalStrateBottomZ,
|
||||||
|
float NaturalBoundarySealThickness)
|
||||||
{
|
{
|
||||||
OutColumn.Reset();
|
OutColumn.Reset();
|
||||||
|
|
||||||
@@ -5183,16 +5364,36 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
|||||||
const FVector ColumnPosition(WorldX, WorldY, 0.0f);
|
const FVector ColumnPosition(WorldX, WorldY, 0.0f);
|
||||||
float FloorZ = 0.0f;
|
float FloorZ = 0.0f;
|
||||||
float SupportRadius = 0.0f;
|
float SupportRadius = 0.0f;
|
||||||
|
int32 SegmentIndex = INDEX_NONE;
|
||||||
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
|
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
|
||||||
Tunnel.WorldControlPoints,
|
Tunnel.WorldControlPoints,
|
||||||
Tunnel.WorldControlRadii,
|
Tunnel.WorldControlRadii,
|
||||||
ColumnPosition,
|
ColumnPosition,
|
||||||
FloorZ,
|
FloorZ,
|
||||||
SupportRadius))
|
SupportRadius,
|
||||||
|
&SegmentIndex))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (NaturalSampler != nullptr && VoxelDensityProfile::AreCountersEnabled())
|
||||||
|
{
|
||||||
|
VoxelDensityProfile::AddCounter(
|
||||||
|
VoxelDensityProfile::ECounter::TunnelSupportFloorNaturalTests);
|
||||||
|
}
|
||||||
|
if (VF_IsNaturalTunnelFloorWalkable(
|
||||||
|
Tunnel, WorldX, WorldY, FloorZ, SupportRadius, SegmentIndex,
|
||||||
|
NaturalSampler, NaturalStrateTopZ, NaturalStrateBottomZ,
|
||||||
|
NaturalBoundarySealThickness))
|
||||||
|
{
|
||||||
|
if (VoxelDensityProfile::AreCountersEnabled())
|
||||||
|
{
|
||||||
|
VoxelDensityProfile::AddCounter(
|
||||||
|
VoxelDensityProfile::ECounter::TunnelSupportFloorNaturalSkips);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
float MinZ = FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels;
|
float MinZ = FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels;
|
||||||
float MaxZ = FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
|
float MaxZ = FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
|
||||||
|
|
||||||
|
|||||||
@@ -3591,8 +3591,10 @@ namespace
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FRoomGraphSource(const FStrateGenerationParams& InP, int32 InSeed,
|
FRoomGraphSource(const FStrateGenerationParams& InP, int32 InSeed,
|
||||||
const UVoxelStrateManager* InManager)
|
const UVoxelStrateManager* InManager,
|
||||||
|
const UVoxelGenerator* InGenerator = nullptr)
|
||||||
: P(InP), Seed(InSeed), SeedU((uint32)InSeed), Manager(InManager)
|
: P(InP), Seed(InSeed), SeedU((uint32)InSeed), Manager(InManager)
|
||||||
|
, Generator(InGenerator)
|
||||||
, ManagerLifetimeId(InManager ? InManager->GetCacheLifetimeId() : 0)
|
, ManagerLifetimeId(InManager ? InManager->GetCacheLifetimeId() : 0)
|
||||||
, ParamsFingerprint(FCrc::MemCrc32(&InP, sizeof(InP)))
|
, ParamsFingerprint(FCrc::MemCrc32(&InP, sizeof(InP)))
|
||||||
{}
|
{}
|
||||||
@@ -3791,6 +3793,37 @@ namespace
|
|||||||
return P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f;
|
return P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BuildSupportColumn(
|
||||||
|
float WorldX, float WorldY,
|
||||||
|
const FChunkSDFCache& Cache,
|
||||||
|
FTunnelSupportFloorColumn& OutColumn) const
|
||||||
|
{
|
||||||
|
if (Generator == nullptr)
|
||||||
|
{
|
||||||
|
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
||||||
|
WorldX, WorldY, Cache, OutColumn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a synchronous, non-owning view of the exact field with only the graph
|
||||||
|
// support slab disabled. BuildTunnelSupportFloorColumn consumes it immediately and
|
||||||
|
// retains only the proven interval decision in the worker-local column cache.
|
||||||
|
const auto SampleWithoutTunnelSupportFloor =
|
||||||
|
[this](float X, float Y, float Z) -> float
|
||||||
|
{
|
||||||
|
return Generator->GetDensityWithParams(
|
||||||
|
X, Y, Z, P, ParamsFingerprint, LayoutVersion,
|
||||||
|
/*bApplyLegacyStructuralPosts=*/true,
|
||||||
|
/*bApplyTunnelSupportFloor=*/false);
|
||||||
|
};
|
||||||
|
FTunnelSupportFloorDensitySampler NaturalSampler =
|
||||||
|
SampleWithoutTunnelSupportFloor;
|
||||||
|
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
||||||
|
WorldX, WorldY, Cache, OutColumn, &NaturalSampler,
|
||||||
|
P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||||
|
P.BoundarySealThickness);
|
||||||
|
}
|
||||||
|
|
||||||
const FTunnelSupportFloorColumn* GetSupportColumn(const FVector& Position) const
|
const FTunnelSupportFloorColumn* GetSupportColumn(const FVector& Position) const
|
||||||
{
|
{
|
||||||
FState& S = State();
|
FState& S = State();
|
||||||
@@ -3809,7 +3842,7 @@ namespace
|
|||||||
IX, IY, &Cache, S.SupportColumns.Clock, bNewColumn);
|
IX, IY, &Cache, S.SupportColumns.Clock, bNewColumn);
|
||||||
if (bNewColumn)
|
if (bNewColumn)
|
||||||
{
|
{
|
||||||
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
BuildSupportColumn(
|
||||||
static_cast<float>(Position.X),
|
static_cast<float>(Position.X),
|
||||||
static_cast<float>(Position.Y),
|
static_cast<float>(Position.Y),
|
||||||
Cache,
|
Cache,
|
||||||
@@ -3818,7 +3851,7 @@ namespace
|
|||||||
return Column;
|
return Column;
|
||||||
}
|
}
|
||||||
|
|
||||||
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
BuildSupportColumn(
|
||||||
static_cast<float>(Position.X), static_cast<float>(Position.Y),
|
static_cast<float>(Position.X), static_cast<float>(Position.Y),
|
||||||
GetCache(), S.FractionalSupportColumn);
|
GetCache(), S.FractionalSupportColumn);
|
||||||
return &S.FractionalSupportColumn;
|
return &S.FractionalSupportColumn;
|
||||||
@@ -6633,6 +6666,7 @@ namespace
|
|||||||
int32 Seed;
|
int32 Seed;
|
||||||
uint32 SeedU;
|
uint32 SeedU;
|
||||||
TWeakObjectPtr<const UVoxelStrateManager> Manager;
|
TWeakObjectPtr<const UVoxelStrateManager> Manager;
|
||||||
|
const UVoxelGenerator* Generator = nullptr;
|
||||||
uint64 ManagerLifetimeId = 0;
|
uint64 ManagerLifetimeId = 0;
|
||||||
uint32 ParamsFingerprint;
|
uint32 ParamsFingerprint;
|
||||||
uint32 LayoutVersion = 0;
|
uint32 LayoutVersion = 0;
|
||||||
@@ -9408,7 +9442,8 @@ namespace VoxelDensityOps
|
|||||||
|
|
||||||
void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P,
|
void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P,
|
||||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
|
||||||
bool bAppendStructuralPosts)
|
bool bAppendStructuralPosts,
|
||||||
|
const UVoxelGenerator* Generator)
|
||||||
{
|
{
|
||||||
// ⚠️ ÉTAPES A + B + C1 — LA PILE EST COMPLÈTE POUR CET ARCHÉTYPE.
|
// ⚠️ ÉTAPES A + B + C1 — LA PILE EST COMPLÈTE POUR CET ARCHÉTYPE.
|
||||||
// Portés : échelle verticale, roc de base, warp, graphe de salles (+ pits + cheminées),
|
// Portés : échelle verticale, roc de base, warp, graphe de salles (+ pits + cheminées),
|
||||||
@@ -9452,7 +9487,8 @@ namespace VoxelDensityOps
|
|||||||
|
|
||||||
// La source de salles est retenue par pointeur non possédant : les terrasses re-interrogent
|
// La source de salles est retenue par pointeur non possédant : les terrasses re-interrogent
|
||||||
// son cache SDF en Z±1. Même motif que `FShaftFieldSource` → `FShaftLedgeMod`.
|
// son cache SDF en Z±1. Même motif que `FShaftFieldSource` → `FShaftLedgeMod`.
|
||||||
TUniquePtr<FRoomGraphSource> RoomSource = MakeUnique<FRoomGraphSource>(P, Seed, StrateManager);
|
TUniquePtr<FRoomGraphSource> RoomSource = MakeUnique<FRoomGraphSource>(
|
||||||
|
P, Seed, StrateManager, Generator);
|
||||||
const FRoomGraphSource* RoomPtr = RoomSource.Get();
|
const FRoomGraphSource* RoomPtr = RoomSource.Get();
|
||||||
|
|
||||||
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
|
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
|
||||||
|
|||||||
@@ -414,6 +414,8 @@ namespace VoxelDensityProfile
|
|||||||
case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates");
|
case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates");
|
||||||
case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries");
|
case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries");
|
||||||
case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks");
|
case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks");
|
||||||
|
case ECounter::TunnelSupportFloorNaturalTests:return TEXT("TunnelSupportFloorNaturalTests");
|
||||||
|
case ECounter::TunnelSupportFloorNaturalSkips:return TEXT("TunnelSupportFloorNaturalSkips");
|
||||||
case ECounter::PassageCandidates: return TEXT("PassageCandidates");
|
case ECounter::PassageCandidates: return TEXT("PassageCandidates");
|
||||||
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
|
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
|
||||||
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
|
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
|
||||||
|
|||||||
@@ -507,7 +507,11 @@ namespace
|
|||||||
|
|
||||||
static void BuildTunnelSupportColumnsForChunk(
|
static void BuildTunnelSupportColumnsForChunk(
|
||||||
FChunkSDFCache& Cache,
|
FChunkSDFCache& Cache,
|
||||||
const FIntVector& ChunkCoord);
|
const FIntVector& ChunkCoord,
|
||||||
|
const UVoxelGenerator* Generator,
|
||||||
|
const FStrateGenerationParams& Params,
|
||||||
|
uint32 ParamsFingerprint,
|
||||||
|
uint32 LayoutVersion);
|
||||||
|
|
||||||
static const FTunnelSupportFloorColumn* FindTunnelSupportColumn(
|
static const FTunnelSupportFloorColumn* FindTunnelSupportColumn(
|
||||||
const FChunkSDFCache& Cache,
|
const FChunkSDFCache& Cache,
|
||||||
@@ -603,6 +607,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PrepareTunnelCoreCache(
|
void PrepareTunnelCoreCache(
|
||||||
|
const UVoxelGenerator& Generator,
|
||||||
const UVoxelStrateManager& Manager,
|
const UVoxelStrateManager& Manager,
|
||||||
const FIntVector& ChunkCoord,
|
const FIntVector& ChunkCoord,
|
||||||
const FStrateGenerationParams& Params,
|
const FStrateGenerationParams& Params,
|
||||||
@@ -638,7 +643,9 @@ namespace
|
|||||||
ChunkMaxX + Expansion, ChunkMaxY + Expansion,
|
ChunkMaxX + Expansion, ChunkMaxY + Expansion,
|
||||||
Params, static_cast<uint32>(Manager.GetWorldSeed()),
|
Params, static_cast<uint32>(Manager.GetWorldSeed()),
|
||||||
StrateIndex, nullptr);
|
StrateIndex, nullptr);
|
||||||
BuildTunnelSupportColumnsForChunk(OutState.Cache, ChunkCoord);
|
BuildTunnelSupportColumnsForChunk(
|
||||||
|
OutState.Cache, ChunkCoord, &Generator, Params,
|
||||||
|
ParamsFingerprint, LayoutVersion);
|
||||||
OutState.OwnerId = OwnerId;
|
OutState.OwnerId = OwnerId;
|
||||||
OutState.ManagerLifetimeId = ManagerLifetimeId;
|
OutState.ManagerLifetimeId = ManagerLifetimeId;
|
||||||
OutState.Chunk = ChunkCoord;
|
OutState.Chunk = ChunkCoord;
|
||||||
@@ -649,7 +656,11 @@ namespace
|
|||||||
|
|
||||||
static void BuildTunnelSupportColumnsForChunk(
|
static void BuildTunnelSupportColumnsForChunk(
|
||||||
FChunkSDFCache& Cache,
|
FChunkSDFCache& Cache,
|
||||||
const FIntVector& ChunkCoord)
|
const FIntVector& ChunkCoord,
|
||||||
|
const UVoxelGenerator* Generator,
|
||||||
|
const FStrateGenerationParams& Params,
|
||||||
|
uint32 ParamsFingerprint,
|
||||||
|
uint32 LayoutVersion)
|
||||||
{
|
{
|
||||||
const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1;
|
const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1;
|
||||||
const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1;
|
const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1;
|
||||||
@@ -661,6 +672,25 @@ namespace
|
|||||||
Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells);
|
Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells);
|
||||||
Cache.SupportColumns.Reset();
|
Cache.SupportColumns.Reset();
|
||||||
|
|
||||||
|
// The conditional floor is tested against the exact density pipeline with only the
|
||||||
|
// graph support slab disabled. The sampler is synchronous and lives only for this cache
|
||||||
|
// build; the immutable cache stores the resulting interval decision, never the callback.
|
||||||
|
const auto SampleWithoutTunnelSupportFloor =
|
||||||
|
[Generator, &Params, ParamsFingerprint, LayoutVersion](
|
||||||
|
float X, float Y, float Z) -> float
|
||||||
|
{
|
||||||
|
return Generator != nullptr
|
||||||
|
? Generator->GetDensityWithParams(
|
||||||
|
X, Y, Z, Params, ParamsFingerprint, LayoutVersion,
|
||||||
|
/*bApplyLegacyStructuralPosts=*/true,
|
||||||
|
/*bApplyTunnelSupportFloor=*/false)
|
||||||
|
: 0.0f;
|
||||||
|
};
|
||||||
|
FTunnelSupportFloorDensitySampler NaturalSamplerRef =
|
||||||
|
SampleWithoutTunnelSupportFloor;
|
||||||
|
const FTunnelSupportFloorDensitySampler* NaturalSampler =
|
||||||
|
Generator != nullptr ? &NaturalSamplerRef : nullptr;
|
||||||
|
|
||||||
// Empty columns are represented only by INDEX_NONE in the slot map, so
|
// Empty columns are represented only by INDEX_NONE in the slot map, so
|
||||||
// each stored column retains the inline bands for actual graph tubes.
|
// each stored column retains the inline bands for actual graph tubes.
|
||||||
for (int32 Y = 0; Y < Cells; ++Y)
|
for (int32 Y = 0; Y < Cells; ++Y)
|
||||||
@@ -672,7 +702,11 @@ namespace
|
|||||||
static_cast<float>(MinX + X),
|
static_cast<float>(MinX + X),
|
||||||
static_cast<float>(MinY + Y),
|
static_cast<float>(MinY + Y),
|
||||||
Cache,
|
Cache,
|
||||||
Column);
|
Column,
|
||||||
|
NaturalSampler,
|
||||||
|
Params.StrateTopWorldZ,
|
||||||
|
Params.StrateBottomWorldZ,
|
||||||
|
Params.BoundarySealThickness);
|
||||||
if (Column.Intervals.Num() == 0)
|
if (Column.Intervals.Num() == 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -736,7 +770,8 @@ namespace
|
|||||||
*/
|
*/
|
||||||
bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs,
|
bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs,
|
||||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* SM,
|
int32 Seed, float SpineRadius, const UVoxelStrateManager* SM,
|
||||||
FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx)
|
FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx,
|
||||||
|
const UVoxelGenerator* Generator = nullptr)
|
||||||
{
|
{
|
||||||
switch (Type)
|
switch (Type)
|
||||||
{
|
{
|
||||||
@@ -793,7 +828,9 @@ namespace
|
|||||||
if (!Refs.Tunnel) { return false; }
|
if (!Refs.Tunnel) { return false; }
|
||||||
OutCtx.StrateTopWorldZ = Refs.Tunnel->StrateTopWorldZ;
|
OutCtx.StrateTopWorldZ = Refs.Tunnel->StrateTopWorldZ;
|
||||||
OutCtx.StrateBottomWorldZ = Refs.Tunnel->StrateBottomWorldZ;
|
OutCtx.StrateBottomWorldZ = Refs.Tunnel->StrateBottomWorldZ;
|
||||||
VoxelDensityOps::BuildTunnelNetworkStack(OutStack, *Refs.Tunnel, Seed, SpineRadius, SM);
|
VoxelDensityOps::BuildTunnelNetworkStack(
|
||||||
|
OutStack, *Refs.Tunnel, Seed, SpineRadius, SM,
|
||||||
|
/*bAppendStructuralPosts=*/true, Generator);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1259,7 +1296,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
|
|
||||||
CP_UseOpStack = VF_BuildOpStackForChunk(
|
CP_UseOpStack = VF_BuildOpStackForChunk(
|
||||||
CP_GenType, Refs, Seed, OriginSpineRadius,
|
CP_GenType, Refs, Seed, OriginSpineRadius,
|
||||||
StrateManager, CP_OpStack, OpCtx);
|
StrateManager, CP_OpStack, OpCtx, this);
|
||||||
|
|
||||||
// Le test appelle PrepareChunk, pas la production : c'est exactement la divergence
|
// Le test appelle PrepareChunk, pas la production : c'est exactement la divergence
|
||||||
// qui rend un opérateur vert en test et faux en jeu. Les sept `PrepareChunk`
|
// qui rend un opérateur vert en test et faux en jeu. Les sept `PrepareChunk`
|
||||||
@@ -1293,7 +1330,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
if (bCanPrepareNativeTunnelCore)
|
if (bCanPrepareNativeTunnelCore)
|
||||||
{
|
{
|
||||||
PrepareTunnelCoreCache(
|
PrepareTunnelCoreCache(
|
||||||
*StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
|
*this, *StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
|
||||||
LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache);
|
LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1647,7 +1684,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
|
float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
|
||||||
const FStrateGenerationParams& Params,
|
const FStrateGenerationParams& Params,
|
||||||
uint32 ParamsFingerprint, uint32 LayoutVersion,
|
uint32 ParamsFingerprint, uint32 LayoutVersion,
|
||||||
bool bApplyLegacyStructuralPosts) const
|
bool bApplyLegacyStructuralPosts,
|
||||||
|
bool bApplyTunnelSupportFloor) const
|
||||||
{
|
{
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// STRATE DENSITY FUNCTION (Morphology Pipeline)
|
// STRATE DENSITY FUNCTION (Morphology Pipeline)
|
||||||
@@ -2713,8 +2751,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
// the graph's own tunnel floor/core are structural route geometry. Direct callers of
|
// the graph's own tunnel floor/core are structural route geometry. Direct callers of
|
||||||
// GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the
|
// GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the
|
||||||
// same predicates once in its common MC-space post section below both generation paths.
|
// same predicates once in its common MC-space post section below both generation paths.
|
||||||
const bool bTunnelSupportFloor =
|
const bool bTunnelSupportFloor = bApplyTunnelSupportFloor
|
||||||
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
&& VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||||
WorldX, WorldY, WorldZ, SDFCache);
|
WorldX, WorldY, WorldZ, SDFCache);
|
||||||
if (bTunnelSupportFloor)
|
if (bTunnelSupportFloor)
|
||||||
{
|
{
|
||||||
@@ -4753,7 +4791,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
|||||||
|
|
||||||
FVoxelOpStack TileStack;
|
FVoxelOpStack TileStack;
|
||||||
if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius,
|
if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius,
|
||||||
StrateManager, TileStack, OpCtx))
|
StrateManager, TileStack, OpCtx, this))
|
||||||
{
|
{
|
||||||
// Strate dégénérée ou archétype non porté : `GetDensityAt` retomberait sur le `switch`,
|
// Strate dégénérée ou archétype non porté : `GetDensityAt` retomberait sur le `switch`,
|
||||||
// donc la pile ne décrit pas ce que le mesher verra. Aucun verdict.
|
// donc la pile ne décrit pas ce que le mesher verra. Aucun verdict.
|
||||||
|
|||||||
@@ -1172,11 +1172,22 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
// This prevents stutters from applying too many meshes in one frame.
|
// This prevents stutters from applying too many meshes in one frame.
|
||||||
// Budget limits how many VISIBLE mesh applies we do per frame (GPU upload cost).
|
// Budget limits how many VISIBLE mesh applies we do per frame (GPU upload cost).
|
||||||
// Empty meshes and stale results are free to drain — don't count them.
|
// Empty meshes and stale results are free to drain — don't count them.
|
||||||
const int32 MaxApplies = Settings ? Settings->MaxMeshAppliesPerFrame : 4;
|
const bool bProfileApply = GVoxelForgeProfileTileGeneration != 0;
|
||||||
|
const uint64 DrainStartCycles = FPlatformTime::Cycles64();
|
||||||
|
const int32 MaxApplies = FMath::Max(
|
||||||
|
Settings ? Settings->MaxMeshAppliesPerFrame : 4, 1);
|
||||||
|
const float ApplyBudgetMilliseconds = Settings
|
||||||
|
? FMath::Max(Settings->MaxMeshApplyMilliseconds, 0.0f)
|
||||||
|
: 2.0f;
|
||||||
|
const double ApplyBudgetSeconds =
|
||||||
|
static_cast<double>(ApplyBudgetMilliseconds) * 0.001;
|
||||||
int32 MeshesApplied = 0;
|
int32 MeshesApplied = 0;
|
||||||
|
int32 ResultsDrained = 0;
|
||||||
|
bool bTimeBudgetHit = false;
|
||||||
FChunkResult DequeuedChunk;
|
FChunkResult DequeuedChunk;
|
||||||
while (ProcessQueue.Dequeue(DequeuedChunk))
|
while (ProcessQueue.Dequeue(DequeuedChunk))
|
||||||
{
|
{
|
||||||
|
++ResultsDrained;
|
||||||
PendingTiles.Remove(DequeuedChunk.Tile);
|
PendingTiles.Remove(DequeuedChunk.Tile);
|
||||||
|
|
||||||
// A worker can observe shutdown after entering the mesher. Do not turn that partial
|
// A worker can observe shutdown after entering the mesher. Do not turn that partial
|
||||||
@@ -1190,13 +1201,37 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
// Only a real (visible) upload counts against the per-frame budget — stale/empty drain free.
|
// Only a real (visible) upload counts against the per-frame budget — stale/empty drain free.
|
||||||
if (ApplyTileResult(DequeuedChunk))
|
if (ApplyTileResult(DequeuedChunk))
|
||||||
{
|
{
|
||||||
if (++MeshesApplied >= MaxApplies)
|
++MeshesApplied;
|
||||||
{
|
}
|
||||||
break;
|
|
||||||
}
|
// Time-box the entire game-thread drain, not only visible uploads. Empty releases and
|
||||||
|
// stale-result cleanup normally cost nothing, but a pooled component teardown or a
|
||||||
|
// backend callback can still be the one result that produces the player's hitch.
|
||||||
|
if (ApplyBudgetSeconds > 0.0
|
||||||
|
&& FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DrainStartCycles)
|
||||||
|
>= ApplyBudgetSeconds)
|
||||||
|
{
|
||||||
|
bTimeBudgetHit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (MeshesApplied >= MaxApplies)
|
||||||
|
{
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bProfileApply && ResultsDrained > 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeApplyFrame] drained=%d meshes=%d elapsed=%.6f "
|
||||||
|
"max_meshes=%d budget_ms=%.3f budget_hit=%d"),
|
||||||
|
ResultsDrained,
|
||||||
|
MeshesApplied,
|
||||||
|
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DrainStartCycles),
|
||||||
|
MaxApplies,
|
||||||
|
ApplyBudgetMilliseconds,
|
||||||
|
bTimeBudgetHit ? 1 : 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Game-thread apply for one gen result. Shared by ProcessPendingChunks (async drain) and
|
// Game-thread apply for one gen result. Shared by ProcessPendingChunks (async drain) and
|
||||||
@@ -1209,13 +1244,21 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GVoxelForgeProfileTileGeneration != 0 && Result.RequestStartCycles != 0)
|
// "Ready" is deliberately emitted after the game-thread apply, not when the worker result is
|
||||||
|
// dequeued. This makes request-to-ready include component setup, stream/buffer submission,
|
||||||
|
// and collision configuration—the point at which the chunk is actually visible/collidable.
|
||||||
|
const bool bResultEmpty = Result.bEmpty || !Result.Streams;
|
||||||
|
const auto LogTileReady = [&]()
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Display,
|
if (GVoxelForgeProfileTileGeneration != 0 && Result.RequestStartCycles != 0)
|
||||||
TEXT("[VoxelForgeTileReady] level=%d empty=%d request_to_ready=%.6f"),
|
{
|
||||||
Result.Tile.Level, Result.bEmpty || !Result.Streams ? 1 : 0,
|
UE_LOG(LogTemp, Display,
|
||||||
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - Result.RequestStartCycles));
|
TEXT("[VoxelForgeTileReady] level=%d empty=%d request_to_ready=%.6f"),
|
||||||
}
|
Result.Tile.Level, bResultEmpty ? 1 : 0,
|
||||||
|
FPlatformTime::ToSeconds64(
|
||||||
|
FPlatformTime::Cycles64() - Result.RequestStartCycles));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Mark the tile loaded (even if empty — so we don't re-submit it).
|
// Mark the tile loaded (even if empty — so we don't re-submit it).
|
||||||
LoadedTiles.Add(Result.Tile);
|
LoadedTiles.Add(Result.Tile);
|
||||||
@@ -1240,17 +1283,35 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
|||||||
// bande déplacée hors de la tuile, ou skip cellule-plus-haute-que-la-bande après un
|
// bande déplacée hors de la tuile, ou skip cellule-plus-haute-que-la-bande après un
|
||||||
// changement de strate (LoadTile). L'ancien composant doit tomber, sinon sa vieille
|
// changement de strate (LoadTile). L'ancien composant doit tomber, sinon sa vieille
|
||||||
// géométrie (l'autre strate !) reste affichée. Première gen vide : Find rate, no-op.
|
// géométrie (l'autre strate !) reste affichée. Première gen vide : Find rate, no-op.
|
||||||
|
const uint64 EmptyReleaseStartCycles = GVoxelForgeProfileTileGeneration != 0
|
||||||
|
? FPlatformTime::Cycles64() : 0;
|
||||||
|
bool bReleasedComponent = false;
|
||||||
if (URealtimeMeshComponent** OldComp = TileComponents.Find(Result.Tile))
|
if (URealtimeMeshComponent** OldComp = TileComponents.Find(Result.Tile))
|
||||||
{
|
{
|
||||||
if (*OldComp) { ReleaseTileComponent(*OldComp); }
|
if (*OldComp)
|
||||||
|
{
|
||||||
|
ReleaseTileComponent(*OldComp);
|
||||||
|
bReleasedComponent = true;
|
||||||
|
}
|
||||||
TileComponents.Remove(Result.Tile);
|
TileComponents.Remove(Result.Tile);
|
||||||
}
|
}
|
||||||
|
if (GVoxelForgeProfileTileGeneration != 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeApplyProfile] level=%d empty=1 component_release=%d total=%.6f"),
|
||||||
|
Result.Tile.Level,
|
||||||
|
bReleasedComponent ? 1 : 0,
|
||||||
|
FPlatformTime::ToSeconds64(
|
||||||
|
FPlatformTime::Cycles64() - EmptyReleaseStartCycles));
|
||||||
|
}
|
||||||
|
LogTileReady();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply mesh (GPU upload). The vertex/index buffers were already built (T1.f, on the worker for
|
// Apply mesh (GPU upload). The vertex/index buffers were already built (T1.f, on the worker for
|
||||||
// the async path or inline for the sync carve path); the game thread only uploads them here.
|
// the async path or inline for the sync carve path); the game thread only uploads them here.
|
||||||
ApplyMeshToTile(Result);
|
ApplyMeshToTile(Result);
|
||||||
|
LogTileReady();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2291,8 +2352,15 @@ static FRealtimeMeshSectionGroupKey VoxelTileGroupKey()
|
|||||||
//=============================================================================
|
//=============================================================================
|
||||||
// Recycler les composants de tuile au lieu de les détruire/recréer.
|
// Recycler les composants de tuile au lieu de les détruire/recréer.
|
||||||
|
|
||||||
URealtimeMeshComponent* AVoxelWorld::AcquireTileComponent()
|
URealtimeMeshComponent* AVoxelWorld::AcquireTileComponent(
|
||||||
|
double* OutCreationSeconds,
|
||||||
|
double* OutRegistrationSeconds,
|
||||||
|
bool* bOutCreated)
|
||||||
{
|
{
|
||||||
|
if (OutCreationSeconds != nullptr) { *OutCreationSeconds = 0.0; }
|
||||||
|
if (OutRegistrationSeconds != nullptr) { *OutRegistrationSeconds = 0.0; }
|
||||||
|
if (bOutCreated != nullptr) { *bOutCreated = false; }
|
||||||
|
|
||||||
// Reuse a parked component when one is available — skips NewObject + RegisterComponent
|
// Reuse a parked component when one is available — skips NewObject + RegisterComponent
|
||||||
// (and the full proxy teardown/GC of a destroy) during fast travel & regen bursts.
|
// (and the full proxy teardown/GC of a destroy) during fast travel & regen bursts.
|
||||||
while (TileComponentPool.Num() > 0)
|
while (TileComponentPool.Num() > 0)
|
||||||
@@ -2305,6 +2373,10 @@ URealtimeMeshComponent* AVoxelWorld::AcquireTileComponent()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool bMeasureComponentPhases = OutCreationSeconds != nullptr
|
||||||
|
|| OutRegistrationSeconds != nullptr;
|
||||||
|
const uint64 CreationStartCycles = bMeasureComponentPhases
|
||||||
|
? FPlatformTime::Cycles64() : 0;
|
||||||
URealtimeMeshComponent* MeshComp = NewObject<URealtimeMeshComponent>(this);
|
URealtimeMeshComponent* MeshComp = NewObject<URealtimeMeshComponent>(this);
|
||||||
// Generated once, never moves → Static so RMC's cached static draw path + VSM shadow
|
// Generated once, never moves → Static so RMC's cached static draw path + VSM shadow
|
||||||
// caching apply (see the root SetMobility note in BeginPlay). Must be set before register.
|
// caching apply (see the root SetMobility note in BeginPlay). Must be set before register.
|
||||||
@@ -2313,8 +2385,21 @@ URealtimeMeshComponent* AVoxelWorld::AcquireTileComponent()
|
|||||||
MeshComp->SetMobility(EComponentMobility::Static);
|
MeshComp->SetMobility(EComponentMobility::Static);
|
||||||
MeshComp->SetGenerateOverlapEvents(false); // chunks use raycasts, not overlaps
|
MeshComp->SetGenerateOverlapEvents(false); // chunks use raycasts, not overlaps
|
||||||
MeshComp->SetCanEverAffectNavigation(false);
|
MeshComp->SetCanEverAffectNavigation(false);
|
||||||
|
if (OutCreationSeconds != nullptr)
|
||||||
|
{
|
||||||
|
*OutCreationSeconds = FPlatformTime::ToSeconds64(
|
||||||
|
FPlatformTime::Cycles64() - CreationStartCycles);
|
||||||
|
}
|
||||||
|
const uint64 RegistrationStartCycles = bMeasureComponentPhases
|
||||||
|
? FPlatformTime::Cycles64() : 0;
|
||||||
MeshComp->RegisterComponent();
|
MeshComp->RegisterComponent();
|
||||||
MeshComp->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
|
MeshComp->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
|
||||||
|
if (OutRegistrationSeconds != nullptr)
|
||||||
|
{
|
||||||
|
*OutRegistrationSeconds = FPlatformTime::ToSeconds64(
|
||||||
|
FPlatformTime::Cycles64() - RegistrationStartCycles);
|
||||||
|
}
|
||||||
|
if (bOutCreated != nullptr) { *bOutCreated = true; }
|
||||||
return MeshComp;
|
return MeshComp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2358,6 +2443,9 @@ void AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
{
|
{
|
||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ApplyMeshToChunk);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ApplyMeshToChunk);
|
||||||
|
|
||||||
|
const bool bProfileApply = GVoxelForgeProfileTileGeneration != 0;
|
||||||
|
const uint64 ApplyStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||||
|
|
||||||
// Streams are pre-built on the worker (T1.f) and guaranteed non-empty by the caller
|
// Streams are pre-built on the worker (T1.f) and guaranteed non-empty by the caller
|
||||||
// (ProcessPendingChunks skips empty tiles). This path is game-thread-CHEAP: material lookup +
|
// (ProcessPendingChunks skips empty tiles). This path is game-thread-CHEAP: material lookup +
|
||||||
// component get/create + the upload + per-section config. No per-vertex work here.
|
// component get/create + the upload + per-section config. No per-vertex work here.
|
||||||
@@ -2414,6 +2502,9 @@ void AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
if (UMaterialInstanceDynamic* MID = GetOrCreateTerrainMID(GroundMaterial)) { GroundMaterial = MID; }
|
if (UMaterialInstanceDynamic* MID = GetOrCreateTerrainMID(GroundMaterial)) { GroundMaterial = MID; }
|
||||||
if (UMaterialInstanceDynamic* MID = GetOrCreateTerrainMID(CeilingMaterial)) { CeilingMaterial = MID; }
|
if (UMaterialInstanceDynamic* MID = GetOrCreateTerrainMID(CeilingMaterial)) { CeilingMaterial = MID; }
|
||||||
}
|
}
|
||||||
|
const double MaterialSeconds = bProfileApply
|
||||||
|
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ApplyStartCycles)
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
// The geometry stream set was built on the worker (BuildTileStreamSet, T1.f); we just upload it.
|
// The geometry stream set was built on the worker (BuildTileStreamSet, T1.f); we just upload it.
|
||||||
// Vertices are world-space; the component sits at the actor origin.
|
// Vertices are world-space; the component sits at the actor origin.
|
||||||
@@ -2422,11 +2513,22 @@ void AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
// cheap on the game thread (no batching needed). Collision + content are level-0 only.
|
// cheap on the game thread (no batching needed). Collision + content are level-0 only.
|
||||||
// T2.c: the component comes from the pool when one is parked (see AcquireTileComponent).
|
// T2.c: the component comes from the pool when one is parked (see AcquireTileComponent).
|
||||||
URealtimeMeshComponent* MeshComp = TileComponents.FindRef(Tile);
|
URealtimeMeshComponent* MeshComp = TileComponents.FindRef(Tile);
|
||||||
|
const bool bComponentAlreadyPresent = MeshComp != nullptr;
|
||||||
|
const uint64 ComponentStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||||
|
double ComponentCreationSeconds = 0.0;
|
||||||
|
double ComponentRegistrationSeconds = 0.0;
|
||||||
|
bool bComponentCreated = false;
|
||||||
if (!MeshComp)
|
if (!MeshComp)
|
||||||
{
|
{
|
||||||
MeshComp = AcquireTileComponent();
|
MeshComp = AcquireTileComponent(
|
||||||
|
bProfileApply ? &ComponentCreationSeconds : nullptr,
|
||||||
|
bProfileApply ? &ComponentRegistrationSeconds : nullptr,
|
||||||
|
bProfileApply ? &bComponentCreated : nullptr);
|
||||||
TileComponents.Add(Tile, MeshComp);
|
TileComponents.Add(Tile, MeshComp);
|
||||||
}
|
}
|
||||||
|
const double ComponentSeconds = bProfileApply
|
||||||
|
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ComponentStartCycles)
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
// §9.4 RENDER-SKIP — a tile only a CollisionOnly anchor wants (not the player clipmap) cooks its
|
// §9.4 RENDER-SKIP — a tile only a CollisionOnly anchor wants (not the player clipmap) cooks its
|
||||||
// collision below but is hidden (no draw / VSM). Set every apply (overrides the pool's default-
|
// collision below but is hidden (no draw / VSM). Set every apply (overrides the pool's default-
|
||||||
@@ -2438,8 +2540,22 @@ void AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
// calling it unconditionally (as before) orphaned one mesh object per re-apply to the GC.
|
// calling it unconditionally (as before) orphaned one mesh object per re-apply to the GC.
|
||||||
// The RemoveSectionGroup below does the actual geometry clearing on reuse.
|
// The RemoveSectionGroup below does the actual geometry clearing on reuse.
|
||||||
URealtimeMeshSimple* RTMesh = MeshComp->GetRealtimeMeshAs<URealtimeMeshSimple>();
|
URealtimeMeshSimple* RTMesh = MeshComp->GetRealtimeMeshAs<URealtimeMeshSimple>();
|
||||||
|
const uint64 MeshInitStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||||
if (!RTMesh) { RTMesh = MeshComp->InitializeRealtimeMesh<URealtimeMeshSimple>(); }
|
if (!RTMesh) { RTMesh = MeshComp->InitializeRealtimeMesh<URealtimeMeshSimple>(); }
|
||||||
if (!RTMesh) { return; }
|
const double MeshInitSeconds = bProfileApply
|
||||||
|
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - MeshInitStartCycles)
|
||||||
|
: 0.0;
|
||||||
|
if (!RTMesh)
|
||||||
|
{
|
||||||
|
if (bProfileApply)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[VoxelForgeApplyProfile] level=%d empty=0 failed_mesh_init=1 total=%.6f"),
|
||||||
|
Tile.Level,
|
||||||
|
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ApplyStartCycles));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Shadow casting: far (level >= 2) tiles never cast; the sky-cap SECTION never casts either
|
// Shadow casting: far (level >= 2) tiles never cast; the sky-cap SECTION never casts either
|
||||||
// — otherwise the high rock ceiling shadows the entire terrain below it. F17: shadow is now
|
// — otherwise the high rock ceiling shadows the entire terrain below it. F17: shadow is now
|
||||||
// PER SECTION, so a mixed tile keeps its ground shadow while its cap stays shadowless.
|
// PER SECTION, so a mixed tile keeps its ground shadow while its cap stays shadowless.
|
||||||
@@ -2447,17 +2563,33 @@ void AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
MeshComp->SetCastShadow(bCastShadow);
|
MeshComp->SetCastShadow(bCastShadow);
|
||||||
|
|
||||||
const FRealtimeMeshSectionGroupKey GroupKey = VoxelTileGroupKey();
|
const FRealtimeMeshSectionGroupKey GroupKey = VoxelTileGroupKey();
|
||||||
|
const uint64 RemoveGroupStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||||
RTMesh->RemoveSectionGroup(GroupKey); // clear old geometry on re-mesh
|
RTMesh->RemoveSectionGroup(GroupKey); // clear old geometry on re-mesh
|
||||||
// (no-op on a fresh/pooled mesh)
|
// (no-op on a fresh/pooled mesh)
|
||||||
|
const double RemoveGroupSeconds = bProfileApply
|
||||||
|
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - RemoveGroupStartCycles)
|
||||||
|
: 0.0;
|
||||||
|
const uint64 MaterialSlotStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||||
RTMesh->SetupMaterialSlot(0, "Main", GroundMaterial);
|
RTMesh->SetupMaterialSlot(0, "Main", GroundMaterial);
|
||||||
RTMesh->SetupMaterialSlot(1, "SkyCap", CeilingMaterial);
|
RTMesh->SetupMaterialSlot(1, "SkyCap", CeilingMaterial);
|
||||||
|
const double MaterialSlotSeconds = bProfileApply
|
||||||
|
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - MaterialSlotStartCycles)
|
||||||
|
: 0.0;
|
||||||
|
// RMC owns both the CPU stream-to-buffer conversion and the render-resource upload behind
|
||||||
|
// CreateSectionGroup. Keep this as one phase: it is the exact plugin/RMC boundary we can
|
||||||
|
// budget without modifying RealtimeMeshComponent.
|
||||||
|
const uint64 StreamUploadStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||||
RTMesh->CreateSectionGroup(GroupKey, MoveTemp(Streams));
|
RTMesh->CreateSectionGroup(GroupKey, MoveTemp(Streams));
|
||||||
|
const double StreamUploadSeconds = bProfileApply
|
||||||
|
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - StreamUploadStartCycles)
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
// RMC casts shadows PER SECTION (FRealtimeMeshSectionConfig::bCastsShadow, default true) — the
|
// RMC casts shadows PER SECTION (FRealtimeMeshSectionConfig::bCastsShadow, default true) — the
|
||||||
// component-level UPrimitiveComponent::CastShadow is NOT honored by the RMC proxy, so the real
|
// component-level UPrimitiveComponent::CastShadow is NOT honored by the RMC proxy, so the real
|
||||||
// shadow lever is the section flag. RMC auto-created one section per non-empty polygroup above
|
// shadow lever is the section flag. RMC auto-created one section per non-empty polygroup above
|
||||||
// (default config already maps material slot = polygroup index); only config sections that
|
// (default config already maps material slot = polygroup index); only config sections that
|
||||||
// exist — the bHas* flags come from the worker. Collision at level 0 only (T1.c), both groups.
|
// exist — the bHas* flags come from the worker. Collision at level 0 only (T1.c), both groups.
|
||||||
|
const uint64 CollisionConfigStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||||
if (bHasGroundTris)
|
if (bHasGroundTris)
|
||||||
{
|
{
|
||||||
FRealtimeMeshSectionConfig GroundConfig(0);
|
FRealtimeMeshSectionConfig GroundConfig(0);
|
||||||
@@ -2474,6 +2606,35 @@ void AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1),
|
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1),
|
||||||
CapConfig, /*bShouldCreateCollision*/ bLevel0);
|
CapConfig, /*bShouldCreateCollision*/ bLevel0);
|
||||||
}
|
}
|
||||||
|
const double CollisionConfigSeconds = bProfileApply
|
||||||
|
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - CollisionConfigStartCycles)
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
if (bProfileApply)
|
||||||
|
{
|
||||||
|
const double TotalSeconds = FPlatformTime::ToSeconds64(
|
||||||
|
FPlatformTime::Cycles64() - ApplyStartCycles);
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeApplyProfile] level=%d empty=0 created=%d pooled=%d "
|
||||||
|
"material=%.6f component=%.6f component_create=%.6f "
|
||||||
|
"component_register=%.6f mesh_init=%.6f remove_group=%.6f "
|
||||||
|
"material_slots=%.6f stream_upload=%.6f create_section_group=%.6f "
|
||||||
|
"collision_config=%.6f total=%.6f"),
|
||||||
|
Tile.Level,
|
||||||
|
bComponentCreated ? 1 : 0,
|
||||||
|
(!bComponentAlreadyPresent && !bComponentCreated) ? 1 : 0,
|
||||||
|
MaterialSeconds,
|
||||||
|
ComponentSeconds,
|
||||||
|
ComponentCreationSeconds,
|
||||||
|
ComponentRegistrationSeconds,
|
||||||
|
MeshInitSeconds,
|
||||||
|
RemoveGroupSeconds,
|
||||||
|
MaterialSlotSeconds,
|
||||||
|
StreamUploadSeconds,
|
||||||
|
StreamUploadSeconds,
|
||||||
|
CollisionConfigSeconds,
|
||||||
|
TotalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
// Water is no longer spawned per tile — it's a single player-following ocean plane (UpdateWater,
|
// Water is no longer spawned per tile — it's a single player-following ocean plane (UpdateWater,
|
||||||
// driven from Tick), so it renders at every LOD and to the horizon with no per-tile gaps.
|
// driven from Tick), so it renders at every LOD and to the horizon with no per-tile gaps.
|
||||||
|
|||||||
@@ -698,6 +698,12 @@ struct FTunnelSupportFloorColumn
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A non-owning probe used only while a support-column is being prepared. The caller supplies the
|
||||||
|
// exact MC-facing density field with the graph support slab disabled; the morphology layer then
|
||||||
|
// runs the same player-fit predicate used by the capability measurement. A missing sampler is a
|
||||||
|
// deliberate safe fallback: retain the authored support band rather than making an unproven skip.
|
||||||
|
using FTunnelSupportFloorDensitySampler = TFunctionRef<float(float, float, float)>;
|
||||||
|
|
||||||
// A pre-baked pit shaft — position and dimensions resolved during BuildChunkCache.
|
// A pre-baked pit shaft — position and dimensions resolved during BuildChunkCache.
|
||||||
//
|
//
|
||||||
// Pits are included in the main SDF evaluation (SmoothMin alongside rooms and
|
// Pits are included in the main SDF evaluation (SmoothMin alongside rooms and
|
||||||
@@ -903,12 +909,18 @@ namespace VoxelCaveMorphology
|
|||||||
const FChunkSDFCache& Cache
|
const FChunkSDFCache& Cache
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build/query the same support-floor predicate at column granularity. This changes only
|
// Build/query the same support-floor predicate at column granularity. When NaturalSampler is
|
||||||
// where the deterministic projection is computed; it does not change the floor interval.
|
// supplied, a projected band is omitted only if the exact field with that graph slab disabled
|
||||||
|
// accepts a player-fit pose at this column. Without that proof the old interval is retained.
|
||||||
|
// The sampler is synchronous and non-owning; it is never retained by the cache.
|
||||||
VOXELFORGE_API void BuildTunnelSupportFloorColumn(
|
VOXELFORGE_API void BuildTunnelSupportFloorColumn(
|
||||||
float WorldX, float WorldY,
|
float WorldX, float WorldY,
|
||||||
const FChunkSDFCache& Cache,
|
const FChunkSDFCache& Cache,
|
||||||
FTunnelSupportFloorColumn& OutColumn
|
FTunnelSupportFloorColumn& OutColumn,
|
||||||
|
const FTunnelSupportFloorDensitySampler* NaturalSampler = nullptr,
|
||||||
|
float NaturalStrateTopZ = 0.0f,
|
||||||
|
float NaturalStrateBottomZ = 0.0f,
|
||||||
|
float NaturalBoundarySealThickness = 0.0f
|
||||||
);
|
);
|
||||||
|
|
||||||
VOXELFORGE_API bool IsTunnelSupportFloorColumnZ(
|
VOXELFORGE_API bool IsTunnelSupportFloorColumnZ(
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
#include "VoxelStrateTypes.h" // FMazeGenerationParams
|
#include "VoxelStrateTypes.h" // FMazeGenerationParams
|
||||||
#include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one
|
#include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one
|
||||||
|
|
||||||
|
class UVoxelGenerator;
|
||||||
class UVoxelStrateManager;
|
class UVoxelStrateManager;
|
||||||
struct FVoxelOpStackRecipe;
|
struct FVoxelOpStackRecipe;
|
||||||
struct FVoxelStrateArchetypeParams;
|
struct FVoxelStrateArchetypeParams;
|
||||||
@@ -473,7 +474,8 @@ namespace VoxelDensityOps
|
|||||||
const FStrateGenerationParams& P,
|
const FStrateGenerationParams& P,
|
||||||
int32 Seed, float SpineRadius,
|
int32 Seed, float SpineRadius,
|
||||||
const UVoxelStrateManager* StrateManager,
|
const UVoxelStrateManager* StrateManager,
|
||||||
bool bAppendStructuralPosts = true);
|
bool bAppendStructuralPosts = true,
|
||||||
|
const UVoxelGenerator* Generator = nullptr);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle
|
* DIAGNOSTIC — la ventilation par CLASSE DE PRIMITIVE de la dernière propagation d'intervalle
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ namespace VoxelDensityProfile
|
|||||||
TunnelSupportColumnCandidates,
|
TunnelSupportColumnCandidates,
|
||||||
TunnelSupportFloorQueries,
|
TunnelSupportFloorQueries,
|
||||||
TunnelSupportFloorChecks,
|
TunnelSupportFloorChecks,
|
||||||
|
TunnelSupportFloorNaturalTests,
|
||||||
|
TunnelSupportFloorNaturalSkips,
|
||||||
PassageCandidates,
|
PassageCandidates,
|
||||||
PassageEvaluated,
|
PassageEvaluated,
|
||||||
MesherBlockTests,
|
MesherBlockTests,
|
||||||
|
|||||||
@@ -167,7 +167,8 @@ public:
|
|||||||
float GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
|
float GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
|
||||||
const FStrateGenerationParams& Params,
|
const FStrateGenerationParams& Params,
|
||||||
uint32 ParamsFingerprint, uint32 LayoutVersion,
|
uint32 ParamsFingerprint, uint32 LayoutVersion,
|
||||||
bool bApplyLegacyStructuralPosts = true) const;
|
bool bApplyLegacyStructuralPosts = true,
|
||||||
|
bool bApplyTunnelSupportFloor = true) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Densité pour une strate Slab (FlatPlain / CrystalChamber).
|
* Densité pour une strate Slab (FlatPlain / CrystalChamber).
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||||
int32 MaxMeshAppliesPerFrame = 4;
|
int32 MaxMeshAppliesPerFrame = 4;
|
||||||
|
|
||||||
|
// Wall-clock cap for the complete game-thread result drain, including component teardown and
|
||||||
|
// collision configuration. The count cap remains the second guard. A zero value disables this
|
||||||
|
// cap; the default keeps one burst of chunk application below a small frame-time budget without
|
||||||
|
// changing worker resolution or mesh quality.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0.0"))
|
||||||
|
float MaxMeshApplyMilliseconds = 2.0f;
|
||||||
|
|
||||||
// Max tile teardowns (component destroy + content-actor Destroy) per frame. A fast traversal
|
// Max tile teardowns (component destroy + content-actor Destroy) per frame. A fast traversal
|
||||||
// culls a whole shell of tiles at once; doing every destroy in one frame is a game-thread spike
|
// culls a whole shell of tiles at once; doing every destroy in one frame is a game-thread spike
|
||||||
// ("stuff torn down behind you"). This spreads it. The drain auto-scales up if the backlog grows
|
// ("stuff torn down behind you"). This spreads it. The drain auto-scales up if the backlog grows
|
||||||
|
|||||||
@@ -206,7 +206,10 @@ public:
|
|||||||
static constexpr int32 MaxPooledTileComponents = 256;
|
static constexpr int32 MaxPooledTileComponents = 256;
|
||||||
|
|
||||||
/** Pop a pooled tile component (made visible again) or create + register a fresh one. */
|
/** Pop a pooled tile component (made visible again) or create + register a fresh one. */
|
||||||
URealtimeMeshComponent* AcquireTileComponent();
|
URealtimeMeshComponent* AcquireTileComponent(
|
||||||
|
double* OutCreationSeconds = nullptr,
|
||||||
|
double* OutRegistrationSeconds = nullptr,
|
||||||
|
bool* bOutCreated = nullptr);
|
||||||
|
|
||||||
/** Park a tile component in the pool (strip geometry/collision, hide) — or destroy it
|
/** Park a tile component in the pool (strip geometry/collision, hide) — or destroy it
|
||||||
* for real when the pool is full. */
|
* for real when the pool is full. */
|
||||||
|
|||||||
Reference in new issue
Block a user