diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeVerticalStrateReachTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeVerticalStrateReachTest.cpp new file mode 100644 index 0000000..9815bac --- /dev/null +++ b/Source/VoxelForge/Private/Tests/VoxelForgeVerticalStrateReachTest.cpp @@ -0,0 +1,89 @@ +// Surface-strate streaming must keep the ground visible when the pawn is high in the same strate. + +#if WITH_DEV_AUTOMATION_TESTS + +#include "Misc/AutomationTest.h" + +#include "VoxelClipmapDesiredTiles.h" + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeVerticalStrateReachTest, + "VoxelForge.Streaming.VerticalStrateReach", + EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) + +bool FVoxelForgeVerticalStrateReachTest::RunTest(const FString& Parameters) +{ + constexpr int32 StrateBottomZ = -63; + constexpr int32 StrateTopZ = 0; + constexpr int32 PlayerChunkZ = 0; // high above this surface strate's ground + + int32 MinChunkZ = MIN_int32; + int32 MaxChunkZ = MAX_int32; + VoxelClipmapDesiredTiles::ResolveVerticalBounds( + PlayerChunkZ, 5, 5, 3, true, true, + StrateTopZ, StrateBottomZ, MinChunkZ, MaxChunkZ); + TestEqual(TEXT("vertical reach follows the full surface strate plus margin"), MinChunkZ, -66); + TestEqual(TEXT("open-strate reach ends at the surface cap margin"), MaxChunkZ, 3); + + VoxelClipmapDesiredTiles::FParameters Clipmap; + Clipmap.ClipRadius = 3; + Clipmap.MaxClipLevel = 3; + Clipmap.RenderDistanceChunks = 768; + Clipmap.bFarSheetRing = true; + Clipmap.FarSheetSpanLevels = 2; + + TArray Desired; + VoxelClipmapDesiredTiles::Build( + FIntVector(0, 0, PlayerChunkZ), MinChunkZ, MaxChunkZ, Clipmap, Desired); + + TArray CountsByLevel; + CountsByLevel.Init(0, 6); + for (const FVoxelTileKey& Tile : Desired) + { + if (CountsByLevel.IsValidIndex(Tile.Level)) + { + ++CountsByLevel[Tile.Level]; + } + } + + const int32 ExpectedCounts[] = {343, 218, 178, 178, 178, 9586}; + for (int32 Level = 0; Level < static_cast(UE_ARRAY_COUNT(ExpectedCounts)); ++Level) + { + TestEqual(FString::Printf(TEXT("desired tile count at level %d"), Level), + CountsByLevel[Level], ExpectedCounts[Level]); + } + + const auto HasTile = [&Desired](int32 Level, const FIntVector& Coord) + { + return Desired.ContainsByPredicate([Level, &Coord](const FVoxelTileKey& Tile) + { + return Tile.Level == Level && Tile.Coord == Coord; + }); + }; + + TestTrue(TEXT("level 0 tile around the high pawn is present"), + HasTile(0, FIntVector(0, 0, 0))); + TestTrue(TEXT("level 4 bridge tile covering the ground below the pawn is present"), + HasTile(4, FIntVector(0, 0, -3))); + TestTrue(TEXT("adjacent level 4 ground tile is present"), + HasTile(4, FIntVector(1, 0, -3))); + TestTrue(TEXT("outer level 5 sheet reaches the 768-chunk horizon"), + HasTile(5, FIntVector(24, 0, 0))); + TestFalse(TEXT("clamping does not request a full hidden LOD0 column"), + HasTile(0, FIntVector(0, 0, -47))); + + for (int32 Level = 0; Level <= 5; ++Level) + { + TestTrue(FString::Printf(TEXT("clip level %d is non-empty"), Level), + CountsByLevel[Level] > 0); + } + + AddInfo(FString::Printf( + TEXT("center_z=%d strate=[%d..%d] view=[%d..%d] levels=[%d,%d,%d,%d,%d,%d] total=%d"), + PlayerChunkZ, StrateBottomZ, StrateTopZ, MinChunkZ, MaxChunkZ, + CountsByLevel[0], CountsByLevel[1], CountsByLevel[2], CountsByLevel[3], + CountsByLevel[4], CountsByLevel[5], Desired.Num())); + return true; +} + +#endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Private/VoxelClipmapDesiredTiles.h b/Source/VoxelForge/Private/VoxelClipmapDesiredTiles.h new file mode 100644 index 0000000..18bb989 --- /dev/null +++ b/Source/VoxelForge/Private/VoxelClipmapDesiredTiles.h @@ -0,0 +1,176 @@ +#pragma once + +#include "VoxelTypes.h" + +namespace VoxelClipmapDesiredTiles +{ + struct FParameters + { + int32 ClipRadius = 3; + int32 MaxClipLevel = 4; + int32 RenderDistanceChunks = 0; + bool bFarSheetRing = true; + int32 FarSheetSpanLevels = 2; + }; + + FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor) + { + return Value >= 0 ? (Value / Divisor) : -(((-Value) + Divisor - 1) / Divisor); + } + + FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor) + { + return FIntVector(FloorDiv(Value.X, Divisor), FloorDiv(Value.Y, Divisor), + FloorDiv(Value.Z, Divisor)); + } + + FORCEINLINE int32 OuterShellRadius(const FParameters& Parameters, int32 Radius, int32 MaxLevel) + { + if (Parameters.RenderDistanceChunks <= 0) + { + return Radius; + } + return FMath::Max(Radius, + (Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel); + } + + FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel, + int32& OutLevel, int32& OutRadius) + { + OutLevel = MaxLevel; + OutRadius = OuterShellRadius(Parameters, Radius, MaxLevel); + if (Parameters.bFarSheetRing && OutRadius > Radius) + { + OutLevel = MaxLevel + FMath::Clamp(Parameters.FarSheetSpanLevels, 1, 4); + OutRadius = FMath::Max(1, + (Parameters.RenderDistanceChunks + (1 << OutLevel) - 1) >> OutLevel); + } + } + + FORCEINLINE void ResolveVerticalBounds( + int32 CenterChunkZ, int32 ViewDistanceUp, int32 ViewDistanceDown, + int32 StrateViewMarginChunks, bool bClampToStrate, bool bHasStrate, + int32 StrateTopChunkZ, int32 StrateBottomChunkZ, + int32& OutMinChunkZ, int32& OutMaxChunkZ) + { + OutMinChunkZ = MIN_int32; + OutMaxChunkZ = MAX_int32; + if (!bClampToStrate) + { + return; + } + + OutMinChunkZ = CenterChunkZ - ViewDistanceDown; + OutMaxChunkZ = CenterChunkZ + ViewDistanceUp; + if (bHasStrate) + { + OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks; + OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks; + } + } + + FORCEINLINE void Build(const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ, + const FParameters& Parameters, TArray& OutTiles) + { + OutTiles.Reset(); + const int32 Radius = FMath::Max(1, Parameters.ClipRadius); + const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8); + const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel); + int32 SheetLevel = MaxLevel; + int32 SheetRadius = OuterRadius; + OuterShell(Parameters, Radius, MaxLevel, SheetLevel, SheetRadius); + const bool bSheetRing = SheetLevel > MaxLevel; + const int32 MaxMCRingLevel = bSheetRing ? SheetLevel - 1 : MaxLevel; + + for (int32 Level = 0; Level <= MaxMCRingLevel; ++Level) + { + const int32 Pow = 1 << Level; + const FIntVector CenterLevel = FloorDiv(Center, Pow); + const FIntVector CenterFiner = Level > 0 + ? FloorDiv(Center, Pow >> 1) : FIntVector::ZeroValue; + const int32 LevelRadius = (Level == MaxLevel && !bSheetRing) + ? OuterRadius : Radius; + + int32 DeltaZMin = -LevelRadius; + int32 DeltaZMax = LevelRadius; + if (LevelRadius > Radius && MinChunkZ != MIN_int32) + { + DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z); + DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z); + } + + for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ) + for (int32 DeltaY = -LevelRadius; DeltaY <= LevelRadius; ++DeltaY) + for (int32 DeltaX = -LevelRadius; DeltaX <= LevelRadius; ++DeltaX) + { + const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ); + const int32 TileMinZ = Tile.Z << Level; + const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1; + if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ) + { + continue; + } + + if (Level > 0) + { + const bool bCovered = + (2 * Tile.X >= CenterFiner.X - Radius) + && (2 * Tile.X + 1 <= CenterFiner.X + Radius) + && (2 * Tile.Y >= CenterFiner.Y - Radius) + && (2 * Tile.Y + 1 <= CenterFiner.Y + Radius) + && (2 * Tile.Z >= CenterFiner.Z - Radius) + && (2 * Tile.Z + 1 <= CenterFiner.Z + Radius); + if (bCovered) + { + continue; + } + } + + OutTiles.Emplace(Tile, Level); + } + } + + if (bSheetRing) + { + const int32 SheetPow = 1 << SheetLevel; + const FIntVector CenterSheet = FloorDiv(Center, SheetPow); + const FIntVector CenterMC = FloorDiv(Center, 1 << MaxMCRingLevel); + const int32 LevelDelta = SheetLevel - MaxMCRingLevel; + + int32 DeltaZMin = -SheetRadius; + int32 DeltaZMax = SheetRadius; + if (MinChunkZ != MIN_int32) + { + DeltaZMin = FMath::Max(DeltaZMin, + FloorDiv(MinChunkZ, SheetPow) - CenterSheet.Z); + DeltaZMax = FMath::Min(DeltaZMax, + FloorDiv(MaxChunkZ, SheetPow) - CenterSheet.Z); + } + + for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ) + for (int32 DeltaY = -SheetRadius; DeltaY <= SheetRadius; ++DeltaY) + for (int32 DeltaX = -SheetRadius; DeltaX <= SheetRadius; ++DeltaX) + { + const FIntVector Tile = CenterSheet + FIntVector(DeltaX, DeltaY, DeltaZ); + const int32 TileMinZ = Tile.Z << SheetLevel; + const int32 TileMaxZ = ((Tile.Z + 1) << SheetLevel) - 1; + if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ) + { + continue; + } + + const bool bCovered = + ((Tile.X << LevelDelta) >= CenterMC.X - Radius) + && ((((Tile.X + 1) << LevelDelta) - 1) <= CenterMC.X + Radius) + && ((Tile.Y << LevelDelta) >= CenterMC.Y - Radius) + && ((((Tile.Y + 1) << LevelDelta) - 1) <= CenterMC.Y + Radius) + && ((Tile.Z << LevelDelta) >= CenterMC.Z - Radius) + && ((((Tile.Z + 1) << LevelDelta) - 1) <= CenterMC.Z + Radius); + if (!bCovered) + { + OutTiles.Emplace(Tile, SheetLevel); + } + } + } + } +} diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 82dd60c..d230762 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -21,6 +21,7 @@ #include "VoxelCaveMorphology.h" #include "VoxelPassageGeometry.h" #include "VoxelTilePostReach.h" +#include "VoxelClipmapDesiredTiles.h" // IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc // APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController était // complet par transitivité seulement : on l'inclut explicitement, c'est exactement la fragilité @@ -53,6 +54,8 @@ #include "Serialization/JsonWriter.h" #include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0) +static int32 VF_MaxMarchingCubesLevel(const UVoxelSettings* Settings); + #if WITH_EDITOR #include "VoxelStrateComposer.h" #include "VoxelStrateMeasure.h" @@ -3580,7 +3583,7 @@ void AVoxelWorld::Tick(float DeltaTime) Min.X, Min.Y, Min.Z, Max.X, Max.Y, Max.Z, *FString::SanitizeFloat(TileExtentMetres), *FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ), - Settings && Level > Settings->MaxClipLevel ? 1 : 0)); + Settings && Level > VF_MaxMarchingCubesLevel(Settings) ? 1 : 0)); } VoxelForgeStartupTrace::Finish(TEXT("desired_set_satisfied_and_queue_drained")); } @@ -5031,30 +5034,28 @@ static FORCEINLINE FIntVector VF_FloorDiv(const FIntVector& V, int32 D) return FIntVector(VF_FloorDiv(V.X, D), VF_FloorDiv(V.Y, D), VF_FloorDiv(V.Z, D)); } -// RENDER DISTANCE — rayon (en tuiles niveau-MaxLevel) de la coquille EXTERNE : ClipRadius, élargi -// si `RenderDistanceChunks` demande une portée horizontale au-delà du naturel R·2^MaxLevel. Partagé -// par BuildDesiredTiles (le desired set) et IsTileInClipRange (le même horizon pour le cull). -static FORCEINLINE int32 VF_OuterShellRadius(const UVoxelSettings* Settings, int32 R, int32 MaxLevel) -{ - const int32 Dist = Settings ? Settings->RenderDistanceChunks : 0; - if (Dist <= 0) return R; - return FMath::Max(R, (Dist + (1 << MaxLevel) - 1) >> MaxLevel); // ceil(Dist / 2^MaxLevel) -} - -// F18 — la coquille LA PLUS EXTERNE : niveau + rayon. Sans anneau feuille = (MaxLevel, rayon -// render-distance). Avec (`bFarSheetRing` et distance > portée naturelle) = l'anneau FEUILLE : -// niveau MaxLevel + FarSheetSpanLevels (une feuille couvre 2^span empreintes MC par axe), rayon -// re-dérivé à ce niveau. Partagé par BuildDesiredTiles et IsTileInClipRange (même horizon). +// F18 — the outer shell is either the MC render-distance level or the larger sheet ring. When +// the sheet span skips levels, VF_MaxMarchingCubesLevel fills them with MC bridge shells. +// Shared by desired selection and range culling so both use the same outer horizon. static FORCEINLINE void VF_OuterShell(const UVoxelSettings* Settings, int32 R, int32 MaxLevel, int32& OutLevel, int32& OutRadius) { - OutLevel = MaxLevel; - OutRadius = VF_OuterShellRadius(Settings, R, MaxLevel); - if (Settings && Settings->bFarSheetRing && OutRadius > R) - { - OutLevel = MaxLevel + FMath::Clamp(Settings->FarSheetSpanLevels, 1, 4); - OutRadius = FMath::Max(1, (Settings->RenderDistanceChunks + (1 << OutLevel) - 1) >> OutLevel); - } + VoxelClipmapDesiredTiles::FParameters Parameters; + Parameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0; + Parameters.bFarSheetRing = Settings && Settings->bFarSheetRing; + Parameters.FarSheetSpanLevels = Settings ? Settings->FarSheetSpanLevels : 2; + VoxelClipmapDesiredTiles::OuterShell(Parameters, R, MaxLevel, OutLevel, OutRadius); +} + +// When an expanded sheet ring skips LOD levels, keep an MC bridge at every intervening level. +static int32 VF_MaxMarchingCubesLevel(const UVoxelSettings* Settings) +{ + const int32 Radius = Settings ? FMath::Max(1, Settings->ClipRadius) : 3; + const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4; + int32 OuterLevel = MaxLevel, OuterRadius = Radius; + VF_OuterShell(Settings, Radius, MaxLevel, OuterLevel, OuterRadius); + (void)OuterRadius; + return OuterLevel > MaxLevel ? OuterLevel - 1 : MaxLevel; } void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition, @@ -5072,128 +5073,37 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3; const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4; - // RENDER DISTANCE (`RenderDistanceChunks`) : la coquille EXTERNE continue au-delà du rayon - // naturel jusqu'à couvrir la distance demandée — en tuiles MC niveau-MaxClipLevel, ou (F18, - // `bFarSheetRing`) en tuiles FEUILLE plus grandes (niveau MaxLevel+span, deux heightfields - // sol/cap au lieu de marching cubes — cf. GenerateSheetMesh). IsTileInClipRange partage - // VF_OuterShell pour que le cull voie le même horizon. - const int32 ROuter = VF_OuterShellRadius(Settings, R, MaxLevel); - int32 SheetLevel = MaxLevel, RSheet = ROuter; - VF_OuterShell(Settings, R, MaxLevel, SheetLevel, RSheet); - const bool bSheetRing = SheetLevel > MaxLevel; + // Render-distance tiles expand horizontally. The sheet ring retains its configured span; + // intermediate levels are emitted as marching-cubes bridge shells below it. + // IsTileInClipRange shares the outer-shell calculation so culling sees this same horizon. + int32 StrateTopZ = 0, StrateBottomZ = 0; + bool bHasStrate = false; + if (StrateManager) + { + bHasStrate = StrateManager->GetStrateChunkZBounds(Center.Z, StrateTopZ, StrateBottomZ); + } - // Strate-aware VERTICAL band (in level-0 chunk-Z). Without this the clipmap generates the - // occluded volume above/below (sealed strates are light-tight, §8.7) AND the full underground - // depth — a huge column of invisible solid rock = the gen lag. So clamp the vertical reach to - // the player's strate ± margin (open-ceiling strates extend UP to the sky-cap). The clipmap - // stays full-reach HORIZONTALLY (the horizon) but limited vertically. ZLo/ZHi span ⇒ no clamp. + // Keep the clamp enabled, but cover the player's complete current strate plus its margin. + // Only when no strate bounds exist (the inter-strate gap) does the small player window apply. int32 ZLo = MIN_int32, ZHi = MAX_int32; - if (Settings && Settings->bClampViewToStrate && StrateManager) + VoxelClipmapDesiredTiles::ResolveVerticalBounds( + Center.Z, + Settings ? Settings->ViewDistanceUp : 5, + Settings ? Settings->ViewDistanceDown : 5, + Settings ? Settings->StrateViewMarginChunks : 3, + Settings && Settings->bClampViewToStrate, + bHasStrate, StrateTopZ, StrateBottomZ, ZLo, ZHi); + + VoxelClipmapDesiredTiles::FParameters TileParameters; + TileParameters.ClipRadius = R; + TileParameters.MaxClipLevel = MaxLevel; + TileParameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0; + TileParameters.bFarSheetRing = Settings && Settings->bFarSheetRing; + TileParameters.FarSheetSpanLevels = Settings ? Settings->FarSheetSpanLevels : 2; + VoxelClipmapDesiredTiles::Build(Center, ZLo, ZHi, TileParameters, DesiredSorted); + for (const FVoxelTileKey& Key : DesiredSorted) { - const int32 Margin = Settings->StrateViewMarginChunks; - ZLo = Center.Z - Settings->ViewDistanceDown; - ZHi = Center.Z + Settings->ViewDistanceUp; - int32 StrTopZ = 0, StrBotZ = 0; - if (StrateManager->GetStrateChunkZBounds(Center.Z, StrTopZ, StrBotZ)) - { - const ECaveGeneratorType GenType = StrateManager->GetGeneratorTypeForChunk(Center); - const bool bOpen = (GenType == ECaveGeneratorType::SurfaceWorld - || GenType == ECaveGeneratorType::FloatingIslands); - ZLo = FMath::Max(ZLo, StrBotZ - Margin); - ZHi = bOpen ? (StrTopZ + Margin) : FMath::Min(ZHi, StrTopZ + Margin); - } - // else: in the bedrock gap → keep the player window (see both sides while descending). - } - - // Concentric shells: level 0 near the player, each coarser level a 2× larger shell beyond. - // A level-L tile is dropped if it's fully covered by the finer (L-1) level's box — that's - // the inner hole, so the shells tile space without big gaps. - for (int32 L = 0; L <= MaxLevel; ++L) - { - const int32 Pow = 1 << L; // level-L tile = 2^L chunks - const FIntVector CL = VF_FloorDiv(Center, Pow); // player's level-L tile coord - const FIntVector CF = (L > 0) ? VF_FloorDiv(Center, Pow >> 1) : FIntVector::ZeroValue; - - // Rayon de CE niveau : R partout, sauf la coquille externe (render distance) — qui, si - // l'anneau FEUILLE est actif (F18), est émise à part plus bas (le niveau MaxLevel reste - // alors à R). Le test "covered by finer" garde R (le niveau plus fin n'est jamais étendu). - const int32 RL = (L == MaxLevel && !bSheetRing) ? ROuter : R; - - // Anneau étendu : le balayage naïf serait (2·RL+1)³ — on restreint dz à la fenêtre de la - // clamp verticale AVANT la boucle (mêmes tuiles retenues : le `continue` Z ci-dessous - // rejetterait tout le reste). Sentinelles MIN/MAX (pas de clamp) ⇒ balayage plein. - int32 DzMin = -RL, DzMax = RL; - if (RL > R && ZLo != MIN_int32) - { - DzMin = FMath::Max(DzMin, VF_FloorDiv(ZLo, Pow) - CL.Z); - DzMax = FMath::Min(DzMax, VF_FloorDiv(ZHi, Pow) - CL.Z); - } - - for (int32 dz = DzMin; dz <= DzMax; ++dz) - for (int32 dy = -RL; dy <= RL; ++dy) - for (int32 dx = -RL; dx <= RL; ++dx) - { - const FIntVector T = CL + FIntVector(dx, dy, dz); - - // Strate-aware vertical clamp: drop tiles whose level-0 chunk-Z footprint doesn't - // overlap [ZLo, ZHi] (the occluded strate above/below / deep underground). - const int32 TZLo = T.Z << L; - const int32 TZHi = ((T.Z + 1) << L) - 1; - if (TZHi < ZLo || TZLo > ZHi) continue; - - if (L > 0) - { - // Covered by the finer level iff the level-(L-1) tiles 2T..2T+1 (per axis) - // all lie inside the finer box [CF-R, CF+R]. - const bool bCovered = - (2 * T.X >= CF.X - R) && (2 * T.X + 1 <= CF.X + R) && - (2 * T.Y >= CF.Y - R) && (2 * T.Y + 1 <= CF.Y + R) && - (2 * T.Z >= CF.Z - R) && (2 * T.Z + 1 <= CF.Z + R); - if (bCovered) continue; - } - const FVoxelTileKey Key(T, L); - DesiredSorted.Add(Key); - DesiredStamped.FindOrAdd(Key) = DesiredStamp; - } - } - - // F18 — ANNEAU FEUILLE : la portée render-distance est couverte par des tuiles feuille - // (niveau SheetLevel > MaxLevel, LoadTile route niveau > MaxClipLevel vers GenerateSheetMesh). - // Trou intérieur = la boîte MC niveau-MaxLevel (rayon R), pas le niveau SheetLevel−1. - if (bSheetRing) - { - const int32 SPow = 1 << SheetLevel; - const FIntVector CS = VF_FloorDiv(Center, SPow); - const FIntVector CM = VF_FloorDiv(Center, 1 << MaxLevel); - const int32 K = SheetLevel - MaxLevel; // 1 feuille = 2^K tuiles MC par axe - - int32 DzMin = -RSheet, DzMax = RSheet; - if (ZLo != MIN_int32) - { - DzMin = FMath::Max(DzMin, VF_FloorDiv(ZLo, SPow) - CS.Z); - DzMax = FMath::Min(DzMax, VF_FloorDiv(ZHi, SPow) - CS.Z); - } - - for (int32 dz = DzMin; dz <= DzMax; ++dz) - for (int32 dy = -RSheet; dy <= RSheet; ++dy) - for (int32 dx = -RSheet; dx <= RSheet; ++dx) - { - const FIntVector T = CS + FIntVector(dx, dy, dz); - const int32 TZLo = T.Z << SheetLevel; - const int32 TZHi = ((T.Z + 1) << SheetLevel) - 1; - if (TZHi < ZLo || TZLo > ZHi) continue; - - // Couverte par la boîte MC (empreinte entièrement dans [CM−R, CM+R] au niveau MaxLevel). - const bool bCovered = - ((T.X << K) >= CM.X - R) && ((((T.X + 1) << K) - 1) <= CM.X + R) && - ((T.Y << K) >= CM.Y - R) && ((((T.Y + 1) << K) - 1) <= CM.Y + R) && - ((T.Z << K) >= CM.Z - R) && ((((T.Z + 1) << K) - 1) <= CM.Z + R); - if (bCovered) continue; - - const FVoxelTileKey Key(T, SheetLevel); - DesiredSorted.Add(Key); - DesiredStamped.FindOrAdd(Key) = DesiredStamp; - } + DesiredStamped.FindOrAdd(Key) = DesiredStamp; } // Streaming anchors (AI / remote players, §9.3): fold each one's small level-0 box into the SAME @@ -5604,10 +5514,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw "\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s],\"sheet\":%d"), Summary.Level, Summary.Count, Summary.Min.X, Summary.Min.Y, Summary.Min.Z, - Summary.Max.X, Summary.Max.Y, Summary.Max.Z, - *FString::SanitizeFloat(TileExtentMetres), - *FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ), - Settings && Summary.Level > Settings->MaxClipLevel ? 1 : 0)); + Summary.Max.X, Summary.Max.Y, Summary.Max.Z, + *FString::SanitizeFloat(TileExtentMetres), + *FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ), + Settings && Summary.Level > VF_MaxMarchingCubesLevel(Settings) ? 1 : 0)); } bStartupTraceDesiredRecorded = true; } @@ -5628,7 +5538,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw // dès que les feuilles sont actives, même si la coupe de contenu MC est désactivée. const bool bSheetsWantBand = Settings && Settings->bFarSheetRing && Settings->RenderDistanceChunks > 0; - const int32 TopMC = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4; + const int32 TopMC = VF_MaxMarchingCubesLevel(Settings); int32 NewLo = MIN_int32, NewHi = MAX_int32; if ((CutMin <= 8 || bSheetsWantBand) && StrateManager) { @@ -5649,7 +5559,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw NewLo, NewHi, MeshBandChunkLo, MeshBandChunkHi, CutMin, CenterChunk.Z); for (const FVoxelTileKey& T : LoadedTiles) { - // Feuilles (niveau > MaxClipLevel) : toujours dépendantes de la bande. + // Sheets (above the last MC bridge level) always depend on the strate band. if (T.Level < CutMin && T.Level <= TopMC) continue; const int32 CLo = T.Coord.Z << T.Level; const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1; @@ -5667,17 +5577,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw } } - // F18 — TROU XY de l'anneau feuille (voir VoxelWorld.h) : boîte MC niveau-MaxClipLevel - // autour du joueur, rétrécie d'UNE tuile — le raccord feuille↔anneau MC garde une tuile - // MC pleine de recouvrement (même pas d'échantillonnage des deux côtés → discret), et en - // avançant, les tuiles MC de la zone nouvellement découpée étaient déjà desired au - // crossing précédent (chargées avant que le trou ne les découvre). Changement (crossing - // de tuile MaxClipLevel, ~tous les 2^L chunks) ⇒ re-queue des feuilles chevauchant - // l'ancien OU le nouveau trou. + // The sheet XY hole follows the outermost MC bridge level and shrinks by one tile, leaving + // an overlap at the mesh transition. Requeue sheets overlapping either the old or new hole + // when the player crosses a bridge-level tile. { int32 NewMinX = MAX_int32, NewMinY = MAX_int32; int32 NewMaxX = MIN_int32, NewMaxY = MIN_int32; - const int32 TopMCLvl = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4; + const int32 TopMCLvl = VF_MaxMarchingCubesLevel(Settings); if (Settings && Settings->bFarSheetRing && Settings->RenderDistanceChunks > 0) { const int32 RClip = FMath::Max(1, Settings->ClipRadius); @@ -5926,11 +5832,10 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority) const int32 FullRes = Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2; const int32 Extent = CHUNK_SIZE << Tile.Level; - // F18 — tuile FEUILLE : BuildDesiredTiles n'émet des clés au-delà de MaxClipLevel que pour - // l'anneau feuille (render distance) — elles se maillent en deux heightfields (GenerateSheetMesh), - // pas en marching cubes. Densité d'échantillonnage = celle de l'anneau MC niveau-MaxClipLevel - // (le nombre de cellules grandit avec la feuille, plafonné à 128/axe — au-delà le pas grossit). - const int32 TopMC = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4; + // F18 — sheet-ring keys follow the last MC bridge level and mesh as two heightfields + // (GenerateSheetMesh). Their sampling step matches that MC level; cells grow with sheet extent + // up to 128 per axis, after which the step increases. + const int32 TopMC = VF_MaxMarchingCubesLevel(Settings); const bool bSheetTile = Tile.Level > TopMC; int32 Cells = (Tile.Level < FullRes) diff --git a/Tools/VoxelForgeTest.ps1 b/Tools/VoxelForgeTest.ps1 index 7bb952e..61e6324 100644 --- a/Tools/VoxelForgeTest.ps1 +++ b/Tools/VoxelForgeTest.ps1 @@ -43,9 +43,17 @@ $BuildRecord = [ordered]@{ requested = [bool]$Build; status = 'not_requested' } $AssetEvidence = @() $CommandletMetrics = [ordered]@{} $GameMetrics = [ordered]@{} -$SurfaceFallMetrics = [ordered]@{ requested = $Scenario -in @('tests', 'surface-fall'); status = 'not_run' } +$SurfaceFallMetrics = [ordered]@{ + requested = $Scenario -eq 'surface-fall' -or + ($Scenario -eq 'tests' -and [string]::IsNullOrWhiteSpace($TestFilter)) + status = 'not_run' +} $StrateCrossingMetrics = [ordered]@{ requested = $Scenario -eq 'crossing'; status = 'not_run' } -$CollisionGateStressMetrics = [ordered]@{ requested = $Scenario -in @('tests', 'gate-stress'); status = 'not_run' } +$CollisionGateStressMetrics = [ordered]@{ + requested = $Scenario -eq 'gate-stress' -or + ($Scenario -eq 'tests' -and [string]::IsNullOrWhiteSpace($TestFilter)) + status = 'not_run' +} $TestMetrics = @() $ParityEvidence = [ordered]@{ requested = $Scenario -eq 'parity'; status = 'not_run' } $DllEvidence = [ordered]@{ @@ -1038,9 +1046,9 @@ try { Add-Failure "automation tests failed: $failedNames" } - # The runtime collision-gate repro is part of the tests scenario as an integration - # test alongside the 44 pure automation tests. It uses the same isolated host/assets - # and gets its own launch ledger entry and log. + if ([string]::IsNullOrWhiteSpace($TestFilter)) { + # The unfiltered suite includes its collision integration repros. A filtered run is + # bounded to its named automation tests and does not launch unrelated scenarios. $surfaceBaseCvars = [ordered]@{ 'voxel.TestSurfaceFall' = 1 'voxel.TestSurfaceFallSpawnHeightVoxels' = 20 @@ -1071,6 +1079,7 @@ try { $stressReason = [string](Get-JsonValue $CollisionGateStressMetrics 'reason' '') Add-Failure "collision-gate stress repro did not pass: $($CollisionGateStressMetrics.status) $stressReason" } + } } 'parity' { $parityArgs = New-CommonArguments 'commandlet' (Join-Path $RunRoot 'Logs\parity-commandlet.log') (Merge-Cvars $baseCvars) diff --git a/Tools/__pycache__/AnalyzeVoxelForgePerfProbes.cpython-312.pyc b/Tools/__pycache__/AnalyzeVoxelForgePerfProbes.cpython-312.pyc new file mode 100644 index 0000000..b967c3c Binary files /dev/null and b/Tools/__pycache__/AnalyzeVoxelForgePerfProbes.cpython-312.pyc differ