perf(apply): stutter is NOT the game thread -- and the flat floor cannot be relaxed

Two things the owner reported from playing 412a7ad. One is answered, one is a null
result that Codex flagged itself rather than dressing up.

THE FLATTENED TUNNEL FLOORS -- capability-safe, and worth almost nothing
He has now flagged these twice: every tunnel carries a smooth flat strip running in
and out through otherwise craggy rock. The idea was to make the floor CONDITIONAL --
assert it only where the natural rock fails walkability, leave the rock alone
elsewhere. Cheaper and better looking, in theory.

The floor is now skipped only when the exact no-floor field passes the same
player-fit validator at foot offsets 0, -1, +1. Unknown or failed keeps the slab.

  natural-floor skips accepted   1,539 of 1,529,241 tests   = 0.10%
  player-fit cells        20,153 -> 20,153
  reachable cells         10,483 -> 10,483
  arrival -> departure    connected, both
  dihedral p50/p90        2.5429 / 33.8499 deg, IDENTICAL
  mesh time (their batch) 1.757135 -> 1.757140 s

Capability preserved exactly. Visual complaint NOT solved, and no speed gained.

⭐ The useful part is what it PROVED by trying to go further. Every broader policy
lost walkable space:
    analytic-only        fit -217, reachable -195
    tightened analytic   fit  -76, reachable  -76
    exact +/-4 foot      fit  -16, reachable  -16
⇒ The floor cannot be relaxed without changing the walkability contract or adding a
route-aware global proof. That is a real constraint, now measured rather than assumed.

Codex's own words, and it was right to lead with them: "the conservative floor change
is capability-safe, but it does not visibly solve the flattened-strip complaint."

THE STUTTER -- measured, and it is not where we thought
Game-thread apply, meshed tiles, under -nullrhi:

  total apply            p50 0.107 ms   p95 0.153 ms   max 0.273 ms
    stream upload/RMC    p50 0.040      p95 0.088      max 0.124
    component creation   p50 0.019      p95 0.021      max 0.035
    registration         p50 0.010      p95 0.011      max 0.020
    collision config     p50 0.005      p95 0.007      max 0.019

A quarter of a millisecond at worst. The game-thread drain is NOT the hitch.
⇒ Remaining visible stutter is most likely render-thread/RHI work or asynchronous
collision cooking behind RealtimeMeshComponent, which is off-limits for edits.

Added anyway as a cheap safety valve: MaxMeshApplyMilliseconds, default 2 ms of drain
budget per frame. Over 729 apply frames: drain p50/p95/max 0.111/0.182/0.313 ms,
budget hits 0, at most 2 visible applies in a frame. It costs nothing today and caps
a future burst.

Request-to-ready, now timed after game-thread application:
  all     p50 57.1 ms   p95 432.9 ms   max 919.4 ms   -> 2.60 m at 6 m/s
  meshed  p50 134.1 ms  p95 457.4 ms   max 919.4 ms

⚠️ My own verification: 1.197 / 1.131 / 1.118 s vs 1.088-1.102 s on 412a7ad. Mean
+4.7% -- below the 17% noise floor, but slower in all three runs, and the validator
runs 1.5M times to accept 1,539 skips. Geometry moved by 4 triangles (153,740 ->
153,744), which is allowed. If the next round cannot make that validator earn its
place, it should come out.

Build 0 errors. Peak RSS 1.67 GB export / 1.90 GB game. Nothing outside
Plugins/VoxelForge; RealtimeMeshComponent untouched.

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-10 14:46:29 +02:00
1 parent 412a7ad219
commit 383ec86c37
11 files changed
+508 -43

No files matched your search

+50 -12
View File
@@ -507,7 +507,11 @@ namespace
static void BuildTunnelSupportColumnsForChunk(
FChunkSDFCache& Cache,
const FIntVector& ChunkCoord);
const FIntVector& ChunkCoord,
const UVoxelGenerator* Generator,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint,
uint32 LayoutVersion);
static const FTunnelSupportFloorColumn* FindTunnelSupportColumn(
const FChunkSDFCache& Cache,
@@ -603,6 +607,7 @@ namespace
}
void PrepareTunnelCoreCache(
const UVoxelGenerator& Generator,
const UVoxelStrateManager& Manager,
const FIntVector& ChunkCoord,
const FStrateGenerationParams& Params,
@@ -638,7 +643,9 @@ namespace
ChunkMaxX + Expansion, ChunkMaxY + Expansion,
Params, static_cast<uint32>(Manager.GetWorldSeed()),
StrateIndex, nullptr);
BuildTunnelSupportColumnsForChunk(OutState.Cache, ChunkCoord);
BuildTunnelSupportColumnsForChunk(
OutState.Cache, ChunkCoord, &Generator, Params,
ParamsFingerprint, LayoutVersion);
OutState.OwnerId = OwnerId;
OutState.ManagerLifetimeId = ManagerLifetimeId;
OutState.Chunk = ChunkCoord;
@@ -649,7 +656,11 @@ namespace
static void BuildTunnelSupportColumnsForChunk(
FChunkSDFCache& Cache,
const FIntVector& ChunkCoord)
const FIntVector& ChunkCoord,
const UVoxelGenerator* Generator,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint,
uint32 LayoutVersion)
{
const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1;
const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1;
@@ -661,6 +672,25 @@ namespace
Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells);
Cache.SupportColumns.Reset();
// The conditional floor is tested against the exact density pipeline with only the
// graph support slab disabled. The sampler is synchronous and lives only for this cache
// build; the immutable cache stores the resulting interval decision, never the callback.
const auto SampleWithoutTunnelSupportFloor =
[Generator, &Params, ParamsFingerprint, LayoutVersion](
float X, float Y, float Z) -> float
{
return Generator != nullptr
? Generator->GetDensityWithParams(
X, Y, Z, Params, ParamsFingerprint, LayoutVersion,
/*bApplyLegacyStructuralPosts=*/true,
/*bApplyTunnelSupportFloor=*/false)
: 0.0f;
};
FTunnelSupportFloorDensitySampler NaturalSamplerRef =
SampleWithoutTunnelSupportFloor;
const FTunnelSupportFloorDensitySampler* NaturalSampler =
Generator != nullptr ? &NaturalSamplerRef : nullptr;
// Empty columns are represented only by INDEX_NONE in the slot map, so
// each stored column retains the inline bands for actual graph tubes.
for (int32 Y = 0; Y < Cells; ++Y)
@@ -672,7 +702,11 @@ namespace
static_cast<float>(MinX + X),
static_cast<float>(MinY + Y),
Cache,
Column);
Column,
NaturalSampler,
Params.StrateTopWorldZ,
Params.StrateBottomWorldZ,
Params.BoundarySealThickness);
if (Column.Intervals.Num() == 0)
{
continue;
@@ -736,7 +770,8 @@ namespace
*/
bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs,
int32 Seed, float SpineRadius, const UVoxelStrateManager* SM,
FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx)
FVoxelOpStack& OutStack, FVoxelOpContext& OutCtx,
const UVoxelGenerator* Generator = nullptr)
{
switch (Type)
{
@@ -793,7 +828,9 @@ namespace
if (!Refs.Tunnel) { return false; }
OutCtx.StrateTopWorldZ = Refs.Tunnel->StrateTopWorldZ;
OutCtx.StrateBottomWorldZ = Refs.Tunnel->StrateBottomWorldZ;
VoxelDensityOps::BuildTunnelNetworkStack(OutStack, *Refs.Tunnel, Seed, SpineRadius, SM);
VoxelDensityOps::BuildTunnelNetworkStack(
OutStack, *Refs.Tunnel, Seed, SpineRadius, SM,
/*bAppendStructuralPosts=*/true, Generator);
return true;
default:
@@ -1259,7 +1296,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_UseOpStack = VF_BuildOpStackForChunk(
CP_GenType, Refs, Seed, OriginSpineRadius,
StrateManager, CP_OpStack, OpCtx);
StrateManager, CP_OpStack, OpCtx, this);
// Le test appelle PrepareChunk, pas la production : c'est exactement la divergence
// qui rend un opérateur vert en test et faux en jeu. Les sept `PrepareChunk`
@@ -1293,7 +1330,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (bCanPrepareNativeTunnelCore)
{
PrepareTunnelCoreCache(
*StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
*this, *StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache);
}
else
@@ -1647,7 +1684,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint, uint32 LayoutVersion,
bool bApplyLegacyStructuralPosts) const
bool bApplyLegacyStructuralPosts,
bool bApplyTunnelSupportFloor) const
{
//=========================================================================
// STRATE DENSITY FUNCTION (Morphology Pipeline)
@@ -2713,8 +2751,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// the graph's own tunnel floor/core are structural route geometry. Direct callers of
// GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the
// same predicates once in its common MC-space post section below both generation paths.
const bool bTunnelSupportFloor =
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
const bool bTunnelSupportFloor = bApplyTunnelSupportFloor
&& VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
WorldX, WorldY, WorldZ, SDFCache);
if (bTunnelSupportFloor)
{
@@ -4753,7 +4791,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
FVoxelOpStack TileStack;
if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius,
StrateManager, TileStack, OpCtx))
StrateManager, TileStack, OpCtx, this))
{
// Strate dégénérée ou archétype non porté : `GetDensityAt` retomberait sur le `switch`,
// donc la pile ne décrit pas ce que le mesher verra. Aucun verdict.