perf(review): gate never deadlocks, collision radius, 20x faster digs
From the outside performance review (items 1-4; item 5 prototyped and reverted).
1. Collision gate: a support request outside the stamped desired set was cancelled by the rebuild
it forced, and the gate held the pawn with gravity 0. Baseline deep-drop (floor at (0,0,-7),
outside the aligned L0 ring): 1 hold of 30.0 s, never released. Fix: the target and 3 tiles
below are retained through rebuilds at CollisionCritical; hold only when the swept capsule is
about to enter pending support; release restores movement mode, gravity and downward speed.
After: 3 holds, 2.21 s total, 112.8 m horizontal progress; gate off still penetrates (-7.4 cm).
2. Suspected cleanup regression (out-of-layout stacks): not real. 44 paired boundary tiles,
30.9 ms vs 32.6 ms at b4ac996, hashes 44/44 identical. No change.
3. PawnCollisionRadiusChunks (new, default 1): L0 tiles outside the collision set stay visible
without Chaos cooks; anchors and the gate's support column are unioned in. Cook requests
~217/min -> ~30/min (-86%). Critical support ready time did NOT improve (9.5 s -> 10.6 s in the
deep-drop fixture; the wait is the cook itself, ~7.4 s cook-to-ready there).
4. Dig: exact pre-edit L0 density cached (one 35^3 grid, 171.5 KB), carve replays current mods on
it. Sync remesh 99.4 ms -> 4.7 ms. 16/16 random carves hash-identical to full regeneration.
5. Parallel grid fill for the critical tile: 16/16 hash parity, but the waited-on tile was already
meshed; the wait was collision cooking. Reverted.
Review G answered: M_Test1 still ray-marches VolTex0 (custom HLSL mini-sun shadow), so the density
volume is live. Known and left alone: outside the strate layout the fallback density can be solid
while the gate treats those tiles as air.
Tests: VoxelForgeCollisionDesiredTilesTest, VoxelForgePreEditDensityCacheTest; no-overlap, edit
ghost, descent corridor pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
116b57f01e
commit
0bcd67a016
13 files changed
+1114
-104
No files matched your search
@@ -0,0 +1,39 @@
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "VoxelCollisionDesiredTiles.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeCollisionDesiredTilesPawnAndAnchorsTest,
|
||||
"VoxelForge.Streaming.CollisionDesiredTiles.PawnAndAnchors",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeCollisionDesiredTilesPawnAndAnchorsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
(void)Parameters;
|
||||
TSet<FVoxelTileKey> CollisionTiles;
|
||||
const FIntVector PawnChunk(2, -3, 4);
|
||||
VoxelCollisionDesiredTiles::AddBox(CollisionTiles, PawnChunk, 1, 1, 1);
|
||||
|
||||
TestTrue(TEXT("pawn center remains collidable"),
|
||||
CollisionTiles.Contains(FVoxelTileKey(PawnChunk, 0)));
|
||||
TestTrue(TEXT("pawn radius includes horizontal edge"),
|
||||
CollisionTiles.Contains(FVoxelTileKey(PawnChunk + FIntVector(1, -1, 0), 0)));
|
||||
TestTrue(TEXT("pawn radius includes support below"),
|
||||
CollisionTiles.Contains(FVoxelTileKey(PawnChunk + FIntVector(0, 0, -1), 0)));
|
||||
TestFalse(TEXT("pawn radius does not follow the full render ring"),
|
||||
CollisionTiles.Contains(FVoxelTileKey(PawnChunk + FIntVector(2, 0, 0), 0)));
|
||||
|
||||
const FIntVector AnchorChunk(-12, 8, -5);
|
||||
VoxelCollisionDesiredTiles::AddBox(CollisionTiles, AnchorChunk, 2, 1, 3);
|
||||
TestTrue(TEXT("anchor box includes its far horizontal edge"),
|
||||
CollisionTiles.Contains(FVoxelTileKey(AnchorChunk + FIntVector(-2, 2, 0), 0)));
|
||||
TestTrue(TEXT("anchor box includes its upper extent"),
|
||||
CollisionTiles.Contains(FVoxelTileKey(AnchorChunk + FIntVector(0, 0, 3), 0)));
|
||||
TestFalse(TEXT("anchor box keeps authored bounds"),
|
||||
CollisionTiles.Contains(FVoxelTileKey(AnchorChunk + FIntVector(3, 0, 0), 0)));
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -0,0 +1,110 @@
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "VoxelGeometryHash.h"
|
||||
#include "VoxelStackSampler.h"
|
||||
#include "../VoxelPreEditDensityCache.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgePreEditDensityCacheParityTest,
|
||||
"VoxelForge.Determinism.PreEditDensityCacheCarveParity",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgePreEditDensityCacheParityTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace VoxelForgeTest;
|
||||
|
||||
FTestWorld World;
|
||||
World.Build(/*Seed*/1337, /*GapChunks*/2);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
|
||||
int32 SlotTop = 0;
|
||||
int32 SlotBottom = 0;
|
||||
if (!World.GetSlotVoxelZRange(FTestWorld::SlotTunnelNetwork, SlotTop, SlotBottom))
|
||||
{
|
||||
AddError(TEXT("TunnelNetwork slot range was unavailable."));
|
||||
return false;
|
||||
}
|
||||
const int32 MidVoxelZ = (SlotTop + SlotBottom) / 2;
|
||||
const int32 TileZ = FMath::FloorToInt(
|
||||
static_cast<float>(MidVoxelZ) / CHUNK_SIZE) * CHUNK_SIZE;
|
||||
const FIntVector TileOrigin(0, 0, TileZ);
|
||||
TestEqual(TEXT("the runtime pre-edit cache holds only the pawn's current tile"),
|
||||
VoxelForgePreEditDensityCache::MaxCachedTiles, 1);
|
||||
TestEqual(TEXT("the cache target radius matches its one-tile memory bound"),
|
||||
VoxelForgePreEditDensityCache::PawnRadiusChunks, 0);
|
||||
|
||||
TStrongObjectPtr<UVoxelMarchingCubesMesher> Mesher(
|
||||
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
Mesher->SetGenerator(World.Generator.Get());
|
||||
|
||||
FScopedVoxelStackRegistration StackRegistration(/*InLODLevel*/0);
|
||||
FVoxelSharedDensityGrid PreEditGrid;
|
||||
int64 BaseDensitySamples = 0;
|
||||
Mesher->GenerateMesh(TileOrigin, 1, CHUNK_SIZE, nullptr, INT32_MIN, INT32_MAX,
|
||||
&BaseDensitySamples, &PreEditGrid);
|
||||
TestEqual(TEXT("the cache captured the complete 35³ lattice"),
|
||||
PreEditGrid.Samples.Num(), (CHUNK_SIZE + 3) * (CHUNK_SIZE + 3) * (CHUNK_SIZE + 3));
|
||||
TestEqual(TEXT("the cache uses the one-voxel mesh halo"), PreEditGrid.Dim, CHUNK_SIZE + 3);
|
||||
TestTrue(TEXT("the initial tile sampled its density lattice"), BaseDensitySamples > 0);
|
||||
if (PreEditGrid.Samples.Num() == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FRandomStream Random(0x4D17);
|
||||
int32 ComparedCarves = 0;
|
||||
int32 NonEmptyPairs = 0;
|
||||
for (int32 CarveIndex = 0; CarveIndex < 16; ++CarveIndex)
|
||||
{
|
||||
FVoxelModification Modification;
|
||||
Modification.Center = FVector(
|
||||
TileOrigin.X + Random.RandRange(5, 27),
|
||||
TileOrigin.Y + Random.RandRange(5, 27),
|
||||
TileOrigin.Z + Random.RandRange(5, 27));
|
||||
Modification.Radius = static_cast<float>(Random.RandRange(3, 7));
|
||||
Modification.Strength = -50.0f;
|
||||
const TArray<FIntVector> Affected = World.DiffLayer->ApplyModification(Modification);
|
||||
TestTrue(FString::Printf(TEXT("random carve %d affected at least one chunk"), CarveIndex),
|
||||
Affected.Num() > 0);
|
||||
|
||||
FVoxelSharedDensityGrid EditedGrid;
|
||||
TestTrue(FString::Printf(TEXT("random carve %d replayed over pre-edit samples"), CarveIndex),
|
||||
VoxelForgePreEditDensityCache::BuildEditedGrid(
|
||||
PreEditGrid, World.DiffLayer.Get(), EditedGrid));
|
||||
|
||||
const FVoxelMeshData FullRegeneration = Mesher->GenerateMesh(
|
||||
TileOrigin, 1, CHUNK_SIZE, nullptr, INT32_MIN, INT32_MAX);
|
||||
const FVoxelMeshData CachedRemesh = Mesher->GenerateMesh(
|
||||
TileOrigin, 1, CHUNK_SIZE, nullptr, INT32_MIN, INT32_MAX,
|
||||
nullptr, nullptr, &EditedGrid);
|
||||
const FString FullHash = VoxelForgeGeometryHash::Compute(FullRegeneration);
|
||||
const FString CachedHash = VoxelForgeGeometryHash::Compute(CachedRemesh);
|
||||
TestEqual(FString::Printf(TEXT("carve %d cached/full tile hash"), CarveIndex),
|
||||
CachedHash, FullHash);
|
||||
if (CachedHash == FullHash)
|
||||
{
|
||||
++ComparedCarves;
|
||||
}
|
||||
if (!FullRegeneration.IsEmpty() && !CachedRemesh.IsEmpty())
|
||||
{
|
||||
++NonEmptyPairs;
|
||||
}
|
||||
}
|
||||
|
||||
TestEqual(TEXT("all random carves matched a full tile regeneration"), ComparedCarves, 16);
|
||||
TestTrue(TEXT("the parity set exercised actual carve geometry"), NonEmptyPairs > 0);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("pre_edit_density_cache: carves=%d hash_matches=%d nonempty_pairs=%d samples=%d bytes=%d"),
|
||||
16, ComparedCarves, NonEmptyPairs, PreEditGrid.Samples.Num(),
|
||||
PreEditGrid.Samples.Num() * static_cast<int32>(sizeof(float))));
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "VoxelTypes.h"
|
||||
|
||||
namespace VoxelCollisionDesiredTiles
|
||||
{
|
||||
inline void AddBox(TSet<FVoxelTileKey>& OutTiles, const FIntVector& Center,
|
||||
int32 XYRadius, int32 ZBelow, int32 ZAbove)
|
||||
{
|
||||
const int32 SafeXYRadius = FMath::Max(0, XYRadius);
|
||||
const int32 SafeZBelow = FMath::Max(0, ZBelow);
|
||||
const int32 SafeZAbove = FMath::Max(0, ZAbove);
|
||||
for (int32 Z = -SafeZBelow; Z <= SafeZAbove; ++Z)
|
||||
{
|
||||
for (int32 Y = -SafeXYRadius; Y <= SafeXYRadius; ++Y)
|
||||
{
|
||||
for (int32 X = -SafeXYRadius; X <= SafeXYRadius; ++X)
|
||||
{
|
||||
OutTiles.Add(FVoxelTileKey(Center + FIntVector(X, Y, Z), 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2709,7 +2709,8 @@ void UVoxelGenerator::EndDensityBlock() const
|
||||
GVoxelDensityBlockSession.End();
|
||||
}
|
||||
|
||||
float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const
|
||||
float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
|
||||
float* OutPreEditDensity) const
|
||||
{
|
||||
// Soundness mode: for a sample in a block whose core or passage-carving reach bit is clear,
|
||||
// evaluate the complete canonical field once with the bypass bit set. The normal call then
|
||||
@@ -3696,6 +3697,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// Fill (diff > 0) → Result -= positive → Result decreases → more solid ✓
|
||||
VoxelDensityProfile::FScopedTimer DensityDiffLayerTimer(
|
||||
VoxelDensityProfile::EBucket::DensityDiffLayer);
|
||||
// The sync-remesh cache keeps the exact procedural field after every structural post, but
|
||||
// before player edits. The cached lattice is replayed with the current DiffLayer snapshot so
|
||||
// a carve sees the same subtraction order and float result as this canonical path.
|
||||
if (OutPreEditDensity != nullptr)
|
||||
{
|
||||
*OutPreEditDensity = Result;
|
||||
}
|
||||
if (DiffLayer && DiffLayer->HasAnyMods())
|
||||
{
|
||||
// ── PER-CHUNK MOD SNAPSHOT ──
|
||||
|
||||
@@ -96,11 +96,14 @@ void UVoxelMarchingCubesMesher::InitializeConsoleSwitches()
|
||||
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 Step, int32 InCellsPerAxis,
|
||||
TArray<uint8>* OutCaptureGrid,
|
||||
int32 BandZMinVox, int32 BandZMaxVox,
|
||||
int64* OutDensitySampleCount)
|
||||
int64* OutDensitySampleCount,
|
||||
FVoxelSharedDensityGrid* OutPreEditDensityGrid,
|
||||
const FVoxelSharedDensityGrid* InSharedDensityGrid)
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
|
||||
FVoxelMeshData MeshData;
|
||||
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
|
||||
if (OutPreEditDensityGrid) { OutPreEditDensityGrid->Reset(); }
|
||||
if (OutDensitySampleCount) { *OutDensitySampleCount = 0; }
|
||||
if (!Generator) return MeshData;
|
||||
VoxelDensityProfile::FScopedTimer MesherProfileTimer(
|
||||
@@ -383,7 +386,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
// COUPE DE CONTENU PAR STRATE — restreint le maillage (et l'échantillonnage) aux cellules
|
||||
// dont l'intervalle Z chevauche la bande [BandZMinVox, BandZMaxVox] (voxels inclusifs).
|
||||
// Les tuiles à capture ne sont jamais bandées (niveau 0 — garde-fou ci-dessous).
|
||||
if (OutCaptureGrid) { BandZMinVox = INT32_MIN; BandZMaxVox = INT32_MAX; }
|
||||
if (OutCaptureGrid || OutPreEditDensityGrid)
|
||||
{
|
||||
BandZMinVox = INT32_MIN;
|
||||
BandZMaxVox = INT32_MAX;
|
||||
}
|
||||
int32 CzLo = 0, CzHi = CellsPerAxis - 1;
|
||||
if (BandZMinVox > INT32_MIN || BandZMaxVox < INT32_MAX)
|
||||
{
|
||||
@@ -660,7 +667,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
|
||||
static thread_local TArray<float> DensityGrid;
|
||||
DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
|
||||
const FVoxelSharedDensityGrid* ReuseGrid = SharedDensityGrid;
|
||||
const FVoxelSharedDensityGrid* ReuseGrid = InSharedDensityGrid != nullptr
|
||||
? InSharedDensityGrid : SharedDensityGrid;
|
||||
const int32 SharedDeltaX = ReuseGrid
|
||||
? (OriginVoxels.X - ReuseGrid->OriginVoxels.X) / Step : 0;
|
||||
const int32 SharedDeltaY = ReuseGrid
|
||||
@@ -680,6 +688,17 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
&& SharedDeltaX + GridDim < ReuseGrid->Dim
|
||||
&& SharedDeltaY + GridDim < ReuseGrid->Dim
|
||||
&& SharedDeltaZ + GridDim < ReuseGrid->Dim;
|
||||
const bool bCapturePreEditGrid = OutPreEditDensityGrid != nullptr
|
||||
&& !bUseSharedDensityGrid
|
||||
&& Step == 1 && CellsPerAxis == CHUNK_SIZE
|
||||
&& CzLo == 0 && CzHi == CellsPerAxis - 1;
|
||||
if (bCapturePreEditGrid)
|
||||
{
|
||||
OutPreEditDensityGrid->OriginVoxels = OriginVoxels + FIntVector(-Step, -Step, -Step);
|
||||
OutPreEditDensityGrid->Step = Step;
|
||||
OutPreEditDensityGrid->Dim = MDim;
|
||||
OutPreEditDensityGrid->Samples.SetNumUninitialized(MDim * MDim * MDim);
|
||||
}
|
||||
// Bande de strate : seules les rangées Z réellement lues (cellules CzLo..CzHi + marges de
|
||||
// gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu.
|
||||
VoxelDensityProfile::FScopedTimer MesherDensityGridTimer(
|
||||
@@ -731,10 +750,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
VoxelGenLOD::TileReachBlockIndex =
|
||||
((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY)
|
||||
* ReachBlocksPerAxis + ReachBlockX;
|
||||
float PreEditDensity = 0.0f;
|
||||
DensityGrid[LocalIndex] = Generator->GetDensityAt(
|
||||
OriginVoxels.X + gx * Step,
|
||||
OriginVoxels.Y + gy * Step,
|
||||
OriginVoxels.Z + gz * Step);
|
||||
OriginVoxels.Z + gz * Step,
|
||||
bCapturePreEditGrid ? &PreEditDensity : nullptr);
|
||||
if (bCapturePreEditGrid)
|
||||
{
|
||||
OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity;
|
||||
}
|
||||
if (OutDensitySampleCount)
|
||||
{
|
||||
++(*OutDensitySampleCount);
|
||||
@@ -848,6 +873,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
}
|
||||
}
|
||||
|
||||
if (OutPreEditDensityGrid && !bCapturePreEditGrid)
|
||||
{
|
||||
OutPreEditDensityGrid->Reset();
|
||||
}
|
||||
|
||||
// Lecture grille (avec offset de marge) + gradient central depuis la grille.
|
||||
auto SampleG = [&](int32 gx, int32 gy, int32 gz) -> float
|
||||
{
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// Bounded helpers for replaying player edits over the pre-edit L0 density lattice.
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "VoxelDiffLayer.h"
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
|
||||
namespace VoxelForgePreEditDensityCache
|
||||
{
|
||||
// Keep only the tile containing the pawn: 35³ floats is a fixed 171,500-byte ceiling. The
|
||||
// current tile is the synchronous brush-centre remesh target and avoids prewarming empty neighbors.
|
||||
constexpr int32 PawnRadiusChunks = 0;
|
||||
constexpr int32 MaxCachedTiles = 1;
|
||||
|
||||
inline bool IsPawnRadiusTile(const FVoxelTileKey& Tile, const FIntVector& PawnChunk)
|
||||
{
|
||||
return Tile.Level == 0
|
||||
&& FMath::Abs(Tile.Coord.X - PawnChunk.X) <= PawnRadiusChunks
|
||||
&& FMath::Abs(Tile.Coord.Y - PawnChunk.Y) <= PawnRadiusChunks
|
||||
&& FMath::Abs(Tile.Coord.Z - PawnChunk.Z) <= PawnRadiusChunks;
|
||||
}
|
||||
|
||||
inline bool BuildEditedGrid(const FVoxelSharedDensityGrid& PreEditGrid,
|
||||
const UVoxelDiffLayer* DiffLayer,
|
||||
FVoxelSharedDensityGrid& OutEditedGrid)
|
||||
{
|
||||
OutEditedGrid.Reset();
|
||||
if (PreEditGrid.Step != 1 || PreEditGrid.Dim != CHUNK_SIZE + 3
|
||||
|| PreEditGrid.Samples.Num() != PreEditGrid.Dim * PreEditGrid.Dim * PreEditGrid.Dim)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutEditedGrid = PreEditGrid;
|
||||
if (DiffLayer == nullptr || !DiffLayer->HasAnyMods())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const FIntVector LastVoxel = PreEditGrid.OriginVoxels
|
||||
+ FIntVector(PreEditGrid.Dim - 1, PreEditGrid.Dim - 1, PreEditGrid.Dim - 1)
|
||||
* PreEditGrid.Step;
|
||||
const FIntVector MinChunk(
|
||||
FMath::FloorToInt(static_cast<float>(PreEditGrid.OriginVoxels.X) / CHUNK_SIZE),
|
||||
FMath::FloorToInt(static_cast<float>(PreEditGrid.OriginVoxels.Y) / CHUNK_SIZE),
|
||||
FMath::FloorToInt(static_cast<float>(PreEditGrid.OriginVoxels.Z) / CHUNK_SIZE));
|
||||
const FIntVector MaxChunk(
|
||||
FMath::FloorToInt(static_cast<float>(LastVoxel.X) / CHUNK_SIZE),
|
||||
FMath::FloorToInt(static_cast<float>(LastVoxel.Y) / CHUNK_SIZE),
|
||||
FMath::FloorToInt(static_cast<float>(LastVoxel.Z) / CHUNK_SIZE));
|
||||
|
||||
TMap<FIntVector, TArray<FVoxelModification>> ChunkMods;
|
||||
for (int32 CZ = MinChunk.Z;; ++CZ)
|
||||
{
|
||||
for (int32 CY = MinChunk.Y;; ++CY)
|
||||
{
|
||||
for (int32 CX = MinChunk.X;; ++CX)
|
||||
{
|
||||
TArray<FVoxelModification> Mods;
|
||||
DiffLayer->GetChunkModsSnapshot(FIntVector(CX, CY, CZ), Mods);
|
||||
ChunkMods.Add(FIntVector(CX, CY, CZ), MoveTemp(Mods));
|
||||
if (CX == MaxChunk.X) { break; }
|
||||
}
|
||||
if (CY == MaxChunk.Y) { break; }
|
||||
}
|
||||
if (CZ == MaxChunk.Z) { break; }
|
||||
}
|
||||
|
||||
const int32 Dim = PreEditGrid.Dim;
|
||||
for (int32 Z = 0; Z < Dim; ++Z)
|
||||
{
|
||||
const int32 WorldZ = PreEditGrid.OriginVoxels.Z + Z * PreEditGrid.Step;
|
||||
const int32 ChunkZ = FMath::FloorToInt(static_cast<float>(WorldZ) / CHUNK_SIZE);
|
||||
for (int32 Y = 0; Y < Dim; ++Y)
|
||||
{
|
||||
const int32 WorldY = PreEditGrid.OriginVoxels.Y + Y * PreEditGrid.Step;
|
||||
const int32 ChunkY = FMath::FloorToInt(static_cast<float>(WorldY) / CHUNK_SIZE);
|
||||
for (int32 X = 0; X < Dim; ++X)
|
||||
{
|
||||
const int32 WorldX = PreEditGrid.OriginVoxels.X + X * PreEditGrid.Step;
|
||||
const int32 ChunkX = FMath::FloorToInt(static_cast<float>(WorldX) / CHUNK_SIZE);
|
||||
const TArray<FVoxelModification>* Mods = ChunkMods.Find(
|
||||
FIntVector(ChunkX, ChunkY, ChunkZ));
|
||||
if (Mods == nullptr || Mods->Num() == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const int32 Index = (Z * Dim + Y) * Dim + X;
|
||||
// Match GetDensityAt's canonical post-edit operation exactly.
|
||||
OutEditedGrid.Samples[Index] -= UVoxelDiffLayer::EvaluateMods(
|
||||
*Mods, static_cast<float>(WorldX), static_cast<float>(WorldY),
|
||||
static_cast<float>(WorldZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -164,7 +164,8 @@ public:
|
||||
*
|
||||
* @param WorldX, WorldY, WorldZ - coordonnées monde en voxels (pas cm)
|
||||
*/
|
||||
float GetDensityAt(float WorldX, float WorldY, float WorldZ) const;
|
||||
float GetDensityAt(float WorldX, float WorldY, float WorldZ,
|
||||
float* OutPreEditDensity = nullptr) const;
|
||||
|
||||
/**
|
||||
* Open a worker-local lattice hand-off for the following GetDensityAt calls. The mesher uses
|
||||
|
||||
@@ -76,7 +76,9 @@ public:
|
||||
FVoxelMeshData GenerateMesh(FIntVector OriginVoxels, int32 Step = 1, int32 CellsPerAxis = CHUNK_SIZE,
|
||||
TArray<uint8>* OutCaptureGrid = nullptr,
|
||||
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
|
||||
int64* OutDensitySampleCount = nullptr);
|
||||
int64* OutDensitySampleCount = nullptr,
|
||||
FVoxelSharedDensityGrid* OutPreEditDensityGrid = nullptr,
|
||||
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr);
|
||||
|
||||
//=========================================================================
|
||||
// SERVICES (injectés par AVoxelWorld)
|
||||
|
||||
@@ -81,6 +81,12 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming")
|
||||
int32 MaxUnloadsPerFrame = 6;
|
||||
|
||||
// Independent level-0 collision radius around the locally controlled pawn. Render tiles outside
|
||||
// this cube remain visible but do not queue Chaos cooks; registered streaming anchors keep their
|
||||
// own authored collision boxes, and the collision gate can extend a landing-support column.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1", ClampMax = "4"))
|
||||
int32 PawnCollisionRadiusChunks = 1;
|
||||
|
||||
// Strate-aware VERTICAL streaming: don't load chunks into a strate you can't see.
|
||||
// Seals + the inter-strate bedrock gap are light-tight (§8.7), so the strate above/below
|
||||
// the one you're in is fully occluded — loading it just wastes scene primitives (the
|
||||
|
||||
@@ -149,6 +149,9 @@ struct FChunkResult
|
||||
// it to UVoxelDensityVolume::IngestTileCapture so the density clipmap reuses the mesher's samples
|
||||
// instead of re-sampling. Moved (not copied) through the MPSC queue. See UVoxelDensityVolume.
|
||||
TArray<uint8> CaptureGrid;
|
||||
// Exact pre-edit level-0 lattice for the bounded local-player dig cache. Sampled on the worker
|
||||
// with the tile's normal generator context and moved through the result queue.
|
||||
FVoxelSharedDensityGrid PreEditDensityGrid;
|
||||
};
|
||||
|
||||
/** One worker-produced mesh identity emitted only by the explicit parity harness. */
|
||||
@@ -550,7 +553,7 @@ private:
|
||||
bool MeasureHeadlessCollisionGateStressTerrain(
|
||||
const FVector& WorldPosition, float CapsuleHalfHeightCm,
|
||||
float& OutFeetDensity, float& OutCenterDensity,
|
||||
float& OutSurfaceLocalVoxelZ, float& OutClearanceCm) const;
|
||||
float& OutSurfaceLocalVoxelZ, float& OutClearanceCm);
|
||||
void AdvanceHeadlessSurfaceFallTest(FVector& InOutPlayerPosition,
|
||||
FVector& InOutPlayerHeading, APawn* PlayerPawn);
|
||||
void InitializeHeadlessStrateCrossingTest();
|
||||
@@ -769,7 +772,9 @@ public:
|
||||
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
|
||||
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
||||
FChunkResult& Result,
|
||||
const std::atomic<bool>* ObsoleteFlag = nullptr);
|
||||
const std::atomic<bool>* ObsoleteFlag = nullptr,
|
||||
bool bWantPreEditDensityGrid = false,
|
||||
const FVoxelSharedDensityGrid* SharedDensityOverride = nullptr);
|
||||
|
||||
/**
|
||||
* Game-thread apply for one gen result (shared by ProcessPendingChunks + SyncRemeshTile):
|
||||
@@ -859,6 +864,7 @@ public:
|
||||
FVoxelTileKey& OutSupportTile) const;
|
||||
|
||||
bool IsTileCollisionReady(const FVoxelTileKey& Tile) const;
|
||||
bool IsTileCollisionDesired(const FVoxelTileKey& Tile) const;
|
||||
bool IsCollisionTileUnavailable(const FVoxelTileKey& Tile,
|
||||
const TCHAR*& OutReason) const;
|
||||
bool IsCollisionReadyFromSupportTile(const FVoxelTileKey& CandidateTile,
|
||||
@@ -872,10 +878,10 @@ public:
|
||||
FVoxelTileKey* OutPendingTile = nullptr) const;
|
||||
void RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile);
|
||||
|
||||
/** Cancel only downward pawn motion while the tile beneath its feet is unresolved. Horizontal
|
||||
* input and movement remain live. The world tick is installed as the pawn prerequisite in the
|
||||
* implementation (and directly on its movement component) so this check runs before
|
||||
* CharacterMovementComponent physics. */
|
||||
/** Hold vertical character motion in gravity-free MOVE_Flying while a swept pending support tile
|
||||
* intersects the capsule, preserving horizontal movement until collision is ready. Restore the
|
||||
* previous mode, gravity, and downward speed on release. The world tick is a prerequisite of the
|
||||
* pawn and movement component so the gate check runs before movement physics. */
|
||||
void UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition, float DeltaTime);
|
||||
void EngagePawnCollisionGate(APawn* Pawn);
|
||||
void ReleasePawnCollisionGate();
|
||||
@@ -951,6 +957,9 @@ public:
|
||||
|
||||
// Player's level-0 tile coord (= chunk coord). The desired set is rebuilt when this changes.
|
||||
FIntVector CurrentCenterChunk = FIntVector::ZeroValue;
|
||||
// Exact unedited L0 lattices retained around the player for same-frame carve remeshes.
|
||||
// The radius helper bounds this to 27 × 35³ floats (4.42 MiB).
|
||||
TMap<FVoxelTileKey, FVoxelSharedDensityGrid> PreEditDensityCache;
|
||||
|
||||
// Non-player streaming anchors (AI now, remote players later — ARCHITECTURE §9.3). BuildDesiredTiles
|
||||
// folds each anchor's small level-0 box into the desired set (same DesiredStamped machinery → the
|
||||
@@ -960,6 +969,7 @@ public:
|
||||
// Adds each anchor's level-0 tiles to DesiredSorted/DesiredStamped (dedup vs the player clipmap)
|
||||
// and records the ones ONLY a CollisionOnly anchor wants in CollisionOnlyTiles.
|
||||
void AddAnchorDesiredTiles();
|
||||
void ReconcileCollisionDesiredTiles();
|
||||
// Forces a desired-set rebuild next Tick even if neither the player nor an anchor crossed a tile
|
||||
// boundary (set by UnregisterStreamingAnchor so a removed anchor's tiles get culled).
|
||||
bool bForceDesiredRebuild = false;
|
||||
@@ -971,6 +981,9 @@ public:
|
||||
// without an O(loaded) scan. Both empty when there are no CollisionOnly anchors → zero cost.
|
||||
TSet<FVoxelTileKey> CollisionOnlyTiles;
|
||||
TSet<FVoxelTileKey> PrevCollisionOnlyTiles;
|
||||
// Independent from DesiredSorted: rendered LOD0 tiles outside the pawn radius remain visible
|
||||
// without a Chaos body. Pawn radius, anchor boxes, and collision-gate support are unioned here.
|
||||
TSet<FVoxelTileKey> CollisionDesiredTiles;
|
||||
// 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;
|
||||
@@ -1103,14 +1116,18 @@ public:
|
||||
bool bHeadlessCollisionGateStressTestFailed = false;
|
||||
bool bHeadlessCollisionGateStressTestExitRequested = false;
|
||||
bool bHeadlessCollisionGateStressTestGateWasEngaged = false;
|
||||
bool bHeadlessCollisionGateStressTestHorizontalGateProbeApplied = false;
|
||||
bool bHeadlessCollisionGateStressTestFullStop = false;
|
||||
bool bHeadlessCollisionGateStressTestStartHeightCrossed = false;
|
||||
bool bHeadlessCollisionGateStressTestTerrainViolation = false;
|
||||
bool bHeadlessCollisionGateStressTestTerrainMeasurementMissing = false;
|
||||
bool bHeadlessCollisionGateStressTestLanded = false;
|
||||
bool bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured = false;
|
||||
bool bHeadlessCollisionGateStressTestSettingsReported = false;
|
||||
bool bHeadlessCollisionGateStressDeepDrop = false;
|
||||
bool bHeadlessCollisionGateStressDeepDropPrepared = false;
|
||||
bool bHeadlessCollisionGateStressDeepDropPrewarmReported = false;
|
||||
bool bHeadlessCollisionGateStressDeepDropSurfaceCached = false;
|
||||
float HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f;
|
||||
float HeadlessCollisionGateStressTestSpawnHeightVoxels = 10.0f;
|
||||
float HeadlessCollisionGateStressTestDurationSeconds = 2.0f;
|
||||
@@ -1139,6 +1156,8 @@ public:
|
||||
FVector HeadlessCollisionGateStressTestLastActualPosition = FVector::ZeroVector;
|
||||
FVector HeadlessCollisionGateStressTestStartPosition = FVector::ZeroVector;
|
||||
FVector HeadlessCollisionGateStressTestMinClearancePosition = FVector::ZeroVector;
|
||||
FVector2D HeadlessCollisionGateStressDeepDropSurfaceXYVox = FVector2D::ZeroVector;
|
||||
float HeadlessCollisionGateStressDeepDropSurfaceZVox = 0.0f;
|
||||
FVoxelTileKey HeadlessCollisionGateStressTestLaunchSupportTile;
|
||||
FString HeadlessCollisionGateStressTestFailureReason;
|
||||
|
||||
@@ -1235,6 +1254,9 @@ public:
|
||||
bool bCollisionGateEnabled = true;
|
||||
float SavedCharacterGravityScale = 1.0f;
|
||||
bool bSavedCharacterGravityScale = false;
|
||||
uint8 SavedCharacterMovementMode = 0;
|
||||
uint8 SavedCharacterCustomMovementMode = 0;
|
||||
bool bSavedCharacterMovementMode = false;
|
||||
float SavedGateVerticalVelocity = 0.0f;
|
||||
bool bSavedGateVerticalVelocity = false;
|
||||
bool bPawnCollisionGateEngaged = false;
|
||||
|
||||
Reference in new issue
Block a user