wip(gate): support column kept through rebuilds, hold only on swept contact -- NEVER COMPILED OR RUN

Stopped mid-round: the PC ran out of memory (another app held 11 GB), new processes failed to start
(0xC0000142), and the round ended without verifying anything after the edit.

Confirmed in code (performance review finding 1): a support request outside the stamped desired set
forces a rebuild that cancels it, the gate asks for one unresolved tile at a time, and it holds
before the pending tile is anywhere near the pawn's next step.

Uncompiled edits: a short collision-only support column retained through desired rebuilds and
prioritised; hold only when the swept capsule is about to enter a pending tile; downward speed
restored on release; opt-in deep-drop fixture (carved shaft) in the stress harness.

Evidence so far: shallow 10-voxel fall, gate on, before the edit: 1 hold of 1.51 s, clearance
+120.6 cm. The 80/160-voxel attempts spawned OUTSIDE the strate layout (support unavailable,
zero holds) and failed the penetration check at -11.1 cm: not the reviewed loop, but worth a look.
Compile risks: GetGravityZ(), TSet pruning in the rebuild, the fixture's carve/log arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-09-28 19:54:28 +02:00
1 parent 0b8fbc5591
commit 116b57f01e
4 files changed
+179 -17

No files matched your search

+165 -15
View File
@@ -585,6 +585,7 @@ void AVoxelWorld::RegenerateAllChunks()
// §9.4 collision-only tracking references destroyed tiles — clear (rebuilt on the next crossing).
CollisionOnlyTiles.Empty();
PrevCollisionOnlyTiles.Empty();
CollisionGateDesiredTiles.Empty();
// Reset streaming state so the next Tick rebuilds the desired set and reloads.
LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
@@ -1883,6 +1884,8 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
bHeadlessCollisionGateStressTestTerrainMeasurementMissing = false;
bHeadlessCollisionGateStressTestLanded = false;
bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured = false;
bHeadlessCollisionGateStressDeepDrop = false;
bHeadlessCollisionGateStressDeepDropPrepared = false;
HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f;
HeadlessCollisionGateStressTestDurationSeconds = 2.0f;
HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f;
@@ -2192,15 +2195,23 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
*HeadlessCollisionGateStressTestFailureReason);
}
float StressSpeed = HeadlessCollisionGateStressTestSpeedCmPerSecond;
float StressSpawnHeight = HeadlessCollisionGateStressTestSpawnHeightVoxels;
float StressDuration = HeadlessCollisionGateStressTestDurationSeconds;
float StressTimeout = HeadlessCollisionGateStressTestTimeoutSeconds;
int32 StressDeepDrop = 0;
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressSpeedCmPerSecond="),
StressSpeed);
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressSpawnHeightVoxels="),
StressSpawnHeight);
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressDeepDrop="),
StressDeepDrop);
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressDurationSeconds="),
StressDuration);
FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressTimeoutSeconds="),
StressTimeout);
HeadlessCollisionGateStressTestSpeedCmPerSecond = FMath::Max(StressSpeed, 1.0f);
HeadlessCollisionGateStressTestSpawnHeightVoxels = FMath::Max(StressSpawnHeight, 1.0f);
bHeadlessCollisionGateStressDeepDrop = StressDeepDrop != 0;
HeadlessCollisionGateStressTestDurationSeconds = FMath::Max(StressDuration, 0.25f);
HeadlessCollisionGateStressTestTimeoutSeconds = FMath::Max(StressTimeout, 1.0f);
// The stress path owns movement and keeps the streamer centred at the launch tile. Do
@@ -2210,8 +2221,10 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
HeadlessCollisionGateStressTestBeginSeconds = 0.0;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] configured=1 speed_cm_s=%.1f "
"duration_after_landing_s=%.3f timeout_s=%.3f"),
"spawn_height_vox=%.2f deep_drop=%d duration_after_landing_s=%.3f timeout_s=%.3f"),
HeadlessCollisionGateStressTestSpeedCmPerSecond,
HeadlessCollisionGateStressTestSpawnHeightVoxels,
bHeadlessCollisionGateStressDeepDrop ? 1 : 0,
HeadlessCollisionGateStressTestDurationSeconds,
HeadlessCollisionGateStressTestTimeoutSeconds);
}
@@ -2725,7 +2738,7 @@ void AVoxelWorld::ObserveHeadlessStrateCrossingTest(
const FVector LocalPosition = WorldToLocalVoxel(PlayerPosition);
FVoxelTileKey PendingFloorTile;
if (IsPawnCapsuleOverlappingPendingFloor(
PlayerPawn, PlayerPosition, &PendingFloorTile))
PlayerPawn, PlayerPosition, PlayerPosition, &PendingFloorTile))
{
bHeadlessStrateCrossingTestPendingFloorOverlap = true;
bHeadlessStrateCrossingTestFailed = true;
@@ -3002,6 +3015,21 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
const double Now = FPlatformTime::Seconds();
if (!bHeadlessCollisionGateStressTestStartPlaced)
{
if (bHeadlessCollisionGateStressDeepDrop && !bHeadlessCollisionGateStressDeepDropPrepared)
{
// Headless regression fixture: carve a narrow shaft through several level-0 tiles so
// the pawn falls through known-empty tiles before reaching an unresolved floor tile.
// This is process-local test data; no asset or save file is touched.
const FVector ShaftTopWorld = LocalVoxelToWorld(FVector(18.2f, 0.0f, 40.0f));
const FVector ShaftBottomWorld = LocalVoxelToWorld(FVector(18.2f, 0.0f, -90.0f));
CarveCapsule(ShaftTopWorld, ShaftBottomWorld, 7.0f, 24.0f);
bHeadlessCollisionGateStressDeepDropPrepared = true;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] deep_drop_fixture=carved "
"local_vox_top=(18.20,0.00,40.00) local_vox_bottom=(18.20,0.00,-90.00) "
"radius_vox=7.00"));
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] owner_settings clip_radius=%d "
"max_clip_level=%d render_distance_chunks=%d"),
@@ -3022,7 +3050,8 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
// must fall onto a freshly requested level-0 tile before the horizontal diagnostic.
constexpr float LaunchLocalVoxelX = 18.2f;
constexpr float LaunchLocalVoxelY = 0.0f;
constexpr float LaunchHeightAboveGroundVoxels = 10.0f;
const float LaunchHeightAboveGroundVoxels =
HeadlessCollisionGateStressTestSpawnHeightVoxels;
const FVector SurfaceProbeWorld = LocalVoxelToWorld(
FVector(LaunchLocalVoxelX, LaunchLocalVoxelY, 0.0f));
float ProbeFeetDensity = 0.0f;
@@ -4032,7 +4061,7 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] result=%s reason=%s elapsed_s=%.6f "
"distance_m=%.6f expected_distance_m=%.6f speed_cm_s=%.1f "
"distance_m=%.6f expected_distance_m=%.6f speed_cm_s=%.1f spawn_height_vox=%.2f deep_drop=%d "
"gate_holds=%d gate_total_s=%.6f max_gate_s=%.6f gate_progress_m=%.6f "
"full_stops=%d full_stop_total_s=%.6f collision_gate=%d "
"support_tile=(%d,%d,%d) support_first_check_to_submit_s=%.6f "
@@ -4051,6 +4080,8 @@ void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest()
HeadlessCollisionGateStressTestDistanceCm / 100.0,
ExpectedDistanceCm / 100.0,
HeadlessCollisionGateStressTestSpeedCmPerSecond,
HeadlessCollisionGateStressTestSpawnHeightVoxels,
bHeadlessCollisionGateStressDeepDrop ? 1 : 0,
HeadlessCollisionGateStressTestGateHoldCount,
HeadlessCollisionGateStressTestTotalGateDurationSeconds,
HeadlessCollisionGateStressTestMaxGateDurationSeconds,
@@ -4978,7 +5009,8 @@ bool AVoxelWorld::IsPlayerSupportCollisionReady(APawn* Pawn, const FVector& Play
}
bool AVoxelWorld::IsPawnCapsuleOverlappingPendingFloor(
APawn* Pawn, const FVector& PlayerPosition, FVoxelTileKey* OutPendingTile) const
APawn* Pawn, const FVector& PlayerPosition, const FVector& NextPlayerPosition,
FVoxelTileKey* OutPendingTile) const
{
if (OutPendingTile != nullptr)
{
@@ -5025,6 +5057,7 @@ bool AVoxelWorld::IsPawnCapsuleOverlappingPendingFloor(
}
const FVector LocalPosition = WorldToLocalVoxel(PlayerPosition);
const FVector LocalNextPosition = WorldToLocalVoxel(NextPlayerPosition);
const float HalfHeightVoxels = HalfHeightCm / VOXEL_SIZE;
const float RadiusVoxels = RadiusCm / VOXEL_SIZE;
const float TileMinX = static_cast<float>(SupportTile.Coord.X * CHUNK_SIZE);
@@ -5033,12 +5066,14 @@ bool AVoxelWorld::IsPawnCapsuleOverlappingPendingFloor(
const float TileMaxY = static_cast<float>((SupportTile.Coord.Y + 1) * CHUNK_SIZE);
const float TileMinZ = static_cast<float>(SupportTile.Coord.Z * CHUNK_SIZE);
const float TileMaxZ = static_cast<float>((SupportTile.Coord.Z + 1) * CHUNK_SIZE);
const float CapsuleMinZ = LocalPosition.Z - HalfHeightVoxels;
const float CapsuleMaxZ = LocalPosition.Z + HalfHeightVoxels;
const bool bHorizontalOverlap = LocalPosition.X + RadiusVoxels > TileMinX
&& LocalPosition.X - RadiusVoxels < TileMaxX
&& LocalPosition.Y + RadiusVoxels > TileMinY
&& LocalPosition.Y - RadiusVoxels < TileMaxY;
const float CapsuleMinZ = FMath::Min(LocalPosition.Z, LocalNextPosition.Z) - HalfHeightVoxels;
const float CapsuleMaxZ = FMath::Max(LocalPosition.Z, LocalNextPosition.Z) + HalfHeightVoxels;
const float CapsuleMinX = FMath::Min(LocalPosition.X, LocalNextPosition.X) - RadiusVoxels;
const float CapsuleMaxX = FMath::Max(LocalPosition.X, LocalNextPosition.X) + RadiusVoxels;
const float CapsuleMinY = FMath::Min(LocalPosition.Y, LocalNextPosition.Y) - RadiusVoxels;
const float CapsuleMaxY = FMath::Max(LocalPosition.Y, LocalNextPosition.Y) + RadiusVoxels;
const bool bHorizontalOverlap = CapsuleMaxX > TileMinX && CapsuleMinX < TileMaxX
&& CapsuleMaxY > TileMinY && CapsuleMinY < TileMaxY;
const bool bVerticalOverlap = CapsuleMaxZ > TileMinZ && CapsuleMinZ < TileMaxZ;
if (!bHorizontalOverlap || !bVerticalOverlap)
{
@@ -5101,10 +5136,38 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
// below adopts the same key through PendingTiles and IsDesired, so it is neither duplicated nor
// cancelled when the full clipmap arrives.
const bool bDesiredAtRequest = DesiredStamp != 0 && IsDesired(SupportTile);
if (!bDesiredAtRequest)
bool bNeedsDesiredRebuild = !bDesiredAtRequest;
constexpr int32 CollisionGatePrefetchDownTiles = 4;
for (auto It = CollisionGateDesiredTiles.CreateIterator(); It; ++It)
{
bForceDesiredRebuild = true;
const FVoxelTileKey& ExistingTile = *It;
const int32 RelativeZ = SupportTile.Coord.Z - ExistingTile.Coord.Z;
if (ExistingTile.Coord.X != SupportTile.Coord.X
|| ExistingTile.Coord.Y != SupportTile.Coord.Y
|| RelativeZ < 0 || RelativeZ >= CollisionGatePrefetchDownTiles)
{
bNeedsDesiredRebuild |= IsDesired(ExistingTile);
It.RemoveCurrent();
}
}
for (int32 Offset = 0; Offset < CollisionGatePrefetchDownTiles; ++Offset)
{
FVoxelTileKey ColumnTile = SupportTile;
ColumnTile.Coord.Z -= Offset;
const TCHAR* UnavailableReason = nullptr;
if (IsTileCollisionReady(ColumnTile)
|| CollisionNotRequiredTiles.Contains(ColumnTile)
|| IsCollisionTileUnavailable(ColumnTile, UnavailableReason))
{
continue;
}
if (!CollisionGateDesiredTiles.Contains(ColumnTile))
{
CollisionGateDesiredTiles.Add(ColumnTile);
bNeedsDesiredRebuild |= !IsDesired(ColumnTile);
}
}
bForceDesiredRebuild |= bNeedsDesiredRebuild;
// Preserve the established critical floor ordering when it already belongs to this desired
// epoch. Before the first build there is no array to promote into; LoadTile below still uses
@@ -5188,7 +5251,6 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition,
float DeltaTime)
{
static_cast<void>(DeltaTime);
if (!IsValid(Pawn) || bShuttingDown.load(std::memory_order_relaxed))
{
return;
@@ -5307,6 +5369,35 @@ void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosi
}
}
// Ask for support now, but do not stop the pawn while an unresolved tile is still below its
// next physics step. The swept capsule bounds catch the step before movement can enter it.
FVector NextPlayerPosition = PlayerPosition;
if (DeltaTime > 0.0f)
{
FVector Velocity = Pawn->GetVelocity();
float GravityZ = 0.0f;
if (const ACharacter* Character = Cast<ACharacter>(Pawn))
{
if (const UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement())
{
Velocity = CharacterMovement->Velocity;
GravityZ = CharacterMovement->GetGravityZ();
}
}
NextPlayerPosition += Velocity * DeltaTime;
if (Velocity.Z <= KINDA_SMALL_NUMBER && GravityZ < 0.0f)
{
NextPlayerPosition.Z += 0.5f * GravityZ * DeltaTime * DeltaTime;
}
}
FVoxelTileKey PendingFloorTile;
if (!IsPawnCapsuleOverlappingPendingFloor(
Pawn, PlayerPosition, NextPlayerPosition, &PendingFloorTile))
{
ReleasePawnCollisionGate();
return;
}
if (!bPawnCollisionGateEngaged)
{
UE_LOG(LogTemp, Display,
@@ -5335,6 +5426,8 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
FVector Velocity = CharacterMovement->Velocity;
if (Velocity.Z < 0.0f)
{
SavedGateVerticalVelocity = Velocity.Z;
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
CharacterMovement->Velocity = Velocity;
CharacterMovement->UpdateComponentVelocity();
@@ -5345,6 +5438,8 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
FVector Velocity = PawnMovement->Velocity;
if (Velocity.Z < 0.0f)
{
SavedGateVerticalVelocity = Velocity.Z;
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
PawnMovement->Velocity = Velocity;
PawnMovement->UpdateComponentVelocity();
@@ -5366,6 +5461,8 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
FVector Velocity = CharacterMovement->Velocity;
if (Velocity.Z < 0.0f)
{
SavedGateVerticalVelocity = Velocity.Z;
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
CharacterMovement->Velocity = Velocity;
}
@@ -5383,6 +5480,8 @@ void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn)
FVector Velocity = PawnMovement->Velocity;
if (Velocity.Z < 0.0f)
{
SavedGateVerticalVelocity = Velocity.Z;
bSavedGateVerticalVelocity = true;
Velocity.Z = 0.0f;
PawnMovement->Velocity = Velocity;
PawnMovement->UpdateComponentVelocity();
@@ -5410,6 +5509,8 @@ void AVoxelWorld::ReleasePawnCollisionGate()
CollisionGatedPawnMovement.Reset();
CollisionGatedCharacterMovement.Reset();
bSavedCharacterGravityScale = false;
bSavedGateVerticalVelocity = false;
SavedGateVerticalVelocity = 0.0f;
bPawnGateSupportRequestReported = false;
PawnGateEngagedAtSeconds = 0.0;
return;
@@ -5420,8 +5521,20 @@ void AVoxelWorld::ReleasePawnCollisionGate()
if (bSavedCharacterGravityScale)
{
CharacterMovement->GravityScale = SavedCharacterGravityScale;
CharacterMovement->UpdateComponentVelocity();
}
if (bSavedGateVerticalVelocity && CharacterMovement->Velocity.Z <= KINDA_SMALL_NUMBER)
{
CharacterMovement->Velocity.Z = SavedGateVerticalVelocity;
}
CharacterMovement->UpdateComponentVelocity();
}
else if (UPawnMovementComponent* PawnMovement = CollisionGatedPawnMovement.Get())
{
if (bSavedGateVerticalVelocity && PawnMovement->Velocity.Z <= KINDA_SMALL_NUMBER)
{
PawnMovement->Velocity.Z = SavedGateVerticalVelocity;
}
PawnMovement->UpdateComponentVelocity();
}
if (APawn* Pawn = CollisionGatedPawn.Get())
@@ -5437,6 +5550,8 @@ void AVoxelWorld::ReleasePawnCollisionGate()
CollisionGatedPawnMovement.Reset();
CollisionGatedCharacterMovement.Reset();
bSavedCharacterGravityScale = false;
bSavedGateVerticalVelocity = false;
SavedGateVerticalVelocity = 0.0f;
bPawnCollisionGateEngaged = false;
PawnGateEngagedAtSeconds = 0.0;
bPawnGateSupportRequestReported = false;
@@ -6516,6 +6631,30 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
// once it moves away or is unregistered. No-op (zero cost) when there are no anchors.
AddAnchorDesiredTiles();
// A pending support target and a few tiles below it are collision-only desired tiles. This
// keeps an out-of-ring request alive across this rebuild and lets the worker/apply queues fill
// the landing column while the pawn is still above it.
for (auto It = CollisionGateDesiredTiles.CreateIterator(); It; ++It)
{
const FVoxelTileKey& Key = *It;
const TCHAR* UnavailableReason = nullptr;
if (IsTileCollisionReady(Key) || CollisionNotRequiredTiles.Contains(Key)
|| IsCollisionTileUnavailable(Key, UnavailableReason))
{
It.RemoveCurrent();
continue;
}
uint32& Stamp = DesiredStamped.FindOrAdd(Key);
const bool bNew = Stamp != DesiredStamp;
if (bNew)
{
Stamp = DesiredStamp;
DesiredSorted.Add(Key);
CollisionOnlyTiles.Add(Key);
}
}
// Balayage UNIQUE de la map : les entrées à stamp périmé viennent de quitter le desired set —
// ce sont les seuls candidats au cull de ce crossing (avec la TransitionHold). On les retire
// ici même (RemoveCurrent est sûr en itérant), la map reste donc == desired set courant.
@@ -6585,6 +6724,17 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
CriticalDesiredTiles.Add(Key);
}
};
if (CollisionGateDesiredTiles.Contains(PawnGateRequestedSupportTile))
{
AddCriticalTile(PawnGateRequestedSupportTile);
}
for (const FVoxelTileKey& Key : DesiredSorted)
{
if (CollisionGateDesiredTiles.Contains(Key))
{
AddCriticalTile(Key);
}
}
AddCriticalTile(OccupiedTile);
FVector Heading = PlayerHeading;
+9
View File
@@ -868,6 +868,7 @@ public:
FVoxelTileKey* OutSupportTile = nullptr,
const TCHAR** OutUnavailableReason = nullptr) const;
bool IsPawnCapsuleOverlappingPendingFloor(APawn* Pawn, const FVector& PlayerPosition,
const FVector& NextPlayerPosition,
FVoxelTileKey* OutPendingTile = nullptr) const;
void RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile);
@@ -970,6 +971,9 @@ public:
// without an O(loaded) scan. Both empty when there are no CollisionOnly anchors → zero cost.
TSet<FVoxelTileKey> CollisionOnlyTiles;
TSet<FVoxelTileKey> PrevCollisionOnlyTiles;
// A short collision-only column below an unresolved player support tile. Keeping these keys
// stamped prevents a critical request outside the render clipmap from being canceled on rebuild.
TSet<FVoxelTileKey> CollisionGateDesiredTiles;
void ReconcileAnchorTileVisibility();
// --- Streaming work-avoidance (perf) ---
@@ -1105,7 +1109,10 @@ public:
bool bHeadlessCollisionGateStressTestTerrainMeasurementMissing = false;
bool bHeadlessCollisionGateStressTestLanded = false;
bool bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured = false;
bool bHeadlessCollisionGateStressDeepDrop = false;
bool bHeadlessCollisionGateStressDeepDropPrepared = false;
float HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f;
float HeadlessCollisionGateStressTestSpawnHeightVoxels = 10.0f;
float HeadlessCollisionGateStressTestDurationSeconds = 2.0f;
float HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f;
double HeadlessCollisionGateStressTestBeginSeconds = 0.0;
@@ -1228,6 +1235,8 @@ public:
bool bCollisionGateEnabled = true;
float SavedCharacterGravityScale = 1.0f;
bool bSavedCharacterGravityScale = false;
float SavedGateVerticalVelocity = 0.0f;
bool bSavedGateVerticalVelocity = false;
bool bPawnCollisionGateEngaged = false;
double PawnGateEngagedAtSeconds = 0.0;
bool bPawnGateUnavailableReported = false;