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:
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user