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
+502
-37
No files matched your search
@@ -35,6 +35,17 @@
|
||||
#include "VoxelStrateTypes.h"
|
||||
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 "VoxelPassageGeometry.h"
|
||||
@@ -72,6 +83,173 @@ namespace
|
||||
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()
|
||||
@@ -903,8 +1081,6 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
using FVFPlayerDensitySampler = TFunctionRef<float(float, float, float)>;
|
||||
|
||||
bool VF_GetPlayerFitInteriorBounds(
|
||||
const FVoxelStrateMeasureSettings& Settings,
|
||||
float StrateTopZ,
|
||||
@@ -5114,7 +5290,8 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
Tunnel.WorldControlRadii,
|
||||
Pos,
|
||||
FloorZ,
|
||||
SupportRadius)
|
||||
SupportRadius,
|
||||
nullptr)
|
||||
|| WorldZ > FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
|
||||
|| WorldZ < FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels)
|
||||
{
|
||||
@@ -5150,7 +5327,11 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
||||
float WorldX, float WorldY,
|
||||
const FChunkSDFCache& Cache,
|
||||
FTunnelSupportFloorColumn& OutColumn)
|
||||
FTunnelSupportFloorColumn& OutColumn,
|
||||
const FTunnelSupportFloorDensitySampler* NaturalSampler,
|
||||
float NaturalStrateTopZ,
|
||||
float NaturalStrateBottomZ,
|
||||
float NaturalBoundarySealThickness)
|
||||
{
|
||||
OutColumn.Reset();
|
||||
|
||||
@@ -5183,16 +5364,36 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
||||
const FVector ColumnPosition(WorldX, WorldY, 0.0f);
|
||||
float FloorZ = 0.0f;
|
||||
float SupportRadius = 0.0f;
|
||||
int32 SegmentIndex = INDEX_NONE;
|
||||
if (!VoxelPassageGeometry::ProjectWalkableTunnelFloor(
|
||||
Tunnel.WorldControlPoints,
|
||||
Tunnel.WorldControlRadii,
|
||||
ColumnPosition,
|
||||
FloorZ,
|
||||
SupportRadius))
|
||||
SupportRadius,
|
||||
&SegmentIndex))
|
||||
{
|
||||
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 MaxZ = FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
|
||||
|
||||
|
||||
@@ -3591,8 +3591,10 @@ namespace
|
||||
{
|
||||
public:
|
||||
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)
|
||||
, Generator(InGenerator)
|
||||
, ManagerLifetimeId(InManager ? InManager->GetCacheLifetimeId() : 0)
|
||||
, ParamsFingerprint(FCrc::MemCrc32(&InP, sizeof(InP)))
|
||||
{}
|
||||
@@ -3791,6 +3793,37 @@ namespace
|
||||
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
|
||||
{
|
||||
FState& S = State();
|
||||
@@ -3809,7 +3842,7 @@ namespace
|
||||
IX, IY, &Cache, S.SupportColumns.Clock, bNewColumn);
|
||||
if (bNewColumn)
|
||||
{
|
||||
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
||||
BuildSupportColumn(
|
||||
static_cast<float>(Position.X),
|
||||
static_cast<float>(Position.Y),
|
||||
Cache,
|
||||
@@ -3818,7 +3851,7 @@ namespace
|
||||
return Column;
|
||||
}
|
||||
|
||||
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
||||
BuildSupportColumn(
|
||||
static_cast<float>(Position.X), static_cast<float>(Position.Y),
|
||||
GetCache(), S.FractionalSupportColumn);
|
||||
return &S.FractionalSupportColumn;
|
||||
@@ -6633,6 +6666,7 @@ namespace
|
||||
int32 Seed;
|
||||
uint32 SeedU;
|
||||
TWeakObjectPtr<const UVoxelStrateManager> Manager;
|
||||
const UVoxelGenerator* Generator = nullptr;
|
||||
uint64 ManagerLifetimeId = 0;
|
||||
uint32 ParamsFingerprint;
|
||||
uint32 LayoutVersion = 0;
|
||||
@@ -9408,7 +9442,8 @@ namespace VoxelDensityOps
|
||||
|
||||
void BuildTunnelNetworkStack(FVoxelOpStack& OutStack, const FStrateGenerationParams& P,
|
||||
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.
|
||||
// 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
|
||||
// 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();
|
||||
|
||||
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
|
||||
|
||||
@@ -414,6 +414,8 @@ namespace VoxelDensityProfile
|
||||
case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates");
|
||||
case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries");
|
||||
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::PassageEvaluated: return TEXT("PassageEvaluated");
|
||||
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
|
||||
|
||||
@@ -507,7 +507,11 @@ namespace
|
||||
|
||||
static void BuildTunnelSupportColumnsForChunk(
|
||||
FChunkSDFCache& Cache,
|
||||
const FIntVector& ChunkCoord);
|
||||
const FIntVector& ChunkCoord,
|
||||
const UVoxelGenerator* Generator,
|
||||
const FStrateGenerationParams& Params,
|
||||
uint32 ParamsFingerprint,
|
||||
uint32 LayoutVersion);
|
||||
|
||||
static const FTunnelSupportFloorColumn* FindTunnelSupportColumn(
|
||||
const FChunkSDFCache& Cache,
|
||||
@@ -603,6 +607,7 @@ namespace
|
||||
}
|
||||
|
||||
void PrepareTunnelCoreCache(
|
||||
const UVoxelGenerator& Generator,
|
||||
const UVoxelStrateManager& Manager,
|
||||
const FIntVector& ChunkCoord,
|
||||
const FStrateGenerationParams& Params,
|
||||
@@ -638,7 +643,9 @@ namespace
|
||||
ChunkMaxX + Expansion, ChunkMaxY + Expansion,
|
||||
Params, static_cast<uint32>(Manager.GetWorldSeed()),
|
||||
StrateIndex, nullptr);
|
||||
BuildTunnelSupportColumnsForChunk(OutState.Cache, ChunkCoord);
|
||||
BuildTunnelSupportColumnsForChunk(
|
||||
OutState.Cache, ChunkCoord, &Generator, Params,
|
||||
ParamsFingerprint, LayoutVersion);
|
||||
OutState.OwnerId = OwnerId;
|
||||
OutState.ManagerLifetimeId = ManagerLifetimeId;
|
||||
OutState.Chunk = ChunkCoord;
|
||||
@@ -649,7 +656,11 @@ namespace
|
||||
|
||||
static void BuildTunnelSupportColumnsForChunk(
|
||||
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 MinY = ChunkCoord.Y * CHUNK_SIZE - 1;
|
||||
@@ -661,6 +672,25 @@ namespace
|
||||
Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells);
|
||||
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
|
||||
// each stored column retains the inline bands for actual graph tubes.
|
||||
for (int32 Y = 0; Y < Cells; ++Y)
|
||||
@@ -672,7 +702,11 @@ namespace
|
||||
static_cast<float>(MinX + X),
|
||||
static_cast<float>(MinY + Y),
|
||||
Cache,
|
||||
Column);
|
||||
Column,
|
||||
NaturalSampler,
|
||||
Params.StrateTopWorldZ,
|
||||
Params.StrateBottomWorldZ,
|
||||
Params.BoundarySealThickness);
|
||||
if (Column.Intervals.Num() == 0)
|
||||
{
|
||||
continue;
|
||||
@@ -736,7 +770,8 @@ namespace
|
||||
*/
|
||||
bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs,
|
||||
int32 Seed, float SpineRadius, const UVoxelStrateManager* SM,
|
||||
FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx)
|
||||
FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx,
|
||||
const UVoxelGenerator* Generator = nullptr)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
@@ -793,7 +828,9 @@ namespace
|
||||
if (!Refs.Tunnel) { return false; }
|
||||
OutCtx.StrateTopWorldZ = Refs.Tunnel->StrateTopWorldZ;
|
||||
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;
|
||||
|
||||
default:
|
||||
@@ -1259,7 +1296,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
|
||||
CP_UseOpStack = VF_BuildOpStackForChunk(
|
||||
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
|
||||
// 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)
|
||||
{
|
||||
PrepareTunnelCoreCache(
|
||||
*StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
|
||||
*this, *StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
|
||||
LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache);
|
||||
}
|
||||
else
|
||||
@@ -1647,7 +1684,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
|
||||
const FStrateGenerationParams& Params,
|
||||
uint32 ParamsFingerprint, uint32 LayoutVersion,
|
||||
bool bApplyLegacyStructuralPosts) const
|
||||
bool bApplyLegacyStructuralPosts,
|
||||
bool bApplyTunnelSupportFloor) const
|
||||
{
|
||||
//=========================================================================
|
||||
// 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
|
||||
// GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the
|
||||
// same predicates once in its common MC-space post section below both generation paths.
|
||||
const bool bTunnelSupportFloor =
|
||||
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
const bool bTunnelSupportFloor = bApplyTunnelSupportFloor
|
||||
&& VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
WorldX, WorldY, WorldZ, SDFCache);
|
||||
if (bTunnelSupportFloor)
|
||||
{
|
||||
@@ -4753,7 +4791,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
|
||||
FVoxelOpStack TileStack;
|
||||
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`,
|
||||
// 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.
|
||||
// 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.
|
||||
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 ResultsDrained = 0;
|
||||
bool bTimeBudgetHit = false;
|
||||
FChunkResult DequeuedChunk;
|
||||
while (ProcessQueue.Dequeue(DequeuedChunk))
|
||||
{
|
||||
++ResultsDrained;
|
||||
PendingTiles.Remove(DequeuedChunk.Tile);
|
||||
|
||||
// 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.
|
||||
if (ApplyTileResult(DequeuedChunk))
|
||||
{
|
||||
if (++MeshesApplied >= MaxApplies)
|
||||
++MeshesApplied;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1209,13 +1244,21 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
return false;
|
||||
}
|
||||
|
||||
// "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 = [&]()
|
||||
{
|
||||
if (GVoxelForgeProfileTileGeneration != 0 && Result.RequestStartCycles != 0)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTileReady] level=%d empty=%d request_to_ready=%.6f"),
|
||||
Result.Tile.Level, Result.bEmpty || !Result.Streams ? 1 : 0,
|
||||
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - Result.RequestStartCycles));
|
||||
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).
|
||||
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
|
||||
// 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.
|
||||
const uint64 EmptyReleaseStartCycles = GVoxelForgeProfileTileGeneration != 0
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
bool bReleasedComponent = false;
|
||||
if (URealtimeMeshComponent** OldComp = TileComponents.Find(Result.Tile))
|
||||
{
|
||||
if (*OldComp) { ReleaseTileComponent(*OldComp); }
|
||||
if (*OldComp)
|
||||
{
|
||||
ReleaseTileComponent(*OldComp);
|
||||
bReleasedComponent = true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// 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.
|
||||
ApplyMeshToTile(Result);
|
||||
LogTileReady();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2291,8 +2352,15 @@ static FRealtimeMeshSectionGroupKey VoxelTileGroupKey()
|
||||
//=============================================================================
|
||||
// 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
|
||||
// (and the full proxy teardown/GC of a destroy) during fast travel & regen bursts.
|
||||
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);
|
||||
// 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.
|
||||
@@ -2313,8 +2385,21 @@ URealtimeMeshComponent* AVoxelWorld::AcquireTileComponent()
|
||||
MeshComp->SetMobility(EComponentMobility::Static);
|
||||
MeshComp->SetGenerateOverlapEvents(false); // chunks use raycasts, not overlaps
|
||||
MeshComp->SetCanEverAffectNavigation(false);
|
||||
if (OutCreationSeconds != nullptr)
|
||||
{
|
||||
*OutCreationSeconds = FPlatformTime::ToSeconds64(
|
||||
FPlatformTime::Cycles64() - CreationStartCycles);
|
||||
}
|
||||
const uint64 RegistrationStartCycles = bMeasureComponentPhases
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
MeshComp->RegisterComponent();
|
||||
MeshComp->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
|
||||
if (OutRegistrationSeconds != nullptr)
|
||||
{
|
||||
*OutRegistrationSeconds = FPlatformTime::ToSeconds64(
|
||||
FPlatformTime::Cycles64() - RegistrationStartCycles);
|
||||
}
|
||||
if (bOutCreated != nullptr) { *bOutCreated = true; }
|
||||
return MeshComp;
|
||||
}
|
||||
|
||||
@@ -2358,6 +2443,9 @@ void AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
{
|
||||
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
|
||||
// (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.
|
||||
@@ -2414,6 +2502,9 @@ void AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
if (UMaterialInstanceDynamic* MID = GetOrCreateTerrainMID(GroundMaterial)) { GroundMaterial = 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.
|
||||
// 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.
|
||||
// T2.c: the component comes from the pool when one is parked (see AcquireTileComponent).
|
||||
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)
|
||||
{
|
||||
MeshComp = AcquireTileComponent();
|
||||
MeshComp = AcquireTileComponent(
|
||||
bProfileApply ? &ComponentCreationSeconds : nullptr,
|
||||
bProfileApply ? &ComponentRegistrationSeconds : nullptr,
|
||||
bProfileApply ? &bComponentCreated : nullptr);
|
||||
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
|
||||
// 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.
|
||||
// The RemoveSectionGroup below does the actual geometry clearing on reuse.
|
||||
URealtimeMeshSimple* RTMesh = MeshComp->GetRealtimeMeshAs<URealtimeMeshSimple>();
|
||||
const uint64 MeshInitStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||
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
|
||||
// — 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.
|
||||
@@ -2447,17 +2563,33 @@ void AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
MeshComp->SetCastShadow(bCastShadow);
|
||||
|
||||
const FRealtimeMeshSectionGroupKey GroupKey = VoxelTileGroupKey();
|
||||
const uint64 RemoveGroupStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||
RTMesh->RemoveSectionGroup(GroupKey); // clear old geometry on re-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(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));
|
||||
const double StreamUploadSeconds = bProfileApply
|
||||
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - StreamUploadStartCycles)
|
||||
: 0.0;
|
||||
|
||||
// RMC casts shadows PER SECTION (FRealtimeMeshSectionConfig::bCastsShadow, default true) — the
|
||||
// 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
|
||||
// (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.
|
||||
const uint64 CollisionConfigStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||
if (bHasGroundTris)
|
||||
{
|
||||
FRealtimeMeshSectionConfig GroundConfig(0);
|
||||
@@ -2474,6 +2606,35 @@ void AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1),
|
||||
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,
|
||||
// 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.
|
||||
//
|
||||
// Pits are included in the main SDF evaluation (SmoothMin alongside rooms and
|
||||
@@ -903,12 +909,18 @@ namespace VoxelCaveMorphology
|
||||
const FChunkSDFCache& Cache
|
||||
);
|
||||
|
||||
// Build/query the same support-floor predicate at column granularity. This changes only
|
||||
// where the deterministic projection is computed; it does not change the floor interval.
|
||||
// Build/query the same support-floor predicate at column granularity. When NaturalSampler is
|
||||
// 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(
|
||||
float WorldX, float WorldY,
|
||||
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(
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "VoxelStrateTypes.h" // FMazeGenerationParams
|
||||
#include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one
|
||||
|
||||
class UVoxelGenerator;
|
||||
class UVoxelStrateManager;
|
||||
struct FVoxelOpStackRecipe;
|
||||
struct FVoxelStrateArchetypeParams;
|
||||
@@ -473,7 +474,8 @@ namespace VoxelDensityOps
|
||||
const FStrateGenerationParams& P,
|
||||
int32 Seed, float SpineRadius,
|
||||
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
|
||||
|
||||
@@ -98,6 +98,8 @@ namespace VoxelDensityProfile
|
||||
TunnelSupportColumnCandidates,
|
||||
TunnelSupportFloorQueries,
|
||||
TunnelSupportFloorChecks,
|
||||
TunnelSupportFloorNaturalTests,
|
||||
TunnelSupportFloorNaturalSkips,
|
||||
PassageCandidates,
|
||||
PassageEvaluated,
|
||||
MesherBlockTests,
|
||||
|
||||
@@ -167,7 +167,8 @@ public:
|
||||
float GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
|
||||
const FStrateGenerationParams& Params,
|
||||
uint32 ParamsFingerprint, uint32 LayoutVersion,
|
||||
bool bApplyLegacyStructuralPosts = true) const;
|
||||
bool bApplyLegacyStructuralPosts = true,
|
||||
bool bApplyTunnelSupportFloor = true) const;
|
||||
|
||||
/**
|
||||
* Densité pour une strate Slab (FlatPlain / CrystalChamber).
|
||||
|
||||
@@ -53,6 +53,13 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -206,7 +206,10 @@ public:
|
||||
static constexpr int32 MaxPooledTileComponents = 256;
|
||||
|
||||
/** 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
|
||||
* for real when the pool is full. */
|
||||
|
||||
Reference in new issue
Block a user