perf(apply): stutter is NOT the game thread -- and the flat floor cannot be relaxed

Two things the owner reported from playing 412a7ad. 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 on 412a7ad. 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:
Fr0zkaandClaude Opus 5 committed 2026-09-10 14:46:29 +02:00
1 parent 412a7ad219
commit 383ec86c37
11 files changed
+502 -37

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");
+50 -12
View File
@@ -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.
+170 -9
View File
@@ -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;
}
// 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; 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;
} }
// "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) if (GVoxelForgeProfileTileGeneration != 0 && Result.RequestStartCycles != 0)
{ {
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTileReady] level=%d empty=%d request_to_ready=%.6f"), TEXT("[VoxelForgeTileReady] level=%d empty=%d request_to_ready=%.6f"),
Result.Tile.Level, Result.bEmpty || !Result.Streams ? 1 : 0, Result.Tile.Level, bResultEmpty ? 1 : 0,
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - Result.RequestStartCycles)); 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.
+15 -3
View File
@@ -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,
+2 -1
View File
@@ -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).
+7
View File
@@ -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
+4 -1
View File
@@ -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. */