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:
Fr0zkaandClaude Opus 5 committed 2026-09-11 04:25:45 +02:00
1 parent e91f1c44bb
commit 8e42d6d807
15 files changed
+706 -95

No files matched your search

+86 -16
View File
@@ -295,7 +295,8 @@ static FORCEINLINE void ApplyOriginLandingFloor(float& Density,
// it works uniformly for every generator type. Stays inside the seal bands so it can
// never breach a strate boundary. All features are hash-placed and deterministic.
static void ApplyDisturbances(float& MC, float X, float Y, float Z,
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir)
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir,
bool bProtectAuthoredTunnelFloor)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::ApplyDisturbances);
@@ -303,6 +304,20 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness;
if (Z <= InnerBot || Z >= InnerTop) return;
// A tunnel floor is now authored into the cached shape with its clearance budget. The generic
// disturbance layer is deliberately outside that shape, so it must not reopen or bridge the
// finite support band after the shape has proved it. This is an ownership hand-off, not a
// per-sample natural-floor validation.
if (bProtectAuthoredTunnelFloor)
{
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelAuthoredFloorDisturbanceSkips);
}
return;
}
const float Solid = D.BaseDensity * 2.0f;
const float Blend = 3.0f;
const FVector P(X, Y, Z);
@@ -1410,6 +1425,43 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
}
DensityCoreTimer.End();
// Resolve the graph's authored floor before the disturbance pass. The operator-stack
// source has already computed this exact result while publishing its sample; native
// tunnel generation uses the prepared immutable cache. Keeping the result here means the
// disturbance layer can respect the shape-level floor and the common tail can reuse the
// same query instead of discovering a floor only after it has been damaged.
FTunnelCoreWorldEvaluation PreDisturbanceTunnelCore;
bool bHavePreDisturbanceTunnelCore = false;
if (CP_UseOpStack)
{
bHavePreDisturbanceTunnelCore = ActiveOpStack->TryGetLastTunnelCoreWorldEvaluation(
PreDisturbanceTunnelCore);
}
if (!bHavePreDisturbanceTunnelCore && ActiveTunnelCoreCache->bValid)
{
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
FTunnelSupportFloorColumn EmptySupportColumn;
const bool bIntegerXY =
WorldX == FMath::FloorToFloat(WorldX)
&& WorldY == FMath::FloorToFloat(WorldY);
if (bIntegerXY)
{
const int32 IX = FMath::FloorToInt(WorldX);
const int32 IY = FMath::FloorToInt(WorldY);
SupportColumn = FindTunnelSupportColumn(
ActiveTunnelCoreCache->Cache, IX, IY, EmptySupportColumn);
}
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts);
PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, SupportColumn);
bHavePreDisturbanceTunnelCore = true;
}
const bool bProtectAuthoredTunnelFloor = bHavePreDisturbanceTunnelCore
&& (PreDisturbanceTunnelCore.bSupportFloor
|| PreDisturbanceTunnelCore.bRoomFloor);
// Disturbance layer (the "wow" post-process) — cached params, MC convention. The
// vertical shaft tree opens a walkable route before this shared pass, so a bridge/ridge
// must not refill it. Both native and operator-stack paths use the same marker helper.
@@ -1418,7 +1470,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
const bool bProtectVerticalShaftAir =
VoxelPassageGeometry::VerticalShaftConnectorAirMarker();
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed,
bProtectVerticalShaftAir);
bProtectVerticalShaftAir, bProtectAuthoredTunnelFloor);
DensityDisturbancesTimer.End();
// A disturbance is allowed to add visual detail, but it must not refill the landing's
@@ -1504,7 +1556,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageStructuralPostsMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
CP_Dist.BoundarySealThickness, bProtectAuthoredTunnelFloor);
// Disturbance features are authored as a generic MC-space post and may add a ridge or
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
@@ -1513,8 +1565,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (ActiveTunnelCoreCache->bValid || CP_UseOpStack)
{
FTunnelCoreWorldEvaluation TunnelCore;
bool bHaveTunnelCore = CP_UseOpStack
&& ActiveOpStack->TryGetLastTunnelCoreWorldEvaluation(TunnelCore);
bool bHaveTunnelCore = bHavePreDisturbanceTunnelCore;
if (bHaveTunnelCore)
{
TunnelCore = PreDisturbanceTunnelCore;
}
if (!bHaveTunnelCore && ActiveTunnelCoreCache->bValid)
{
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
@@ -1540,33 +1595,43 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (bHaveTunnelCore)
{
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
if (TunnelCore.bRoomFloor)
if (TunnelCore.bRoomFloor && !bProtectAuthoredTunnelFloor)
{
const float StructuralSolidDensity =
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelRoomFloorBackstopFires);
if (Result > 0.0f)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelRoomFloorBackstopFires);
}
}
// A tunnel that penetrates a room cannot reassert its own floor through the
// room's floor. The morphology query marks only the finite room support band;
// preserve it after the generic post-disturbance air backstop.
Result = FMath::Min(
Result,
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
StructuralSolidDensity);
}
// The swept capsule owns the relief profile, while this finite support band is
// the conservative post-disturbance backstop that keeps terrain modifiers from
// removing the only player support at a mouth or a narrow cross-section.
if (bTunnelSupportFloor)
if (bTunnelSupportFloor && !bProtectAuthoredTunnelFloor)
{
const float StructuralSolidDensity =
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
if (Result > 0.0f)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
}
}
Result = FMath::Min(
Result,
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
StructuralSolidDensity);
}
const float CoreSDF = TunnelCore.SDF;
if (!bTunnelSupportFloor
@@ -2746,12 +2811,17 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// operator-stack path; its support band follows the same relieved swept floor.
if (bTunnelSupportFloor)
{
const float StructuralSolidDensity =
FMath::Max(Params.BaseDensity * 2.0f, 1.0f);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
if (Density < 0.0f)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorBackstopFires);
}
}
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
Density = FMath::Max(Density, StructuralSolidDensity);
}
const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(WorldX, WorldY, WorldZ, SDFCache)