feat(tunnels): floor authored at BUILD time, room clips tunnel, climb tier added
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
This commit is contained in:
1 parent
e91f1c44bb
commit
8e42d6d807
15 files changed
+706
-95
No files matched your search
@@ -31,6 +31,7 @@
|
||||
#include "IImageWrapperModule.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
||||
@@ -47,6 +48,18 @@ namespace
|
||||
TEXT("voxel.ProfileTileGeneration"),
|
||||
GVoxelForgeProfileTileGeneration,
|
||||
TEXT("Log worker tile generation time with level/step/cell count."));
|
||||
|
||||
// Startup command-line cvars can arrive before the game world has spawned AVoxelWorld (or
|
||||
// before its first tiles have become visible). Keep the diagnostic edit pending until Tick has
|
||||
// a loaded tile; otherwise a startup measurement silently becomes a no-op or edits the wrong
|
||||
// position.
|
||||
int32 GVoxelForgePendingTestModification = 0;
|
||||
float GVoxelForgePendingModificationRadius = 3.0f;
|
||||
float GVoxelForgePendingModificationStrength = 10.0f;
|
||||
bool GVoxelForgeTestModificationCommandLineConsumed = false;
|
||||
bool GVoxelForgeStreamingBudgetReported = false;
|
||||
bool GVoxelForgeGenerationCapHitReported = false;
|
||||
int32 GVoxelForgeMaxPendingTilesObserved = 0;
|
||||
}
|
||||
|
||||
AVoxelWorld::AVoxelWorld()
|
||||
@@ -1075,7 +1088,52 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
// Independent of the density volume (self-guards on OrbLightMPC); this is the replacement path.
|
||||
UpdateOrbLightMPC();
|
||||
}
|
||||
// The diagnostic harness uses an explicit command-line token rather than a console command:
|
||||
// Unreal processes startup ExecCmds before this runtime module's console registrations are
|
||||
// guaranteed to exist. Reading the command line here is order-independent, and the actual edit
|
||||
// still waits for a loaded tile and goes through the ordinary modification path below.
|
||||
if (!GVoxelForgeTestModificationCommandLineConsumed)
|
||||
{
|
||||
GVoxelForgeTestModificationCommandLineConsumed = true;
|
||||
const FString CommandLine(FCommandLine::Get());
|
||||
if (CommandLine.Contains(TEXT("-voxel.TestModification=1"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
GVoxelForgePendingModificationRadius = 3.0f;
|
||||
GVoxelForgePendingModificationStrength = 10.0f;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTestModification] queued radius=%.2f strength=%.2f"),
|
||||
GVoxelForgePendingModificationRadius,
|
||||
GVoxelForgePendingModificationStrength);
|
||||
GVoxelForgePendingTestModification = 1;
|
||||
}
|
||||
}
|
||||
ProcessPendingChunks();
|
||||
|
||||
// Complete the one-shot diagnostic request only after the player's level-0 centre tile is
|
||||
// loaded and idle. Waiting for any coarse tile would take the async-neighbour branch and would
|
||||
// not measure the normal synchronous centre remesh that makes a local edit feel expensive.
|
||||
FVector ModificationPosition = PlayerLastPos;
|
||||
if (ModificationPosition == FVector::ZeroVector)
|
||||
{
|
||||
ModificationPosition = GetActorLocation();
|
||||
}
|
||||
const FVoxelTileKey ModificationCenterTile(
|
||||
WorldToChunkCoord(WorldToLocalCm(ModificationPosition)), 0);
|
||||
if (GVoxelForgePendingTestModification != 0
|
||||
&& DiffLayer
|
||||
&& LoadedTiles.Contains(ModificationCenterTile)
|
||||
&& !PendingTiles.Contains(ModificationCenterTile))
|
||||
{
|
||||
GVoxelForgePendingTestModification = 0;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTestModification] applying position=(%.2f,%.2f,%.2f) radius=%.2f strength=%.2f"),
|
||||
ModificationPosition.X, ModificationPosition.Y, ModificationPosition.Z,
|
||||
GVoxelForgePendingModificationRadius, GVoxelForgePendingModificationStrength);
|
||||
CarveAtPosition(
|
||||
ModificationPosition,
|
||||
GVoxelForgePendingModificationRadius,
|
||||
GVoxelForgePendingModificationStrength);
|
||||
}
|
||||
ProcessUnloadQueue();
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
@@ -1322,6 +1380,9 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
||||
{
|
||||
if (!Generator || !Mesher || ShouldAbortWork()) return;
|
||||
|
||||
const bool bProfileModification = GVoxelForgeProfileTileGeneration != 0;
|
||||
const uint64 SyncStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
|
||||
|
||||
const FIntVector OriginVoxels = Tile.OriginVoxels();
|
||||
const int32 Cells = CHUNK_SIZE; // level 0 is always full-res (level 0 < FullResClipLevels)
|
||||
const int32 Step = 1; // Extent(=CHUNK_SIZE) / Cells
|
||||
@@ -1341,14 +1402,30 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
||||
}
|
||||
|
||||
FChunkResult Result;
|
||||
Result.RequestStartCycles = GVoxelForgeProfileTileGeneration != 0
|
||||
Result.RequestStartCycles = bProfileModification
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
const uint64 GenerateStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
|
||||
GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false,
|
||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||
/*bSheetTile*/ false, /*SheetChunkZ*/ 0,
|
||||
/*Hole*/ 0, 0, 0, 0, Result); // hole unused (not a sheet tile)
|
||||
|
||||
const double GenerateSeconds = bProfileModification
|
||||
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - GenerateStartCycles)
|
||||
: 0.0;
|
||||
const uint64 ApplyStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
|
||||
ApplyTileResult(Result);
|
||||
if (bProfileModification)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeSyncRemeshProfile] tile=(%d,%d,%d) empty=%d generation=%.6f "
|
||||
"apply=%.6f total=%.6f"),
|
||||
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z,
|
||||
(Result.bEmpty || !Result.Streams) ? 1 : 0,
|
||||
GenerateSeconds,
|
||||
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ApplyStartCycles),
|
||||
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - SyncStartCycles));
|
||||
}
|
||||
}
|
||||
|
||||
void AVoxelWorld::ProcessUnloadQueue()
|
||||
@@ -1717,18 +1794,46 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
|
||||
|
||||
int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
||||
{
|
||||
// T2.d — the asset value, capped to the spare LOGICAL cores. BackgroundNormal priority
|
||||
// (see LoadTile) already stops gen from starving the frame; this cap stops a flat 16 from
|
||||
// thrashing context switches on small CPUs where 16 > the machine's spare parallelism.
|
||||
// T2.d — both sides of the cap are authored in Voxel|Streaming. BackgroundNormal priority
|
||||
// (see LoadTile) already stops gen from starving the frame; this cap stops a flat task count
|
||||
// from thrashing context switches on small CPUs where it exceeds spare parallelism.
|
||||
const int32 Asset = Settings ? Settings->MaxConcurrentTasks : 16;
|
||||
const int32 SpareCores = FMath::Max(2, FPlatformMisc::NumberOfCoresIncludingHyperthreads() - 2);
|
||||
return FMath::Clamp(Asset, 1, SpareCores);
|
||||
const int32 LogicalCores = FMath::Max(
|
||||
1, FPlatformMisc::NumberOfCoresIncludingHyperthreads());
|
||||
const int32 AutomaticCap = FMath::Max(2, LogicalCores - 2);
|
||||
const int32 AuthoredCoreCap = Settings ? Settings->MaxGenerationWorkerCores : 0;
|
||||
const int32 CoreCap = AuthoredCoreCap > 0
|
||||
? FMath::Clamp(AuthoredCoreCap, 1, LogicalCores)
|
||||
: AutomaticCap;
|
||||
return FMath::Clamp(Asset, 1, CoreCap);
|
||||
}
|
||||
|
||||
void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks);
|
||||
const int32 MaxTasks = GetMaxConcurrentTasks();
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& !GVoxelForgeStreamingBudgetReported)
|
||||
{
|
||||
const int32 LogicalCores = FMath::Max(
|
||||
1, FPlatformMisc::NumberOfCoresIncludingHyperthreads());
|
||||
const int32 AutomaticCap = FMath::Max(2, LogicalCores - 2);
|
||||
const int32 AssetCap = Settings ? Settings->MaxConcurrentTasks : 16;
|
||||
const int32 AuthoredCoreCap = Settings ? Settings->MaxGenerationWorkerCores : 0;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d "
|
||||
"authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d "
|
||||
"max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"),
|
||||
LogicalCores,
|
||||
AutomaticCap,
|
||||
AuthoredCoreCap,
|
||||
AssetCap,
|
||||
MaxTasks,
|
||||
Settings ? Settings->MaxMeshAppliesPerFrame : 4,
|
||||
Settings ? Settings->MaxMeshApplyMilliseconds : 2.0f,
|
||||
Settings ? Settings->MaxUnloadsPerFrame : 6);
|
||||
GVoxelForgeStreamingBudgetReported = true;
|
||||
}
|
||||
|
||||
const FIntVector CenterChunk = WorldToChunkCoord(WorldToLocalCm(CenterPosition)); // player's level-0 tile
|
||||
CurrentCenterChunk = CenterChunk;
|
||||
@@ -2013,6 +2118,24 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
||||
++Submitted;
|
||||
}
|
||||
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& PendingTiles.Num() > GVoxelForgeMaxPendingTilesObserved)
|
||||
{
|
||||
GVoxelForgeMaxPendingTilesObserved = PendingTiles.Num();
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStreamingBudget] pending_max=%d effective=%d submitted=%d"),
|
||||
GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted);
|
||||
}
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& PendingTiles.Num() >= MaxTasks
|
||||
&& !GVoxelForgeGenerationCapHitReported)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d effective=%d"),
|
||||
PendingTiles.Num(), MaxTasks);
|
||||
GVoxelForgeGenerationCapHitReported = true;
|
||||
}
|
||||
|
||||
if (Submitted == 0 && PendingTiles.Num() == 0 && BandRemeshQueue.Num() == 0 && DirtyRemeshQueue.Num() == 0)
|
||||
{
|
||||
// Everything desired is loaded → load-before-unload is satisfied: drop the
|
||||
@@ -2036,6 +2159,14 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
const int32 MaxTasks = GetMaxConcurrentTasks(); // T2.d — core-clamped
|
||||
if (PendingTiles.Num() >= MaxTasks)
|
||||
{
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& !GVoxelForgeGenerationCapHitReported)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d effective=%d"),
|
||||
PendingTiles.Num(), MaxTasks);
|
||||
GVoxelForgeGenerationCapHitReported = true;
|
||||
}
|
||||
return; // Budget full — wait for a task to finish.
|
||||
}
|
||||
PendingTiles.Add(Tile);
|
||||
@@ -2727,7 +2858,13 @@ void AVoxelWorld::FillAtPosition(FVector Position, float Radius, float Strength)
|
||||
void AVoxelWorld::ApplyModification(const FVoxelModification& Modification)
|
||||
{
|
||||
if (!DiffLayer) return;
|
||||
const bool bProfileModification = GVoxelForgeProfileTileGeneration != 0;
|
||||
const uint64 ModificationStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
|
||||
const uint64 DiffStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
|
||||
TArray<FIntVector> AffectedChunks = DiffLayer->ApplyModification(Modification);
|
||||
const double DiffSeconds = bProfileModification
|
||||
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DiffStartCycles)
|
||||
: 0.0;
|
||||
|
||||
// INSTANT DIG FEEL — synchronously re-mesh the level-0 tile the brush CENTRE sits in, so the hole
|
||||
// appears THIS frame right where the player is looking. Neighbour tiles (brush edge) re-mesh async
|
||||
@@ -2743,11 +2880,30 @@ void AVoxelWorld::ApplyModification(const FVoxelModification& Modification)
|
||||
&& LoadedTiles.Contains(CenterTile)
|
||||
&& !PendingTiles.Contains(CenterTile))
|
||||
{
|
||||
const uint64 SyncStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
|
||||
SyncRemeshTile(CenterTile);
|
||||
if (bProfileModification)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeModificationProfile] center_sync=%.6f"),
|
||||
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - SyncStartCycles));
|
||||
}
|
||||
bSyncedCenter = true;
|
||||
}
|
||||
|
||||
const uint64 QueueStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
|
||||
RemeshDirtyChunks(AffectedChunks, bSyncedCenter ? &CenterTile : nullptr);
|
||||
if (bProfileModification)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeModificationProfile] affected=%d diff=%.6f queue=%.6f total=%.6f "
|
||||
"center_synced=%d"),
|
||||
AffectedChunks.Num(),
|
||||
DiffSeconds,
|
||||
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - QueueStartCycles),
|
||||
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ModificationStartCycles),
|
||||
bSyncedCenter ? 1 : 0);
|
||||
}
|
||||
|
||||
// Remove decorations inside the modified volume so grass doesn't float over a dug hole (or bury under a
|
||||
// fill). Instant + flicker-free (only the affected instances go); the placer already skips carved columns
|
||||
|
||||
Reference in new issue
Block a user