fix(streaming): the player can walk from one strate into the next

Owner playtest: he could not walk from strate A into B. Forcing the pawn through in the editor
showed the junction tunnel (and his edits to the lower strate) were there.

Cause: UVoxelSettings::StrateContentCutMinLevel defaulted to 0, and DA_Settings does not override
it, so the strate content band (the PAWN's strate) also cut LOD0, the collision tiles. While the
player stood in A, the passage below A's seal, the gap and strate B had no LOD0 geometry, and the
collision gate correctly refused to step onto missing collision. The band only moves once the pawn
is inside B: a deadlock. bClampViewToStrate is a different setting, which is why toggling it did
not help. Some historical fall-throughs were likely this, before the gate turned "fall" into
"blocked".

Fix (Codex; reviewed by me):
- StrateContentCutMinLevel default 0 -> 1, ClampMin 1, and
  UVoxelSettings::GetEffectiveStrateContentCutMinLevel() clamps to [1,9] at every runtime
  consumer, so a legacy asset serialising 0 cannot bring the deadlock back. LOD1+ stay banded
  inside strates and go full in the gap.
- A regression test, -voxel.TestStrateCrossing=1 (off unless passed): it walks the real manager
  passage control points with AddMovementInput and normal swept character movement, with limits
  of 10 s max gate, 120 s total and a 2-voxel floor tolerance. Old code: FAIL
  collision_gate_timeout at route 5/9 (10.0 s gated). Fixed: PASS reached_lower_strate, strate
  0 -> 1, max gate 0.092 s. The trace shows band [-7..0] -> full in the gap -> [-17..-10].

Field unchanged: canonical export b3e5f4c3..9b377, capability 20,830 / 10,909. In game, the LOD0
change in the crossing scene is 11 transition-gap tiles going from 0 to 22,390 triangles; no LOD0
interior tile changed.

COST, stated plainly (two interleaved runs, baseline vs fixed DLL):
  static  generation CPU 34.1 -> 44.1 s (+29%), density calls +23%, LOD0 request p95 +8.9%
  moving  generation CPU 78.8 -> 91.1 s (+16%), density calls +26%, LOD0 request p95 +3.1%
  applied triangles identical (669,834 / 1,331,482): the extra work is LOD0 tiles in sealed rock
  that the old cut skipped. The next round proves them solid instead of sampling them.

Visual caveat: the old default of 0 was a deliberate July choice, because LOD0/1 tiles straddling
a strate boundary mixed both strates' materials. With LOD0 never cut, that mixing may reappear
near seals. The headless runs are -nullrhi, so this still needs checking in the editor.

Launches: all serialized and waited by PID. Failures: one malformed first baseline attempt, one
over-strict early harness assertion, and one run against stale staging (it logged CutMinLevel=0),
all superseded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-14 19:46:35 +02:00
1 parent 7ece3ceafc
commit 4c8d06fd8f
5 files changed
+735 -27

No files matched your search

+41
View File
@@ -504,6 +504,11 @@ private:
void ConfigureHeadlessStreamingTest();
void AdvanceHeadlessStreamingTest(FVector& InOutPlayerPosition,
FVector& InOutPlayerHeading, APawn* PlayerPawn);
void InitializeHeadlessStrateCrossingTest();
void AdvanceHeadlessStrateCrossingTest(FVector& InOutPlayerPosition,
FVector& InOutPlayerHeading, APawn* PlayerPawn);
void ObserveHeadlessStrateCrossingTest(const FVector& PlayerPosition, APawn* PlayerPawn);
void MaybeFinishHeadlessStrateCrossingTest();
void MaybeFinishHeadlessStreamingTest();
// Packed shader params, recomputed each Tick. ALL meaningful data is in .xyz — a material Vector
@@ -937,6 +942,9 @@ public:
uint64 AppliedTileCount = 0;
uint64 AppliedVisibleTileCount = 0;
uint64 AppliedTriangleCount = 0;
uint64 AppliedVisibleTileCountByLevel[TrackedClassifierLODCount]{};
uint64 AppliedTriangleCountByLevel[TrackedClassifierLODCount]{};
uint64 AppliedBandTileCountByLevel[TrackedClassifierLODCount]{};
struct FStreamingLatencySample
{
@@ -961,6 +969,39 @@ public:
double HeadlessStreamingTestBeginSeconds = 0.0;
double HeadlessStreamingTestLastElapsedSeconds = 0.0;
// Real inter-strate crossing validation. This is enabled only by -voxel.TestStrateCrossing=1;
// the route is copied from UVoxelStrateManager's generated passage and movement is supplied to
// the ordinary pawn movement component. The initial placement onto the upper landing is test
// setup; every point after that is reached through swept character movement and the collision gate.
bool bHeadlessStrateCrossingTest = false;
bool bHeadlessStrateCrossingTestStartPlaced = false;
bool bHeadlessStrateCrossingTestStarted = false;
bool bHeadlessStrateCrossingTestPassed = false;
bool bHeadlessStrateCrossingTestFailed = false;
bool bHeadlessStrateCrossingTestExitRequested = false;
bool bHeadlessStrateCrossingTestGateWasEngaged = false;
int32 HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE;
int32 HeadlessStrateCrossingTestPassageIndex = INDEX_NONE;
int32 HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE;
int32 HeadlessStrateCrossingTestLowerStrateIndex = INDEX_NONE;
int32 HeadlessStrateCrossingTestRouteTargetIndex = 1;
int32 HeadlessStrateCrossingTestLastReportedStrate = INDEX_NONE;
float HeadlessStrateCrossingTestSpeedCmPerSecond = 800.0f;
float HeadlessStrateCrossingTestStartDelaySeconds = 1.0f;
float HeadlessStrateCrossingTestTimeoutSeconds = 120.0f;
float HeadlessStrateCrossingTestMaxGateSeconds = 10.0f;
float HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 2.0f;
float HeadlessStrateCrossingTestPlayerHalfHeightVoxels = 3.52f;
float HeadlessStrateCrossingTestMinFloorClearanceVoxels = 1000000.0f;
double HeadlessStrateCrossingTestBeginSeconds = 0.0;
double HeadlessStrateCrossingTestStartSeconds = 0.0;
double HeadlessStrateCrossingTestGateStartSeconds = 0.0;
double HeadlessStrateCrossingTestMaxGateDurationSeconds = 0.0;
double HeadlessStrateCrossingTestTotalGateDurationSeconds = 0.0;
FVector HeadlessStrateCrossingTestLastActualPosition = FVector::ZeroVector;
FString HeadlessStrateCrossingTestFailureReason;
TArray<FVector> HeadlessStrateCrossingTestRoute;
// PLAYER COLLISION GATE — the pawn is allowed to enter/leave a tile only when the level-0
// collision body covering its feet has completed the RMC cook. The movement component state is
// saved so a temporary stream wait does not permanently change a character's movement mode.