perf(collision): cook stages measured, no standing on dug-out ground; review F/H/E measured

1. Collision cook breakdown (request -> ready), critical support tile:
   spawn (0,0,-1): generate 107 ms, apply < 1 ms, RMC submit->body 479 ms, total 587 ms;
   deep floor (0,0,-7): generate 82 ms, RMC 1,441 ms, total 1,548 ms.
   Almost all of the wait is inside RMC (end-of-frame update + async pool + Chaos cook + body
   install, not separable from our side). Critical priority does not shorten it. Collision is
   async, complex-as-simple, fast-cook off; skirts are ~16-18% of collision triangles.
   No cook change made. Startup outlier: first support tile waited 2.09 s for its game-thread apply.
   Underfoot dig: a carving brush that intersects the pawn's capsule and support tile now
   disables the old body and withdraws readiness on the carve frame: 0 frames standing on removed
   ground (replacement 91.8 ms later). 8 neighbouring digs: 0 gate frames. Gap audit 0.
2. Stack sampler, 49.4 s game path: top resolved leaf was the profiler's own
   VoxelDensityProfile::BucketFromName (4.86%). Linear label scan -> CRC-keyed lookup with exact
   compare: 59.26 -> 57.87 ms/tile (-2.35%), 980/980 tile hashes identical.
3. Measured only: F (classifier mode 1, no exact check) +48% worker CPU, 0/980 tiles differ ->
   not worth it. H (handoff component scans) p95 0.07 ms/frame -> not worth it.
   E (FullResClipLevels=1): worker CPU 56.7 -> 35.7 s, settle 9.6 -> 7.0 s, crossing samples
   -30%; changes the look, owner's decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-09-29 07:52:18 +02:00
1 parent ce6812dbeb
commit bf22f0ffe0
7 files changed
+356 -36

No files matched your search

@@ -1,4 +1,5 @@
#include "VoxelDensityProfile.h"
#include "Misc/Crc.h"
namespace VoxelDensityProfile
{
@@ -778,11 +779,25 @@ namespace VoxelDensityProfile
{ TEXT("MesherStreamBuilding"), EBucket::MesherStreamBuilding },
{ TEXT("MesherOther"), EBucket::MesherOther },
};
for (const FNameBucket& Entry : Names)
static const TMap<uint32, TArray<const FNameBucket*>> NamesByHash = []()
{
if (FCString::Strcmp(Name, Entry.Name) == 0)
TMap<uint32, TArray<const FNameBucket*>> Lookup;
for (const FNameBucket& Entry : Names)
{
return Entry.Bucket;
Lookup.FindOrAdd(FCrc::StrCrc32(Entry.Name)).Add(&Entry);
}
return Lookup;
}();
const TArray<const FNameBucket*>* Candidates = NamesByHash.Find(FCrc::StrCrc32(Name));
if (Candidates != nullptr)
{
for (const FNameBucket* Entry : *Candidates)
{
if (Entry != nullptr && FCString::Strcmp(Name, Entry->Name) == 0)
{
return Entry->Bucket;
}
}
}
return EBucket::OtherOp;
@@ -1122,6 +1122,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
Tris.Add(iA); Tris.Add(iA2); Tris.Add(iB2);
}
}
MeshData.NumSkirtTriangles += (Tris.Num() - BaseTriNum) / 3;
};
EmitSkirts(GroundTris);
EmitSkirts(CapTris);
+265 -15
View File
@@ -84,6 +84,18 @@ namespace
GVoxelForgeProfileTileGeneration,
TEXT("Log worker tile generation time with level/step/cell count."));
int32 GVoxelForgeProfileCollisionCookStages = 0;
FAutoConsoleVariableRef CVarVoxelForgeProfileCollisionCookStages(
TEXT("voxel.ProfileCollisionCookStages"),
GVoxelForgeProfileCollisionCookStages,
TEXT("Log per-tile collision queue/apply/RMC/future timing and cooked geometry counts."));
int32 GVoxelForgeTestFullResClipLevels = -1;
FAutoConsoleVariableRef CVarVoxelForgeTestFullResClipLevels(
TEXT("voxel.TestFullResClipLevels"),
GVoxelForgeTestFullResClipLevels,
TEXT("Measurement-only override for the streaming full-resolution clip level count; -1 uses the asset."));
int32 GVoxelForgeProfileDensity = 0;
FAutoConsoleVariableRef CVarVoxelForgeProfileDensity(
TEXT("voxel.ProfileDensity"),
@@ -663,6 +675,8 @@ void AVoxelWorld::RegenerateAllChunks()
ModificationRevisions.Reset();
AppliedModificationEpoch.Reset();
CollisionReadyTiles.Empty();
CollisionSuppressedUntilCook.Empty();
CollisionSuppressedAtCycles.Empty();
CollisionNotRequiredTiles.Empty();
CollisionSolidTiles.Empty();
PendingCollisionCooks.Empty();
@@ -1506,6 +1520,8 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
BandRemeshQueue.Empty();
PendingCollisionCooks.Empty();
CollisionReadyTiles.Empty();
CollisionSuppressedUntilCook.Empty();
CollisionSuppressedAtCycles.Empty();
CollisionDesiredTiles.Empty();
CollisionNotRequiredTiles.Empty();
CollisionSolidTiles.Empty();
@@ -1615,6 +1631,8 @@ void AVoxelWorld::BeginPlay()
int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull;
int32 CommandLinePerfAttribution = GVoxelForgePerfAttribution;
int32 CommandLineProfileTileGeneration = GVoxelForgeProfileTileGeneration;
int32 CommandLineProfileCollisionCookStages = GVoxelForgeProfileCollisionCookStages;
int32 CommandLineTestFullResClipLevels = GVoxelForgeTestFullResClipLevels;
int32 CommandLineUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut;
int32 CommandLineOuterClassifierMode = GVoxelForgeOuterClassifierMode;
int32 CommandLineSealedSolidProof = GVoxelForgeSealedSolidProof;
@@ -1639,6 +1657,20 @@ void AVoxelWorld::BeginPlay()
{
GVoxelForgeProfileTileGeneration = CommandLineProfileTileGeneration;
}
if (FParse::Value(
FCommandLine::Get(),
TEXT("voxel.ProfileCollisionCookStages="),
CommandLineProfileCollisionCookStages))
{
GVoxelForgeProfileCollisionCookStages = CommandLineProfileCollisionCookStages;
}
if (FParse::Value(
FCommandLine::Get(),
TEXT("voxel.TestFullResClipLevels="),
CommandLineTestFullResClipLevels))
{
GVoxelForgeTestFullResClipLevels = CommandLineTestFullResClipLevels;
}
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.UseBlockEarlyOut="), CommandLineUseBlockEarlyOut))
{
GVoxelForgeUseBlockEarlyOut = CommandLineUseBlockEarlyOut;
@@ -1894,6 +1926,14 @@ void AVoxelWorld::BeginPlay()
GVoxelForgeOuterClassifierMode, GVoxelForgeSealedSolidProof,
Mesher->bUseBlockEarlyOut ? 1 : 0);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeFullResClipConfig] authored=%d effective=%d override=%d"),
Settings->FullResClipLevels,
GVoxelForgeTestFullResClipLevels >= 0
? FMath::Clamp(GVoxelForgeTestFullResClipLevels, 1, 9)
: FMath::Max(1, Settings->FullResClipLevels),
GVoxelForgeTestFullResClipLevels);
VoxelForgeStartupTrace::RecordEvent(TEXT("generator_mesher_constructed"), FString::Printf(
TEXT("\"generator\":1,\"mesher\":1,\"generate_skirts\":%d,\"full_res_clip_levels\":%d,\"coarse_tile_cells\":%d"),
Settings->bGenerateSkirts ? 1 : 0,
@@ -5603,6 +5643,38 @@ void AVoxelWorld::LogReadyTransitionVisibilityMeasure()
MinComponents, MaxComponents, MinDesired, MaxDesired,
static_cast<double>(TotalComponents) / Sorted.Num(),
static_cast<double>(TotalDesired) / Sorted.Num());
constexpr int32 ComponentBucketWidth = 128;
TMap<int32, TArray<double>> MillisecondsByComponentBucket;
for (const FReadyTransitionVisibilitySample& Sample : Sorted)
{
const int32 BucketMin = (Sample.TileComponentCount / ComponentBucketWidth)
* ComponentBucketWidth;
MillisecondsByComponentBucket.FindOrAdd(BucketMin).Add(
FPlatformTime::ToSeconds64(Sample.Cycles) * 1000.0);
}
TArray<int32> BucketMins;
MillisecondsByComponentBucket.GetKeys(BucketMins);
BucketMins.Sort();
for (const int32 BucketMin : BucketMins)
{
TArray<double>& Times = MillisecondsByComponentBucket.FindChecked(BucketMin);
Times.Sort();
double TotalMs = 0.0;
for (const double TimeMs : Times)
{
TotalMs += TimeMs;
}
const int32 P50BucketIndex = Times.Num() / 2;
const int32 P95BucketIndex = FMath::Clamp(
FMath::CeilToInt(static_cast<float>(Times.Num()) * 0.95f) - 1,
0, Times.Num() - 1);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeReadyVisibilityBucket] components=%d..%d samples=%d "
"p50_ms=%.6f p95_ms=%.6f mean_ms=%.6f"),
BucketMin, BucketMin + ComponentBucketWidth - 1, Times.Num(),
Times[P50BucketIndex], Times[P95BucketIndex], TotalMs / Times.Num());
}
}
if (!FarFieldSubmitSamples.IsEmpty())
@@ -6905,6 +6977,7 @@ void AVoxelWorld::HandleRealtimeMeshCollisionBodyUpdated(URealtimeMesh* Mesh, UB
{
Pending.bBodyUpdatedEventSeen = true;
Pending.BodyUpdatedEventCycles = FPlatformTime::Cycles64();
Pending.BodyUpdatedEventFrame = GFrameCounter;
return;
}
}
@@ -7117,18 +7190,20 @@ void AVoxelWorld::LogStreamingLatencySummary() const
}
void AVoxelWorld::HandleTileCollisionCookComplete(
const FVoxelTileKey& Tile, uint64 SubmissionId, URealtimeMesh* Mesh, uint8 Result)
const FVoxelTileKey& Tile, uint64 SubmissionId, URealtimeMesh* Mesh, uint8 Result,
uint64 FutureCallbackCycles)
{
const uint64 CompletionGameThreadCycles = FPlatformTime::Cycles64();
if (!IsInGameThread())
{
TWeakObjectPtr<AVoxelWorld> WeakWorld(this);
AsyncTask(ENamedThreads::GameThread,
[WeakWorld, Tile, SubmissionId, Mesh, Result]()
[WeakWorld, Tile, SubmissionId, Mesh, Result, FutureCallbackCycles]()
{
if (AVoxelWorld* World = WeakWorld.Get())
{
World->HandleTileCollisionCookComplete(
Tile, SubmissionId, Mesh, Result);
Tile, SubmissionId, Mesh, Result, FutureCallbackCycles);
}
});
return;
@@ -7149,14 +7224,29 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
return;
}
Pending->FutureCallbackCycles = FutureCallbackCycles;
Pending->CompletionGameThreadCycles = CompletionGameThreadCycles;
const uint64 MeasuredMeshSubmittedCycles = Pending->MeshSubmittedCycles;
const uint64 MeasuredCollisionSubmittedCycles = Pending->CollisionSubmittedCycles;
const bool bBodyUpdatedEventSeen = Pending->bBodyUpdatedEventSeen;
const uint64 BodyUpdatedEventCycles = Pending->BodyUpdatedEventCycles;
const uint64 BodyUpdatedEventFrame = Pending->BodyUpdatedEventFrame;
const uint64 MeasuredCollisionSubmittedFrame = Pending->CollisionSubmittedFrame;
const uint64 MeasuredRequestStartCycles = Pending->RequestStartCycles;
const uint64 MeasuredGenerationStartCycles = Pending->GenerationStartCycles;
const uint64 MeasuredGenerationEndCycles = Pending->GenerationEndCycles;
const uint64 MeasuredApplyStartCycles = Pending->ApplyStartCycles;
const EVoxelTileWorkPriority WorkPriority = Pending->WorkPriority;
const int32 NumVertices = Pending->NumVertices;
const int32 NumTriangles = Pending->NumTriangles;
const int32 NumSkirtTriangles = Pending->NumSkirtTriangles;
const int32 PendingCooksAtSubmit = Pending->PendingCooksAtSubmit;
const bool bUseAsyncCook = Pending->bUseAsyncCook;
const bool bUseComplexAsSimpleCollision = Pending->bUseComplexAsSimpleCollision;
const bool bShouldFastCookMeshes = Pending->bShouldFastCookMeshes;
const bool bMergeAllMeshes = Pending->bMergeAllMeshes;
const uint64 SuppressedAtCycles = Pending->SuppressedAtCycles;
const bool bRecordStreamingLatency = Pending->bRecordStreamingLatency;
PendingCollisionCooks.Remove(Tile);
@@ -7167,6 +7257,12 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
const bool bCollisionStillDesired = IsTileCollisionDesired(Tile);
if (bBodyInstalled && bCollisionStillDesired)
{
CollisionSuppressedUntilCook.Remove(Tile);
CollisionSuppressedAtCycles.Remove(Tile);
if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Tile))
{
Comp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
}
CollisionReadyTiles.Add(Tile);
if (bStartupCollisionGateTimingCaptured
&& StartupCollisionGateSupportTile.Coord == Tile.Coord
@@ -7196,6 +7292,11 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
else
{
CollisionReadyTiles.Remove(Tile);
if (!bCollisionStillDesired)
{
CollisionSuppressedUntilCook.Remove(Tile);
CollisionSuppressedAtCycles.Remove(Tile);
}
if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Tile))
{
Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
@@ -7224,6 +7325,33 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
const double BodyEventToFutureSeconds = bBodyUpdatedEventSeen
&& ReadyCycles >= BodyUpdatedEventCycles
? FPlatformTime::ToSeconds64(ReadyCycles - BodyUpdatedEventCycles) : -1.0;
const auto SecondsBetween = [](uint64 EndCycles, uint64 StartCycles) -> double
{
return StartCycles != 0 && EndCycles >= StartCycles
? FPlatformTime::ToSeconds64(EndCycles - StartCycles)
: -1.0;
};
const double RequestToWorkerStartSeconds = SecondsBetween(
MeasuredGenerationStartCycles, MeasuredRequestStartCycles);
const double WorkerGenerationSeconds = SecondsBetween(
MeasuredGenerationEndCycles, MeasuredGenerationStartCycles);
const double WorkerResultToApplySeconds = SecondsBetween(
MeasuredApplyStartCycles, MeasuredGenerationEndCycles);
const double ApplyToMeshSubmitSeconds = SecondsBetween(
MeasuredMeshSubmittedCycles, MeasuredApplyStartCycles);
const double MeshSubmitToCollisionSubmitSeconds = SecondsBetween(
MeasuredCollisionSubmittedCycles, MeasuredMeshSubmittedCycles);
const double RMCSubmitToBodyUpdateSeconds = bBodyUpdatedEventSeen
? SecondsBetween(BodyUpdatedEventCycles, MeasuredCollisionSubmittedCycles) : -1.0;
const double BodyUpdateToFutureCallbackSeconds = bBodyUpdatedEventSeen
? SecondsBetween(FutureCallbackCycles, BodyUpdatedEventCycles) : -1.0;
const double FutureCallbackToGameThreadSeconds = SecondsBetween(
CompletionGameThreadCycles, FutureCallbackCycles);
const double SuppressedToReadySeconds = SecondsBetween(ReadyCycles, SuppressedAtCycles);
const int64 FramesToBodyUpdate = bBodyUpdatedEventSeen
&& BodyUpdatedEventFrame >= MeasuredCollisionSubmittedFrame
? static_cast<int64>(BodyUpdatedEventFrame - MeasuredCollisionSubmittedFrame)
: -1;
const TCHAR* ResultName = TEXT("Unknown");
switch (CollisionResult)
{
@@ -7232,32 +7360,66 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
case ERealtimeMeshCollisionUpdateResult::Error: ResultName = TEXT("Error"); break;
default: break;
}
if (GVoxelForgeProfileTileGeneration != 0 || VoxelForgeStartupTrace::IsActive()
if (GVoxelForgeProfileTileGeneration != 0 || GVoxelForgeProfileCollisionCookStages != 0
|| VoxelForgeStartupTrace::IsActive()
|| !bBodyInstalled)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionReady] tile=(%d,%d,%d) level=%d result=%s body_setup=%d "
"ready=%d body_event=%d mesh_submit_to_cook=%.6f "
"collision_request_to_cook=%.6f body_event_to_future=%.6f"),
"ready=%d body_event=%d priority=%d critical=%d vertices=%d triangles=%d "
"skirt_triangles=%d pending_cooks=%d async=%d complex_as_simple=%d fast_cook=%d "
"merge=%d request_to_worker_s=%.6f worker_gen_s=%.6f worker_to_apply_s=%.6f "
"apply_to_mesh_submit_s=%.6f mesh_to_collision_submit_s=%.6f "
"rmc_submit_to_body_update_s=%.6f submit_to_body_frames=%lld "
"body_update_to_future_callback_s=%.6f future_callback_to_gt_s=%.6f "
"suppressed_to_ready_s=%.6f mesh_submit_to_ready=%.6f "
"collision_request_to_ready=%.6f body_event_to_future=%.6f"),
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, ResultName,
CompletedMesh->GetBodySetup() != nullptr ? 1 : 0,
bBodyInstalled ? 1 : 0,
bBodyUpdatedEventSeen ? 1 : 0,
MeshToCookSeconds, CollisionRequestToCookSeconds, BodyEventToFutureSeconds);
static_cast<int32>(WorkPriority),
WorkPriority == EVoxelTileWorkPriority::CollisionCritical ? 1 : 0,
NumVertices, NumTriangles, NumSkirtTriangles, PendingCooksAtSubmit,
bUseAsyncCook ? 1 : 0, bUseComplexAsSimpleCollision ? 1 : 0,
bShouldFastCookMeshes ? 1 : 0, bMergeAllMeshes ? 1 : 0,
RequestToWorkerStartSeconds, WorkerGenerationSeconds, WorkerResultToApplySeconds,
ApplyToMeshSubmitSeconds, MeshSubmitToCollisionSubmitSeconds,
RMCSubmitToBodyUpdateSeconds, FramesToBodyUpdate,
BodyUpdateToFutureCallbackSeconds, FutureCallbackToGameThreadSeconds,
SuppressedToReadySeconds, MeshToCookSeconds,
CollisionRequestToCookSeconds, BodyEventToFutureSeconds);
}
if (VoxelForgeStartupTrace::IsActive())
{
VoxelForgeStartupTrace::RecordEvent(TEXT("collision_ready"), FString::Printf(
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"result\":\"%s\",\"body_setup\":%d,"
"\"ready\":%d,\"body_update_event\":%d,"
"\"mesh_submit_to_cook_s\":%s,\"collision_request_to_cook_s\":%s,"
"\"body_event_to_future_s\":%s"),
"\"priority\":%d,\"critical\":%d,\"vertices\":%d,\"triangles\":%d,"
"\"skirt_triangles\":%d,\"pending_cooks_at_submit\":%d,"
"\"request_to_worker_s\":%s,\"worker_generation_s\":%s,"
"\"worker_result_to_apply_s\":%s,\"apply_to_mesh_submit_s\":%s,"
"\"mesh_to_collision_submit_s\":%s,\"rmc_submit_to_body_update_s\":%s,"
"\"submit_to_body_frames\":%lld,\"body_update_to_future_callback_s\":%s,"
"\"future_callback_to_gt_s\":%s,\"suppressed_to_ready_s\":%s,"
"\"mesh_submit_to_ready_s\":%s,\"collision_request_to_ready_s\":%s"),
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, ResultName,
CompletedMesh->GetBodySetup() != nullptr ? 1 : 0, bBodyInstalled ? 1 : 0,
bBodyUpdatedEventSeen ? 1 : 0,
static_cast<int32>(WorkPriority),
WorkPriority == EVoxelTileWorkPriority::CollisionCritical ? 1 : 0,
NumVertices, NumTriangles, NumSkirtTriangles, PendingCooksAtSubmit,
*FString::SanitizeFloat(RequestToWorkerStartSeconds),
*FString::SanitizeFloat(WorkerGenerationSeconds),
*FString::SanitizeFloat(WorkerResultToApplySeconds),
*FString::SanitizeFloat(ApplyToMeshSubmitSeconds),
*FString::SanitizeFloat(MeshSubmitToCollisionSubmitSeconds),
*FString::SanitizeFloat(RMCSubmitToBodyUpdateSeconds), FramesToBodyUpdate,
*FString::SanitizeFloat(BodyUpdateToFutureCallbackSeconds),
*FString::SanitizeFloat(FutureCallbackToGameThreadSeconds),
*FString::SanitizeFloat(SuppressedToReadySeconds),
*FString::SanitizeFloat(MeshToCookSeconds),
*FString::SanitizeFloat(CollisionRequestToCookSeconds),
*FString::SanitizeFloat(BodyEventToFutureSeconds)));
*FString::SanitizeFloat(CollisionRequestToCookSeconds)));
}
// A completion can occur between world ticks. Refresh immediately so the pawn is released on
@@ -7532,6 +7694,8 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
// An empty re-gen invalidates any previous body and any completion future for the old
// geometry. Empty tiles are loaded bookkeeping, never collision-ready.
CollisionReadyTiles.Remove(Result.Tile);
CollisionSuppressedUntilCook.Remove(Result.Tile);
CollisionSuppressedAtCycles.Remove(Result.Tile);
CollisionSolidTiles.Remove(Result.Tile);
if (Result.Tile.Level == 0)
{
@@ -8904,7 +9068,9 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
// away + skirts hide the seams). Near levels (< FullResClipLevels) stay full CHUNK_SIZE cells.
// Extent stays CHUNK_SIZE<<Level (so the shell tiling is unchanged) — only the cell count and
// step change: Step = Extent / Cells.
const int32 FullRes = Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2;
const int32 FullRes = GVoxelForgeTestFullResClipLevels >= 0
? FMath::Clamp(GVoxelForgeTestFullResClipLevels, 1, 9)
: (Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2);
const int32 Extent = CHUNK_SIZE << Tile.Level;
const int32 Cells = (Tile.Level < FullRes)
@@ -9526,6 +9692,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
const int32 NumTris = MeshData.Triangles.Num() / 3;
Result.NumTriangles = NumTris;
Result.NumCeilingTriangles = MeshData.NumCeilingTriangles;
Result.NumSkirtTriangles = MeshData.NumSkirtTriangles;
Result.bHasCeilingTris = MeshData.NumCeilingTriangles > 0;
Result.bHasGroundTris = NumTris > MeshData.NumCeilingTriangles;
}
@@ -9646,6 +9813,8 @@ void AVoxelWorld::UnloadTile(const FVoxelTileKey& Tile)
(*Cancellation)->bObsolete.store(true, std::memory_order_release);
}
CollisionReadyTiles.Remove(Tile);
CollisionSuppressedUntilCook.Remove(Tile);
CollisionSuppressedAtCycles.Remove(Tile);
CollisionNotRequiredTiles.Remove(Tile);
CollisionSolidTiles.Remove(Tile);
PendingCollisionCooks.Remove(Tile);
@@ -9682,13 +9851,14 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
const bool bHasCeilingTris = Result.bHasCeilingTris;
const bool bLevel0 = (Tile.Level == 0);
const bool bWantsCollision = bLevel0 && IsTileCollisionDesired(Tile);
const bool bCollisionSuppressed = CollisionSuppressedUntilCook.Contains(Tile);
// RMC keeps its installed BodySetup active while the replacement collision cooks. Preserve
// support readiness across that interval when this tile already has a live cooked body; the
// cook-complete callback below replaces the body and decides readiness for the new geometry.
// A first cook, an unavailable body, or a tile leaving collision range still withdraws ready.
URealtimeMeshComponent* ExistingCollisionComponent = TileComponents.FindRef(Tile);
const bool bRetainInstalledCollisionReady = bWantsCollision
const bool bRetainInstalledCollisionReady = bWantsCollision && !bCollisionSuppressed
&& CollisionReadyTiles.Contains(Tile)
&& IsValid(ExistingCollisionComponent)
&& ExistingCollisionComponent->GetCollisionEnabled() != ECollisionEnabled::NoCollision
@@ -9819,7 +9989,9 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
MeshComp->SetVisibility(
!CollisionOnlyTiles.Contains(Tile) && !bDeferTransitionVisibility);
MeshComp->SetCollisionEnabled(
bWantsCollision ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision);
bWantsCollision && !bCollisionSuppressed
? ECollisionEnabled::QueryAndPhysics
: ECollisionEnabled::NoCollision);
// Reuse the component's existing mesh object when it has one (pooled component or carve
// re-mesh) — InitializeRealtimeMesh allocates a brand-new URealtimeMesh EVERY call, so
@@ -9920,10 +10092,21 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
Pending.SubmissionId = SubmissionId;
Pending.MeshSubmittedCycles = MeshSubmittedCycles;
Pending.CollisionSubmittedCycles = CollisionSubmittedCycles;
Pending.CollisionSubmittedFrame = GFrameCounter;
Pending.RequestStartCycles = Result.RequestStartCycles;
Pending.GenerationStartCycles = Result.GenerationStartCycles;
Pending.GenerationEndCycles = Result.GenerationEndCycles;
Pending.ApplyStartCycles = Result.ApplyStartCycles;
Pending.WorkPriority = Result.WorkPriority;
Pending.NumVertices = Result.NumVertices;
Pending.NumTriangles = Result.NumTriangles;
Pending.NumSkirtTriangles = Result.NumSkirtTriangles;
Pending.PendingCooksAtSubmit = PendingCollisionCooks.Num();
Pending.bUseAsyncCook = CollisionConfig.bUseAsyncCook;
Pending.bUseComplexAsSimpleCollision = CollisionConfig.bUseComplexAsSimpleCollision;
Pending.bShouldFastCookMeshes = CollisionConfig.bShouldFastCookMeshes;
Pending.bMergeAllMeshes = CollisionConfig.bMergeAllMeshes;
Pending.SuppressedAtCycles = CollisionSuppressedAtCycles.FindRef(Tile);
Pending.bRecordStreamingLatency = Result.bRecordStreamingLatency;
PendingCollisionCooks.Add(Tile, Pending);
@@ -9941,7 +10124,8 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
if (AVoxelWorld* World = WeakWorld.Get())
{
World->HandleTileCollisionCookComplete(
Tile, SubmissionId, WeakMesh.Get(), static_cast<uint8>(CollisionResult));
Tile, SubmissionId, WeakMesh.Get(), static_cast<uint8>(CollisionResult),
FPlatformTime::Cycles64());
}
});
}
@@ -10072,6 +10256,72 @@ void AVoxelWorld::ApplyModification(const FVoxelModification& Modification)
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DiffStartCycles)
: 0.0;
// If a carve removes the surface directly beneath this pawn, an installed old BodySetup would
// otherwise keep it standing on geometry that the diff has already removed. Suppress only the
// current support tile, and only when the brush bounds intersect the pawn bounds; nearby digs
// keep their existing same-tile recook handoff.
if (Modification.Strength < 0.0f && AffectedChunks.Num() > 0)
{
FVector PlayerPosition = FVector::ZeroVector;
APawn* Pawn = nullptr;
FVoxelTileKey SupportTile;
if (TryGetPlayerPosition(PlayerPosition, &Pawn)
&& GetPlayerSupportTile(Pawn, PlayerPosition, SupportTile)
&& AffectedChunks.Contains(SupportTile.Coord)
&& IsTileCollisionReady(SupportTile))
{
FVector EditMinVoxel, EditMaxVoxel;
Modification.GetWorldBounds(EditMinVoxel, EditMaxVoxel);
FBox EditWorldBounds(ForceInit);
for (int32 Corner = 0; Corner < 8; ++Corner)
{
const FVector LocalVoxel(
(Corner & 1) != 0 ? EditMaxVoxel.X : EditMinVoxel.X,
(Corner & 2) != 0 ? EditMaxVoxel.Y : EditMinVoxel.Y,
(Corner & 4) != 0 ? EditMaxVoxel.Z : EditMinVoxel.Z);
EditWorldBounds += LocalVoxelToWorld(LocalVoxel);
}
FBox PawnWorldBounds(ForceInit);
if (const ACharacter* Character = Cast<ACharacter>(Pawn))
{
if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent())
{
PawnWorldBounds = Capsule->Bounds.GetBox();
}
}
if (!PawnWorldBounds.IsValid)
{
FVector PawnBoundsOrigin = FVector::ZeroVector;
FVector PawnBoundsExtent = FVector::ZeroVector;
Pawn->GetActorBounds(true, PawnBoundsOrigin, PawnBoundsExtent);
PawnWorldBounds = FBox(
PawnBoundsOrigin - PawnBoundsExtent,
PawnBoundsOrigin + PawnBoundsExtent);
}
if (EditWorldBounds.Intersect(PawnWorldBounds))
{
if (URealtimeMeshComponent* SupportComponent = TileComponents.FindRef(SupportTile))
{
CollisionSuppressedUntilCook.Add(SupportTile);
CollisionSuppressedAtCycles.Add(SupportTile, FPlatformTime::Cycles64());
CollisionReadyTiles.Remove(SupportTile);
// A prior cook can still complete after this edit. Remove its serial so that
// its completion cannot restore readiness over the replacement geometry.
PendingCollisionCooks.Remove(SupportTile);
SupportComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeUnderfootCollisionSuppressed] tile=(%d,%d,%d) "
"frame=%llu brush_capsule_bounds_intersect=1"),
SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z,
static_cast<unsigned long long>(GFrameCounter));
UpdatePawnCollisionGate(Pawn, PlayerPosition, 0.0f);
}
}
}
}
if (AffectedChunks.Num() > 0)
{
++ModificationEpoch;
+6
View File
@@ -374,6 +374,11 @@ struct FVoxelMeshData
// this prefix and must not be used as witnesses for terrain visibility audits.
int32 NumSurfaceVertices = 0;
// Diagnostic-only count: skirt triangles are render geometry and are included in the RMC
// sections that currently request collision. This lets collision profiling quantify their
// share without changing the emitted indices.
int32 NumSkirtTriangles = 0;
// F17 — classe de surface par TRIANGLE, portée par l'ORDRE du buffer d'indices : les
// NumCeilingTriangles DERNIERS triangles de Triangles sont la classe "plafond sky-cap"
// (polygroup 1 → slot matériau 1, sans ombre) ; tout ce qui précède est "sol/roche"
@@ -388,6 +393,7 @@ struct FVoxelMeshData
Normals.Empty();
Colors.Empty();
NumSurfaceVertices = 0;
NumSkirtTriangles = 0;
NumCeilingTriangles = 0;
}
+22 -2
View File
@@ -117,7 +117,7 @@ struct FChunkResult
uint64 GenerationEndCycles = 0;
uint64 ApplyStartCycles = 0;
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0; int32 NumCeilingTriangles = 0;
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0; int32 NumCeilingTriangles = 0; int32 NumSkirtTriangles = 0;
int32 NumVertices = 0;
uint64 GeometryBytes = 0;
bool bSealedSolidProof = false; // true when the cheap gap/seal proof skipped meshing
@@ -270,6 +270,11 @@ public:
* submission and LoadedTiles are intentionally not sufficient. */
TSet<FVoxelTileKey> CollisionReadyTiles;
/** A carve intersected the pawn's current support tile. Its previous body is withdrawn
* immediately and readiness returns only after the replacement cook is installed. */
TSet<FVoxelTileKey> CollisionSuppressedUntilCook;
TMap<FVoxelTileKey, uint64> CollisionSuppressedAtCycles;
/** Level-0 tiles whose applied result is known to contain no surface (uniform empty/solid).
* They need no BodySetup themselves, but the pawn gate may walk past them while looking for
* the first resolved collidable support tile. */
@@ -285,13 +290,27 @@ public:
uint64 SubmissionId = 0;
uint64 MeshSubmittedCycles = 0;
uint64 CollisionSubmittedCycles = 0;
uint64 CollisionSubmittedFrame = 0;
bool bBodyUpdatedEventSeen = false;
uint64 BodyUpdatedEventCycles = 0;
uint64 BodyUpdatedEventFrame = 0;
uint64 FutureCallbackCycles = 0;
uint64 CompletionGameThreadCycles = 0;
// Game-thread-only timing copied from the generation result for the clean LOD0 summary.
uint64 RequestStartCycles = 0;
uint64 GenerationStartCycles = 0;
uint64 GenerationEndCycles = 0;
uint64 ApplyStartCycles = 0;
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
int32 NumVertices = 0;
int32 NumTriangles = 0;
int32 NumSkirtTriangles = 0;
int32 PendingCooksAtSubmit = 0;
bool bUseAsyncCook = true;
bool bUseComplexAsSimpleCollision = true;
bool bShouldFastCookMeshes = false;
bool bMergeAllMeshes = false;
uint64 SuppressedAtCycles = 0;
bool bRecordStreamingLatency = false;
};
@@ -896,7 +915,8 @@ public:
/** Called by the RMC collision future after async cook + BodySetup installation. */
void HandleTileCollisionCookComplete(const FVoxelTileKey& Tile, uint64 SubmissionId,
URealtimeMesh* Mesh, uint8 Result);
URealtimeMesh* Mesh, uint8 Result,
uint64 FutureCallbackCycles);
void BindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh);
void UnbindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh);
void HandleRealtimeMeshCollisionBodyUpdated(URealtimeMesh* Mesh, UBodySetup* BodySetup);