The round the owner asked for, with three corrections from Codex that are worth more
than the features.
WHAT LANDED
- The complete tunnel floor profile is authored into FCachedTunnel during
BuildChunkCache, not derived per sample. This was his point: "it should be done at
creation time, since it knows its destination."
- Room floor ownership blends/clips the tunnel floor at mouths -- his A > B boolean.
- The traversal validator gained a climb tier, structured exactly as agreed:
bWalkOnlyCanReachDeparture / bClimbCanReachDeparture kept SEPARATE, with
WalkOnly / Climb / MineRequired cell counts reported alongside.
The comment in the header states the contract: "The primordial capability contract
is walk-only... climb is reported as a separate tier and never makes the hard
connectivity verdict pass." That is the distinction we wanted: reporting tiers, not
permission.
- Floor controls exposed under Cave Morphology|Tunnels|Floor.
⚠️ CORRECTION 1 -- LARGE LEDGES ARE NOT CAPABILITY-SAFE YET, so they stay opt-in.
default (preference 0) fit 20,804 walk reach 10,884 0 ledges
preference 2 fit 20,765 walk reach 10,712 360 ledges
Ledges cost 172 walk-reachable cells. Codex kept preference 0 (the old staircase) as
the default rather than shipping the owner's preferred look at the price of walkable
space, and led its report with that. Correct call.
⚠️ CORRECTION 2 -- THE BACKSTOP STILL CANNOT GO. Support/room fallback counters were
zero, but the structural protection clamp fired on 31,017 of 31,017 authored floor
samples -- 100%. So the shape is still not correct by construction; the clamp is
doing the work. Instrumenting it was the right move: we now have a number instead of
an argument.
⚠️ CORRECTION 3 -- THE "X + 2A" CLEARANCE BUDGET IS NOT IMPLEMENTABLE AS I SPECIFIED.
Interval propagation bounds FIELD VALUES, not SPATIAL DISPLACEMENT. Deriving A needs
a proven Lipschitz/influence bound for every post-operation, which we do not have.
Both the owner and I assumed the interval work already gave us this; it does not.
The idea is still right, the prerequisite is a displacement bound.
MEASURED BY ME: build 0 errors; export 1.365 / 1.357 / 1.420 s (checkpoint was
1.321 / 1.357), flat within noise; 153,346 triangles.
ALSO MEASURED THIS ROUND
- Edit cost: 8 affected chunks, 195.5 ms total -- generation 195.471 ms, apply 0.059,
diff 0.020, queue 0.001. It is full level-0 regeneration; a dirty-mesh path needs
partial section support that is unavailable without modifying RMC.
- Streaming caps: effective generation cap 14, pending max 14, CAP HIT. Mesh applies
per frame 4 (not hit), 2 ms apply budget (not hit), unloads 6 (not hit).
⇒ The generation core cap is what binds during startup fill.
- Peak export RSS 1.70 GB (was 19.5 GB before the memory work).
- LOD4 empty outliers gone. LOD0 not solved: 4,600 empty tiles at 3 us median, but 11
mixed-classifier fallbacks over 100 ms, max 554 ms -- verdict=Mixed, then the full
mesher, producing no triangles.
⚠️ UNVERIFIED AND CONCERNING: the reported in-game meshed times are far worse than
412a7ad's -- p50 498.8 ms vs 115 ms, p95 1.227 s vs 410 ms, max 1.974 s vs 844 ms.
The samples differ (9,336 ready events vs 841 requests, different traversal, possibly
queue wait included), and the export path is flat, so I do not know whether this is a
real regression or a different measurement. I have NOT reproduced it myself. It is
the first thing the next round must settle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
290 lines
10 KiB
C++
290 lines
10 KiB
C++
// Opt-in density-path profiling used by the editor commandlet and targeted PIE diagnostics.
|
|
// Disabled by default so the shipping density path pays only one predictable branch in the
|
|
// scoped timers below.
|
|
|
|
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "HAL/PlatformTime.h"
|
|
|
|
#include <atomic>
|
|
|
|
namespace VoxelDensityProfile
|
|
{
|
|
enum class EMode : uint8
|
|
{
|
|
Disabled,
|
|
Sampled,
|
|
Full,
|
|
};
|
|
|
|
// Count-based sampling is deterministic with respect to the profiler's own counters, but the
|
|
// report is intentionally diagnostic only. It never participates in density or mesh output.
|
|
constexpr uint32 DefaultSampleInterval = 64;
|
|
|
|
enum class EBucket : uint8
|
|
{
|
|
GetDensityAt,
|
|
ApplyDisturbances,
|
|
PassageModifier,
|
|
PassageLandingAir,
|
|
PassageLandingFloor,
|
|
PassageLandingRoomFloor,
|
|
PassageTunnelAir,
|
|
PassageStructuralPosts,
|
|
TunnelCoreSupport,
|
|
TunnelCoreSdf,
|
|
TunnelCorePosts,
|
|
StructuralTail,
|
|
RoomGraphSource,
|
|
SdfCarve,
|
|
CaveRoughnessMod,
|
|
CaveTerraceMod,
|
|
LayerLineMod,
|
|
RibbingMod,
|
|
CaveOverhangMod,
|
|
CaveCliffMod,
|
|
ScallopMod,
|
|
CaveArchMod,
|
|
RoomColumnMod,
|
|
DomeMod,
|
|
PinchMod,
|
|
FloorBiasMod,
|
|
WormFieldSource,
|
|
OriginSpineOp,
|
|
BoundarySealOp,
|
|
PassageCarveOp,
|
|
XYEdgeSealOp,
|
|
OtherOp,
|
|
// Non-overlapping top-level phases. The named operation buckets above are intentionally
|
|
// nested diagnostics; these phase buckets are the accounting ledger that must add up to
|
|
// GetDensityAt without double-counting a stack op inside its enclosing phase.
|
|
DensityPrologue,
|
|
DensityCore,
|
|
DensityDisturbances,
|
|
DensityStructuralPosts,
|
|
DensityBoundarySeal,
|
|
DensityDiffLayer,
|
|
DensityTail,
|
|
// Mesher-side accounting. GenerateMesh is the inclusive total; the following buckets
|
|
// cover its regular-grid work and its non-grid setup/finalisation.
|
|
MesherGenerateMesh,
|
|
MesherDensityGrid,
|
|
MesherCellClassification,
|
|
MesherGradientNormals,
|
|
MesherVertexInterpolation,
|
|
MesherStreamBuilding,
|
|
MesherOther,
|
|
Count
|
|
};
|
|
|
|
constexpr int32 BucketCount = static_cast<int32>(EBucket::Count);
|
|
|
|
enum class ECounter : uint8
|
|
{
|
|
TunnelCacheLookup,
|
|
TunnelCacheHit,
|
|
TunnelCacheMiss,
|
|
SdfCacheBuild,
|
|
CaveRoomCandidates,
|
|
CaveRoomEvaluated,
|
|
CaveTunnelCandidates,
|
|
CaveTunnelEvaluated,
|
|
TunnelCoreCandidates,
|
|
TunnelCoreEvaluated,
|
|
TunnelSupportColumnBuilds,
|
|
TunnelSupportColumnCandidates,
|
|
TunnelSupportFloorQueries,
|
|
TunnelSupportFloorChecks,
|
|
TunnelSupportFloorBackstopFires,
|
|
TunnelRoomFloorBackstopFires,
|
|
TunnelAuthoredFloorSamples,
|
|
TunnelAuthoredFloorProtectionClamps,
|
|
TunnelAuthoredFloorDisturbanceSkips,
|
|
TunnelFloorProfileFallbacks,
|
|
TunnelFloorProfileBuilds,
|
|
TunnelFloorProfileSegments,
|
|
TunnelFloorProfileLedges,
|
|
TunnelFloorProfileLedgesBelow2Voxels,
|
|
TunnelFloorProfileLedges2To4Voxels,
|
|
TunnelFloorProfileLedges4To8Voxels,
|
|
TunnelFloorProfileLedges8PlusVoxels,
|
|
PassageCandidates,
|
|
PassageEvaluated,
|
|
MesherBlockTests,
|
|
MesherBlockAllSolid,
|
|
MesherBlockAllAir,
|
|
Count
|
|
};
|
|
|
|
constexpr int32 CounterCount = static_cast<int32>(ECounter::Count);
|
|
|
|
// Resident allocator-backed bytes for one worker's generation cache family. The profiler
|
|
// aggregates this by worker; the fields intentionally describe allocations owned by the
|
|
// cache, not the inline size of the enclosing C++ object. SlotStorageBytes is the one
|
|
// exception: it is the inline/static storage reserved for the worker's fixed cache table.
|
|
struct FCacheMemoryBreakdown
|
|
{
|
|
uint64 SlotStorageBytes = 0;
|
|
uint64 OpStackBytes = 0;
|
|
uint64 RoomsBytes = 0;
|
|
uint64 RoomFloorJoinsBytes = 0;
|
|
uint64 TunnelsBytes = 0;
|
|
uint64 PitsBytes = 0;
|
|
uint64 ChimneysBytes = 0;
|
|
uint64 ColumnsBytes = 0;
|
|
uint64 SupportColumnEntriesBytes = 0;
|
|
uint64 SupportColumnsBytes = 0;
|
|
uint64 SupportColumnIntervalsBytes = 0;
|
|
uint64 TotalBytes() const
|
|
{
|
|
return SlotStorageBytes + OpStackBytes
|
|
+ RoomsBytes + RoomFloorJoinsBytes + TunnelsBytes + PitsBytes
|
|
+ ChimneysBytes + ColumnsBytes + SupportColumnEntriesBytes
|
|
+ SupportColumnsBytes + SupportColumnIntervalsBytes;
|
|
}
|
|
|
|
uint64 DynamicBytes() const
|
|
{
|
|
return TotalBytes() - SlotStorageBytes;
|
|
}
|
|
|
|
FCacheMemoryBreakdown& operator+=(const FCacheMemoryBreakdown& Other)
|
|
{
|
|
SlotStorageBytes += Other.SlotStorageBytes;
|
|
OpStackBytes += Other.OpStackBytes;
|
|
RoomsBytes += Other.RoomsBytes;
|
|
RoomFloorJoinsBytes += Other.RoomFloorJoinsBytes;
|
|
TunnelsBytes += Other.TunnelsBytes;
|
|
PitsBytes += Other.PitsBytes;
|
|
ChimneysBytes += Other.ChimneysBytes;
|
|
ColumnsBytes += Other.ColumnsBytes;
|
|
SupportColumnEntriesBytes += Other.SupportColumnEntriesBytes;
|
|
SupportColumnsBytes += Other.SupportColumnsBytes;
|
|
SupportColumnIntervalsBytes += Other.SupportColumnIntervalsBytes;
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
struct FSnapshot
|
|
{
|
|
uint64 Calls[BucketCount]{};
|
|
// Sum of sampled worker time. Useful for CPU-work accounting, but not directly comparable
|
|
// with an export wall clock when the mesher runs in parallel.
|
|
uint64 Cycles[BucketCount]{};
|
|
uint64 Samples[BucketCount]{};
|
|
// Largest estimated worker contribution. This is the report-facing parallel wall
|
|
// estimate; it avoids mistaking several parallel workers for several exports.
|
|
uint64 WallCycles[BucketCount]{};
|
|
uint32 ActiveWorkers[BucketCount]{};
|
|
uint64 Counters[CounterCount]{};
|
|
|
|
uint64 TimerPairCycles = 0;
|
|
uint32 SampleInterval = DefaultSampleInterval;
|
|
EMode Mode = EMode::Disabled;
|
|
|
|
// Opt-in worker-cache footprint snapshot. These are current per-thread values (not
|
|
// allocation-event counters), so Snapshot() reports the resident cache footprint at the
|
|
// end of a profiling run rather than multiplying bytes by cache churn.
|
|
uint64 TunnelCacheWorkers = 0;
|
|
uint64 TunnelCacheCapacityEntries = 0;
|
|
uint64 TunnelCacheValidEntries = 0;
|
|
uint64 TunnelCacheStaticBytes = 0;
|
|
uint64 TunnelCacheDynamicBytes = 0;
|
|
uint64 TunnelCacheEntryBytes = 0;
|
|
uint64 TunnelCacheLargestEntryBytes = 0;
|
|
uint64 TunnelCacheLargestWorkerValidEntries = 0;
|
|
uint64 TunnelCacheLargestWorkerBytes = 0;
|
|
FCacheMemoryBreakdown TunnelCacheBreakdown;
|
|
|
|
uint64 RoomGraphCacheWorkers = 0;
|
|
uint64 RoomGraphCacheCapacityEntries = 0;
|
|
uint64 RoomGraphCacheValidEntries = 0;
|
|
uint64 RoomGraphCacheStaticBytes = 0;
|
|
uint64 RoomGraphCacheDynamicBytes = 0;
|
|
uint64 RoomGraphCacheEntryBytes = 0;
|
|
uint64 RoomGraphCacheLargestEntryBytes = 0;
|
|
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
|
|
uint64 RoomGraphCacheLargestWorkerBytes = 0;
|
|
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
|
|
};
|
|
|
|
VOXELFORGE_API void SetEnabled(bool bEnabled);
|
|
VOXELFORGE_API void SetMode(EMode Mode, uint32 SampleInterval = DefaultSampleInterval);
|
|
// Coarse counters are intentionally always collected, even when cycle timing is disabled.
|
|
// They are the low-cost diagnostic path; only cycle timing is opt-in.
|
|
FORCEINLINE bool AreCountersEnabled() { return true; }
|
|
VOXELFORGE_API bool IsEnabled();
|
|
VOXELFORGE_API bool IsCycleTimingEnabled();
|
|
VOXELFORGE_API EMode GetMode();
|
|
VOXELFORGE_API uint32 GetSampleInterval();
|
|
VOXELFORGE_API uint64 GetTimerPairCycles();
|
|
VOXELFORGE_API void CalibrateTimer();
|
|
VOXELFORGE_API void Reset();
|
|
VOXELFORGE_API FSnapshot Snapshot();
|
|
VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1);
|
|
VOXELFORGE_API void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls = 1);
|
|
VOXELFORGE_API void RecordCall(EBucket Bucket);
|
|
VOXELFORGE_API bool ShouldSample(EBucket Bucket);
|
|
VOXELFORGE_API void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles);
|
|
|
|
VOXELFORGE_API void SetWorkerTunnelCacheFootprint(
|
|
uint64 CapacityEntries, uint64 ValidEntries,
|
|
uint64 StaticBytes, uint64 DynamicBytes,
|
|
uint64 EntryBytes, uint64 LargestEntryBytes,
|
|
uint64 LargestWorkerValidEntries,
|
|
const FCacheMemoryBreakdown& Breakdown);
|
|
VOXELFORGE_API void SetWorkerRoomGraphCacheFootprint(
|
|
uint64 CapacityEntries, uint64 ValidEntries,
|
|
uint64 StaticBytes, uint64 DynamicBytes,
|
|
uint64 EntryBytes, uint64 LargestEntryBytes,
|
|
uint64 LargestWorkerValidEntries,
|
|
const FCacheMemoryBreakdown& Breakdown);
|
|
VOXELFORGE_API const TCHAR* CounterName(ECounter Counter);
|
|
VOXELFORGE_API EBucket BucketFromName(const TCHAR* Name);
|
|
VOXELFORGE_API const TCHAR* BucketName(EBucket Bucket);
|
|
|
|
struct FScopeToken
|
|
{
|
|
bool bEntered = false;
|
|
bool bSampled = false;
|
|
bool bBlockTiming = false;
|
|
bool bPreviousSampled = false;
|
|
bool bPreviousSampledScopeActive = false;
|
|
uint64 StartCycles = 0;
|
|
};
|
|
|
|
VOXELFORGE_API FScopeToken BeginScope(EBucket Bucket);
|
|
VOXELFORGE_API void EndScope(EBucket Bucket, const FScopeToken& Token);
|
|
|
|
class FScopedTimer final
|
|
{
|
|
public:
|
|
explicit FScopedTimer(EBucket InBucket)
|
|
: Bucket(InBucket)
|
|
, Token(BeginScope(InBucket))
|
|
{
|
|
}
|
|
|
|
explicit FScopedTimer(const TCHAR* InName)
|
|
: Bucket(EBucket::OtherOp)
|
|
{
|
|
Bucket = BucketFromName(InName);
|
|
Token = BeginScope(Bucket);
|
|
}
|
|
|
|
~FScopedTimer();
|
|
|
|
// End a range before its lexical scope ends. This lets GetDensityAt split its enclosing
|
|
// total into disjoint prologue/core/post phases without moving the large cache block.
|
|
void End();
|
|
|
|
FScopedTimer(const FScopedTimer&) = delete;
|
|
FScopedTimer& operator=(const FScopedTimer&) = delete;
|
|
|
|
private:
|
|
EBucket Bucket = EBucket::OtherOp;
|
|
FScopeToken Token;
|
|
};
|
|
}
|