Files
VoxelForge/Source/VoxelForge/Private/VoxelContentManager.cpp
T
Fr0zkaandClaude Opus 5.5 135957831b fix: sanitizer sweep across all generators; bound signed shifts/division/overflow at coordinate limits (L61); R31
New verify.py sanitize tier (Clang address,undefined,float-cast-overflow; GCC address,undefined;
-fno-sanitize-recover) runs 19 cases per compiler at LOD0-8 across every generator, passages and the
crossing: 0 runtime reports; a planted signed-overflow control is caught by both. A static scan fixed
INT_MIN negation in floor division, unbounded signed LOD shifts and coordinate-times-extent overflow in
VoxelClipmapDesiredTiles.h and VoxelTypes.h (plugin and standalone), and VoxelWorld.cpp, VoxelContentManager.cpp
and VoxelDensityVolume.cpp (plugin only, uncompiled here). Supported-input output unchanged (version 211).
fast/medium/full PASS. Research R31 (self-checked): far-LOD 4-8 cut plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-26 05:19:18 +01:00

2733 lines
132 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// VoxelContentManager.cpp
// Distance-based world-grid decoration scatter with ASYNC surface marching (no LOD pop, no frame cost)
// + aesthetic water surfaces.
#include "VoxelContentManager.h"
#include "VoxelStrateManager.h"
#include "VoxelStrateDefinition.h"
#include "VoxelBiomeDefinition.h"
#include "VoxelGenerator.h"
#include "VoxelSettings.h"
#include "VoxelWorld.h"
#include "VoxelCaveMorphology.h" // VoxelHash
#include "Components/StaticMeshComponent.h"
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
#include "Engine/StaticMesh.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
#include "Materials/MaterialInterface.h"
#include "Tasks/Task.h"
#include "HAL/PlatformProcess.h"
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes
#include <VoxelForgeCore/VoxelForgeCore.h>
#include <cmath>
#include <utility>
#include <string>
struct FCoreSurfaceSnapshot
{
double CoreVoxelSizeCm = 2.0 * VOXEL_SIZE;
VoxelForgeCore::WorldAssemblyRecipeV1 Recipe;
TMap<int32, VoxelForgeCore::WorldAssemblyStratumPlanV1> Plans;
const VoxelForgeCore::WorldAssemblyStratumPlanV1* FindPlan(int32 StrateIndex) const
{
return Plans.Find(StrateIndex);
}
bool GetHeights(int32 StrateIndex, float CoreVoxelX, float CoreVoxelY,
float& OutGroundCoreVoxels, float& OutCapCoreVoxels) const
{
const auto* Plan = FindPlan(StrateIndex);
if (!Plan || !VoxelMath::IsFinite(CoreVoxelX) || !VoxelMath::IsFinite(CoreVoxelY))
{
return false;
}
constexpr double SubvoxelsPerCoreVoxel = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const double MaxInput = static_cast<double>(MAX_int64) / SubvoxelsPerCoreVoxel;
if (FMath::Abs(static_cast<double>(CoreVoxelX)) > MaxInput
|| FMath::Abs(static_cast<double>(CoreVoxelY)) > MaxInput)
{
return false;
}
const VoxelForgeCore::Heightfield2D Heightfield(Recipe, *Plan);
if (!Heightfield.isValid())
{
return false;
}
const int64 XSubvoxels = static_cast<int64>(std::llround(
static_cast<double>(CoreVoxelX) * SubvoxelsPerCoreVoxel));
const int64 YSubvoxels = static_cast<int64>(std::llround(
static_cast<double>(CoreVoxelY) * SubvoxelsPerCoreVoxel));
const auto Sample = Heightfield.point(
XSubvoxels, YSubvoxels, 0);
int64 SurfaceHeightSubvoxels = Sample.groundHeightSubvoxels;
int64 SearchTopSubvoxels = Sample.groundHeightSubvoxels;
for (const auto& Feature : Plan->features)
{
if ((Feature.type != 2 && Feature.type != 4)
|| XSubvoxels < Feature.minimumSubvoxels[0] - 4 * 256
|| XSubvoxels > Feature.maximumSubvoxels[0] + 4 * 256
|| YSubvoxels < Feature.minimumSubvoxels[1] - 4 * 256
|| YSubvoxels > Feature.maximumSubvoxels[1] + 4 * 256)
{
continue;
}
SearchTopSubvoxels = FMath::Max(
SearchTopSubvoxels, Feature.maximumSubvoxels[2]);
}
SearchTopSubvoxels = FMath::Min(
Sample.capHeightSubvoxels, SearchTopSubvoxels + 256);
if (SearchTopSubvoxels > Sample.groundHeightSubvoxels)
{
// Quarter Core voxel, bounded to relevant features. / Quart de voxel Core, borne aux volumes pertinents.
constexpr int64 ScanStepSubvoxels = 64;
int64 PreviousZ = SearchTopSubvoxels;
int64 PreviousDensity = Heightfield.point(XSubvoxels, YSubvoxels, PreviousZ).densitySubvoxels;
for (int64 Z = SearchTopSubvoxels - ScanStepSubvoxels;
Z >= Sample.groundHeightSubvoxels; Z -= ScanStepSubvoxels)
{
const int64 Density = Heightfield.point(XSubvoxels, YSubvoxels, Z).densitySubvoxels;
if (PreviousDensity >= 0 && Density < 0)
{
const double Denominator = static_cast<double>(PreviousDensity - Density);
const double Alpha = Denominator > 0.0
? static_cast<double>(PreviousDensity) / Denominator : 0.5;
SurfaceHeightSubvoxels = static_cast<int64>(std::llround(
static_cast<double>(PreviousZ) + (static_cast<double>(Z - PreviousZ) * Alpha)));
break;
}
PreviousZ = Z;
PreviousDensity = Density;
}
}
OutGroundCoreVoxels = static_cast<float>(SurfaceHeightSubvoxels / SubvoxelsPerCoreVoxel);
OutCapCoreVoxels = static_cast<float>(Sample.capHeightSubvoxels / SubvoxelsPerCoreVoxel);
return VoxelMath::IsFinite(OutGroundCoreVoxels) && VoxelMath::IsFinite(OutCapCoreVoxels);
}
bool GetWater(int32 StrateIndex, bool& OutHasPlan, bool& OutEnabled, float& OutLocalCm) const
{
OutHasPlan = false;
OutEnabled = false;
OutLocalCm = -FLT_MAX;
const auto* Plan = FindPlan(StrateIndex);
if (!Plan)
{
return false;
}
OutHasPlan = true;
OutEnabled = Plan->waterEnabled;
if (OutEnabled)
{
OutLocalCm = static_cast<float>(Plan->waterSurfaceSubvoxels
* CoreVoxelSizeCm / VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
}
return true;
}
};
struct FCoreFloatingIslandFieldSnapshot
{
VoxelForgeCore::FloatingIslandRecipeV1 Recipe;
VoxelForgeCore::FloatingIslandPlanV1 Plan;
TUniquePtr<VoxelForgeCore::FloatingIslandFieldV1> Field;
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> SolidSupportBounds;
};
struct FCoreFloatingIslandSnapshot
{
double CoreVoxelSizeCm = 2.0 * VOXEL_SIZE;
TMap<int32, TSharedPtr<const FCoreFloatingIslandFieldSnapshot, ESPMode::ThreadSafe>> Fields;
const FCoreFloatingIslandFieldSnapshot* FindField(int32 StrateIndex) const
{
const auto* Found = Fields.Find(StrateIndex);
return Found != nullptr && Found->IsValid() ? Found->Get() : nullptr;
}
bool GetDensity(int32 StrateIndex, float CoreVoxelX, float CoreVoxelY,
float CoreVoxelZ, float& OutDensity) const
{
OutDensity = 1.0f;
const FCoreFloatingIslandFieldSnapshot* Entry = FindField(StrateIndex);
if (Entry == nullptr || !Entry->Field.IsValid()
|| !FMath::IsFinite(CoreVoxelX) || !FMath::IsFinite(CoreVoxelY)
|| !FMath::IsFinite(CoreVoxelZ))
{
return false;
}
constexpr double SubvoxelsPerCoreVoxel = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const double MaximumInput = static_cast<double>(MAX_int64) / SubvoxelsPerCoreVoxel;
if (FMath::Abs(static_cast<double>(CoreVoxelX)) > MaximumInput
|| FMath::Abs(static_cast<double>(CoreVoxelY)) > MaximumInput
|| FMath::Abs(static_cast<double>(CoreVoxelZ)) > MaximumInput)
{
return false;
}
const int64 Density = Entry->Field->densitySubvoxels(
static_cast<int64>(std::llround(static_cast<double>(CoreVoxelX) * SubvoxelsPerCoreVoxel)),
static_cast<int64>(std::llround(static_cast<double>(CoreVoxelY) * SubvoxelsPerCoreVoxel)),
static_cast<int64>(std::llround(static_cast<double>(CoreVoxelZ) * SubvoxelsPerCoreVoxel)));
OutDensity = static_cast<float>(static_cast<double>(Density) / SubvoxelsPerCoreVoxel);
return FMath::IsFinite(OutDensity);
}
bool GetTopAnchor(int32 StrateIndex, const FVector2D& LocalXYCm,
float& OutGroundLocalZCm) const
{
OutGroundLocalZCm = 0.0f;
const FCoreFloatingIslandFieldSnapshot* Entry = FindField(StrateIndex);
if (Entry == nullptr || !Entry->Field.IsValid()
|| Entry->Plan.islands.empty()
|| !FMath::IsFinite(LocalXYCm.X) || !FMath::IsFinite(LocalXYCm.Y))
{
return false;
}
constexpr double SubvoxelsPerCoreVoxel = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const double CentimetersPerCoreVoxel = CoreVoxelSizeCm;
const double XSubvoxels = static_cast<double>(LocalXYCm.X)
* SubvoxelsPerCoreVoxel / CentimetersPerCoreVoxel;
const double YSubvoxels = static_cast<double>(LocalXYCm.Y)
* SubvoxelsPerCoreVoxel / CentimetersPerCoreVoxel;
constexpr double MaximumInput = static_cast<double>(MAX_int64) - 1024.0;
if (FMath::Abs(XSubvoxels) > MaximumInput || FMath::Abs(YSubvoxels) > MaximumInput)
{
return false;
}
const int64 X = static_cast<int64>(std::llround(XSubvoxels));
const int64 Y = static_cast<int64>(std::llround(YSubvoxels));
int64 SearchTop = Entry->Plan.islands.front().topZSubvoxels;
int64 SearchBottom = Entry->Plan.islands.front().bottomZSubvoxels;
for (const auto& Island : Entry->Plan.islands)
{
SearchTop = FMath::Max(SearchTop, Island.topZSubvoxels);
SearchBottom = FMath::Min(SearchBottom, Island.bottomZSubvoxels);
}
SearchTop += 4 * 256;
SearchBottom -= 4 * 256;
constexpr int64 ScanStepSubvoxels = 128;
int64 PreviousZ = SearchTop;
int64 PreviousDensity = Entry->Field->densitySubvoxels(X, Y, PreviousZ);
if (PreviousDensity < 0) return false;
for (int64 Z = SearchTop - ScanStepSubvoxels; Z >= SearchBottom; Z -= ScanStepSubvoxels)
{
const int64 Density = Entry->Field->densitySubvoxels(X, Y, Z);
if (PreviousDensity >= 0 && Density < 0)
{
const double Denominator = static_cast<double>(PreviousDensity - Density);
const double Alpha = Denominator > 0.0
? static_cast<double>(PreviousDensity) / Denominator : 0.5;
const double SurfaceSubvoxels = static_cast<double>(PreviousZ)
+ static_cast<double>(Z - PreviousZ) * Alpha;
OutGroundLocalZCm = static_cast<float>(SurfaceSubvoxels
* CentimetersPerCoreVoxel / SubvoxelsPerCoreVoxel);
return FMath::IsFinite(OutGroundLocalZCm);
}
PreviousZ = Z;
PreviousDensity = Density;
}
return false;
}
};
// Shared consumer preparation; snapshots own all pointers for the entire worker lifetime.
// Preparation partagee : les snapshots gardent les pointeurs vivants durant toute la tache.
static std::vector<VoxelForgeCore::WorldAssemblyFeatureQueryV1> VF_ContentFeatureQueries(
const FCoreSurfaceSnapshot* Surface, const FCoreFloatingIslandSnapshot* Islands,
int32 StrateIndex, float MinX, float MinY, float MinZ, float MaxX, float MaxY, float MaxZ)
{
using namespace VoxelForgeCore;
constexpr double Q = WorldAssemblySubvoxelsPerVoxel;
const std::array<int64,3> Min{static_cast<int64>(std::floor(MinX*Q)),
static_cast<int64>(std::floor(MinY*Q)),static_cast<int64>(std::floor(MinZ*Q))};
const std::array<int64,3> Max{static_cast<int64>(std::ceil(MaxX*Q)),
static_cast<int64>(std::ceil(MaxY*Q)),static_cast<int64>(std::ceil(MaxZ*Q))};
std::vector<WorldAssemblyFeatureQueryV1> Queries;
if (Surface)
if (const auto* Plan = Surface->FindPlan(StrateIndex))
{
const Heightfield2D Field(Surface->Recipe,*Plan);
Queries.push_back({Field.recipe(),Field.plan(),Field.buildFeatureTileList(Min,Max)});
}
if (Islands)
if (const auto* Entry = Islands->FindField(StrateIndex))
Queries.push_back({&Entry->Recipe,&Entry->Plan,Entry->Field->buildFeatureTileList(Min,Max)});
return Queries;
}
// Preserve the original march lattice and crossing refinement, skipping only certified air.
// Garder la phase du pas et le raffinement d'origine; sauter uniquement l'air prouve.
static bool VF_ConstrainContentMarch(std::span<const VoxelForgeCore::WorldAssemblyFeatureBoundsV1> Bounds,
float X, float Y, float Step, float& Top, float& Bottom)
{
constexpr double Q = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const auto Range = VoxelForgeCore::BuildWorldAssemblyFeatureColumnRangeV1(Bounds,
static_cast<int64>(std::llround(X*Q)),static_cast<int64>(std::llround(Y*Q)));
if (!Range.known) return true;
if (Range.empty) return false;
const float SupportTop = static_cast<float>(Range.maximumZSubvoxels/Q);
const float SupportBottom = static_cast<float>(Range.minimumZSubvoxels/Q);
const float SkipSteps = FMath::Max(0.0f,FMath::FloorToFloat((Top-SupportTop)/Step)-1.0f);
Top -= SkipSteps*Step;
Bottom = FMath::Max(Bottom,SupportBottom-Step);
return Top >= Bottom;
}
// Global safety cap on spawned decoration ACTORS per cell (across all entries).
// HISM instances are exempt — they're batched render data, capped per entry by MaxPerChunk.
static constexpr int32 GMaxDecorationActorsPerCell = 400;
// Hard cap on total companion satellites (level 1 + level 2) spawned per placed PARENT — the safety net that
// makes 2-level nesting impossible to blow up regardless of authored counts.
static constexpr int32 GMaxCompanionsPerParent = 256;
// One cell = one chunk XY footprint (so DecorationRadiusChunks reads as a radius in chunks, and a
// decoration's per-cell MaxPerChunk keeps its "per chunk" meaning).
static constexpr int32 DECO_CELL_VOXELS = CHUNK_SIZE;
static FORCEINLINE int32 CellChebyshev(const FIntPoint& A, const FIntPoint& B)
{
return FMath::Max(FMath::Abs(A.X - B.X), FMath::Abs(A.Y - B.Y));
}
static bool VF_DecoTransformLess(const FTransform& A, const FTransform& B)
{
// A transform is the complete deterministic identity of a placement. Comparing the matrix
// avoids the q/-q representation ambiguity of raw quaternions and gives the final region order
// the same result regardless of which worker cell completed first.
const FMatrix AM = A.ToMatrixWithScale();
const FMatrix BM = B.ToMatrixWithScale();
for (int32 Row = 0; Row < 4; ++Row)
{
for (int32 Col = 0; Col < 4; ++Col)
{
if (AM.M[Row][Col] != BM.M[Row][Col])
{
return AM.M[Row][Col] < BM.M[Row][Col];
}
}
}
return false;
}
void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager,
UVoxelGenerator* InGenerator, UVoxelSettings* InSettings, int32 InSeed)
{
Owner = InOwner;
StrateManager = InStrateManager;
Generator = InGenerator;
Settings = InSettings;
Seed = InSeed;
NativeSurfaceSnapshot.Reset();
NativeFloatingIslandSnapshot.Reset();
if (Settings)
{
FString ManifestJson, ManifestHash, ManifestError;
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
const bool bHasRuntimeAssembly = VoxelWorld != nullptr
&& VoxelWorld->GetVoxelForgeCoreWorldAssembly(ManifestJson, ManifestHash);
if (!bHasRuntimeAssembly)
{
Settings->GetWorldAssemblyManifestJson(ManifestJson, ManifestHash, ManifestError);
}
if (!ManifestJson.IsEmpty())
{
FTCHARToUTF8 Utf8Json(*ManifestJson);
std::string CoreError;
const std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest =
VoxelForgeCore::WorldAssemblyManifest::FromJson(
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
if (Manifest)
{
TSharedPtr<FCoreSurfaceSnapshot, ESPMode::ThreadSafe> Snapshot =
MakeShared<FCoreSurfaceSnapshot, ESPMode::ThreadSafe>();
Snapshot->CoreVoxelSizeCm = VoxelWorld != nullptr
? VoxelWorld->GetActiveVoxelSizeCm() : 2.0 * VOXEL_SIZE;
Snapshot->Recipe = Manifest->recipe;
for (const auto& Plan : Manifest->plan.strates)
{
Snapshot->Plans.Add(Plan.strateIndex, Plan);
}
NativeSurfaceSnapshot = Snapshot;
if (!Manifest->floatingIslands.empty())
{
TSharedPtr<FCoreFloatingIslandSnapshot, ESPMode::ThreadSafe> IslandSnapshot =
MakeShared<FCoreFloatingIslandSnapshot, ESPMode::ThreadSafe>();
IslandSnapshot->CoreVoxelSizeCm = VoxelWorld != nullptr
? VoxelWorld->GetActiveVoxelSizeCm() : 2.0 * VOXEL_SIZE;
for (const auto& IslandPlan : Manifest->floatingIslands)
{
TSharedPtr<FCoreFloatingIslandFieldSnapshot, ESPMode::ThreadSafe> Entry =
MakeShared<FCoreFloatingIslandFieldSnapshot, ESPMode::ThreadSafe>();
Entry->Recipe = IslandPlan.manifest.recipe;
Entry->Plan = IslandPlan.manifest.plan;
Entry->Field = MakeUnique<VoxelForgeCore::FloatingIslandFieldV1>(
Entry->Recipe, Entry->Plan);
if (!Entry->Field->isValid())
{
UE_LOG(LogTemp, Warning,
TEXT("[VoxelContentManager] Core floating-island field rejected strate %d."),
IslandPlan.strateIndex);
continue;
}
Entry->SolidSupportBounds = Entry->Field->solidSupportBounds();
IslandSnapshot->Fields.Add(IslandPlan.strateIndex, Entry);
}
if (!IslandSnapshot->Fields.IsEmpty())
{
NativeFloatingIslandSnapshot = IslandSnapshot;
}
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[VoxelContentManager] native surface snapshot rejected: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
}
}
}
if (!PlaneMesh)
{
PlaneMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Plane.Plane"));
}
}
bool UVoxelContentManager::QueryNativeSurfaceAnchors(
int32 StrateIndex, const FVector2D& LocalXYCm, float& OutGroundLocalZCm,
bool& bOutHasWater, float& OutWaterLocalZCm) const
{
OutGroundLocalZCm = 0.0f;
bOutHasWater = false;
OutWaterLocalZCm = -FLT_MAX;
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
if (VoxelWorld == nullptr || !VoxelWorld->bUseVoxelForgeCore
|| !NativeSurfaceSnapshot.IsValid())
{
return false;
}
const float VoxelSizeCm = VoxelWorld->GetActiveVoxelSizeCm();
float GroundCoreVoxels = 0.0f;
float CapCoreVoxels = 0.0f;
if (!NativeSurfaceSnapshot->GetHeights(StrateIndex,
LocalXYCm.X / VoxelSizeCm, LocalXYCm.Y / VoxelSizeCm,
GroundCoreVoxels, CapCoreVoxels))
{
return false;
}
OutGroundLocalZCm = GroundCoreVoxels * VoxelSizeCm;
bool bHasPlan = false;
if (!NativeSurfaceSnapshot->GetWater(StrateIndex, bHasPlan,
bOutHasWater, OutWaterLocalZCm)
|| !bHasPlan)
{
return false;
}
return true;
}
bool UVoxelContentManager::QueryNativeFloatingIslandAnchor(
int32 StrateIndex, const FVector2D& LocalXYCm, float& OutGroundLocalZCm) const
{
OutGroundLocalZCm = 0.0f;
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
if (VoxelWorld == nullptr || !VoxelWorld->bUseVoxelForgeCore
|| !NativeFloatingIslandSnapshot.IsValid())
{
return false;
}
return NativeFloatingIslandSnapshot->GetTopAnchor(
StrateIndex, LocalXYCm, OutGroundLocalZCm);
}
bool UVoxelContentManager::QueryStrateWaterAnchor(
int32 StrateIndex, bool& bOutHasWater, float& OutWaterLocalZCm) const
{
bOutHasWater = false;
OutWaterLocalZCm = -FLT_MAX;
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
if (VoxelWorld == nullptr || StrateManager == nullptr)
{
return false;
}
if (VoxelWorld->bUseVoxelForgeCore && NativeSurfaceSnapshot.IsValid())
{
bool bHasPlan = false;
bool bEnabled = false;
float NativeWaterLocalZCm = -FLT_MAX;
if (NativeSurfaceSnapshot->GetWater(
StrateIndex, bHasPlan, bEnabled, NativeWaterLocalZCm) && bHasPlan)
{
bOutHasWater = bEnabled;
OutWaterLocalZCm = bEnabled ? NativeWaterLocalZCm : -FLT_MAX;
return true;
}
}
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
if (Slot.StrateIndex != StrateIndex)
{
continue;
}
const int32 MidChunkZ = Slot.BottomChunkZ
+ (Slot.TopChunkZ - Slot.BottomChunkZ) / 2;
const float ManagerWaterVoxelZ = StrateManager->GetWaterLevelWorldZForChunk(
FIntVector(0, 0, MidChunkZ));
if (ManagerWaterVoxelZ != -FLT_MAX)
{
bOutHasWater = true;
const float ManagerWaterLocalZCm = ManagerWaterVoxelZ * VOXEL_SIZE;
OutWaterLocalZCm = VoxelWorld->FromStrateManagerLocalZ(ManagerWaterLocalZCm);
}
return true;
}
return false;
}
void UVoxelContentManager::BeginDestroy()
{
// Backstop: stop any worker from touching us. EndPlay → NotifyShutdown should have already drained.
bShuttingDown.store(true, std::memory_order_release);
JoinDecorationTasks();
Super::BeginDestroy();
}
void UVoxelContentManager::NotifyShutdown()
{
bShuttingDown.store(true, std::memory_order_release);
// A timeout is safe for the editor's pre-mutation probe, but not for UObject destruction:
// join this manager's tasks fully before dropping its worker-captured `this`.
JoinDecorationTasks();
DrainDecoResults();
ResetGridBuildState(NearGrid);
ResetGridBuildState(FarGrid);
}
bool UVoxelContentManager::WaitForDecorationTasks(double Deadline)
{
while (ActiveDecorationTasks.load(std::memory_order_relaxed) > 0)
{
if (FPlatformTime::Seconds() > Deadline) return false;
FPlatformProcess::Yield();
}
return true;
}
void UVoxelContentManager::JoinDecorationTasks()
{
while (ActiveDecorationTasks.load(std::memory_order_acquire) > 0)
{
FPlatformProcess::Yield();
}
}
void UVoxelContentManager::DrainDecoResults()
{
FDecoCellResult Discard;
while (DecoResults.Dequeue(Discard)) {}
}
void UVoxelContentManager::ResetGridBuildState(FDecoGrid& G)
{
G.Builds.Reset();
G.Completed.Reset();
G.PendingLaunch.Reset();
G.InFlightCells.Reset();
}
//=============================================================================
// Cell ↔ region helpers (region = RxR cells). Floor-division tiles across the origin (negative cells).
//=============================================================================
int32 UVoxelContentManager::RegionSize() const
{
return FMath::Max(1, Settings ? Settings->DecorationRegionSizeCells : 4);
}
// TRUE floor division (rounds toward -infinity). DO NOT use FMath::DivideAndRoundDown for cell↔region
// mapping: it is `A / B`, which TRUNCATES toward zero for negatives (a UE naming footgun). Cells are
// assigned to a region by the exact inverse `region*R + offset`, so the mapping back MUST floor — with
// truncation, a negative-coord cell is queued under one region but routed back to another → its march
// result is dropped → blank chunk. Truncation == floor for A>=0 and for R==1, which is exactly why only
// NEGATIVE coordinates at R>=2 were affected. R (region size) is always > 0 here.
static FORCEINLINE int32 FloorDivPos(int64 A, int32 R)
{
if (R <= 0) { return 0; }
const int64 Quotient = A / R;
const int64 Remainder = A % R;
return static_cast<int32>(FMath::Clamp<int64>(
Remainder < 0 ? Quotient - 1 : Quotient, MIN_int32, MAX_int32));
}
static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
{
return FIntPoint(FloorDivPos(Cell.X, R), FloorDivPos(Cell.Y, R));
}
// Region is desired iff its cell footprint intersects the radius-FarR cell box around the player.
static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell,
int32 FarR, int32 R)
{
const int32 MinX = FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R);
const int32 MaxX = FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R);
const int32 MinY = FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R);
const int32 MaxY = FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R);
return Region.X >= MinX && Region.X <= MaxX && Region.Y >= MinY && Region.Y <= MaxY;
}
//=============================================================================
// DECORATIONS — distance-based world grid (async march)
//=============================================================================
// Deterministic placement hash. Pure function of cell, column, crossing, entry, seed.
static FORCEINLINE uint32 DecoHash(int32 CX, int32 CY, int32 GX, int32 GY,
int32 Crossing, int32 Entry, uint32 Seed, uint32 Salt)
{
uint32 H = VoxelHash::Cell(CX, CY, Seed ^ Salt);
H = VoxelHash::Mix(H ^ ((uint32)GX * 73856093u) ^ ((uint32)GY * 19349663u));
H = VoxelHash::Mix(H ^ ((uint32)Crossing * 83492791u + 0x9E3779B1u));
H = VoxelHash::Mix(H ^ ((uint32)Entry * 2654435761u + 40503u));
return VoxelHash::Mix(H);
}
void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
{
if (!StrateManager || !Generator || !Settings) return;
AActor* OwnerActor = Owner.Get();
if (!OwnerActor) return;
const FTransform OwnerXf = OwnerActor->GetActorTransform();
// All strate/water/density queries are in actor-LOCAL space; Core uses the actor's active voxel step.
// Les requetes de strate/eau/densite sont locales a l'acteur; Core utilise son pas de voxel actif.
const FVector LocalPlayer = OwnerXf.InverseTransformPosition(PlayerWorldPos);
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(OwnerActor);
const float ManagerLocalZ = VoxelWorld
? VoxelWorld->ToStrateManagerLocalZ(LocalPlayer.Z) : LocalPlayer.Z;
// Player's strate band (actor-local cm). No strate (inter-strate gap / outside world) ⇒ no decos.
float TopZ, BotZ;
const bool bInStrate = StrateManager->GetStrateUnrealZRange(ManagerLocalZ, TopZ, BotZ);
const int32 StrateIndex = bInStrate ? StrateManager->GetStrateIndex(ManagerLocalZ) : INT32_MIN;
if (!bInStrate)
{
ClearAllDecorations();
LastStrateIndex = INT32_MIN;
LastDecoCell = FIntPoint(INT32_MIN, INT32_MIN);
return;
}
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
const float CellWorld = static_cast<float>(DECO_CELL_VOXELS) * VoxelSizeCm;
const FIntPoint PlayerCell(
FMath::FloorToInt(LocalPlayer.X / CellWorld),
FMath::FloorToInt(LocalPlayer.Y / CellWorld));
// Shared per-update strate context (a strate is a horizontal slab → same for every cell).
CurrentCtx = FDecoContext();
CurrentCtx.StrateIndex = StrateIndex;
CurrentCtx.VoxelSizeCm = VoxelSizeCm;
CurrentCtx.ActiveToOwnerVoxelScale = VoxelSizeCm / VOXEL_SIZE;
CurrentCtx.TopVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ) / VoxelSizeCm;
CurrentCtx.BottomVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(BotZ) : BotZ) / VoxelSizeCm;
CurrentCtx.RepChunkZ = FMath::FloorToInt(((TopZ + BotZ) * 0.5f / VOXEL_SIZE) / (float)CHUNK_SIZE);
const FIntVector RepChunk(PlayerCell.X, PlayerCell.Y, CurrentCtx.RepChunkZ);
CurrentCtx.Def = StrateManager->GetStrateForChunk(RepChunk);
CurrentCtx.bSurfaceWorld =
(StrateManager->GetGeneratorTypeForChunk(RepChunk) == ECaveGeneratorType::SurfaceWorld);
CurrentCtx.NativeSurface = NativeSurfaceSnapshot;
CurrentCtx.bNativeSurface = VoxelWorld != nullptr && VoxelWorld->bUseVoxelForgeCore
&& CurrentCtx.bSurfaceWorld
&& CurrentCtx.NativeSurface.IsValid()
&& CurrentCtx.NativeSurface->FindPlan(StrateIndex) != nullptr;
CurrentCtx.NativeIslands = NativeFloatingIslandSnapshot;
CurrentCtx.bNativeFloatingIslands = VoxelWorld != nullptr && VoxelWorld->bUseVoxelForgeCore
&& StrateManager->GetGeneratorTypeForChunk(RepChunk) == ECaveGeneratorType::FloatingIslands
&& CurrentCtx.NativeIslands.IsValid()
&& CurrentCtx.NativeIslands->FindField(StrateIndex) != nullptr;
{
bool bHasNativePlan = false;
bool bNativeWaterEnabled = false;
float NativeWaterLocalZ = -FLT_MAX;
if (CurrentCtx.bNativeSurface
&& CurrentCtx.NativeSurface->GetWater(StrateIndex, bHasNativePlan,
bNativeWaterEnabled, NativeWaterLocalZ)
&& bHasNativePlan)
{
CurrentCtx.bHasWater = bNativeWaterEnabled;
CurrentCtx.WaterLocalZ = bNativeWaterEnabled ? NativeWaterLocalZ : -FLT_MAX;
}
else
{
const float Wv = StrateManager->GetWaterLevelWorldZForChunk(RepChunk);
CurrentCtx.bHasWater = (Wv != -FLT_MAX);
CurrentCtx.WaterLocalZ = CurrentCtx.bHasWater
? (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(Wv * VOXEL_SIZE) : Wv * VOXEL_SIZE)
: -FLT_MAX;
}
}
// Strate biome field (XY-global → resolved once; the worker picks the dominant biome per COLUMN).
CurrentCtx.BiomeCtx = StrateManager->GetBiomeContextForChunk(RepChunk);
// Refresh each grid's (tier, radius, spacing) from settings for this update. Radius/spacing are read
// every frame so live edits to the data asset take effect; the grids themselves persist across updates.
// A stationary player still needs a rebuild when one of these values changes: otherwise already
// loaded regions remain outside the new radius (or retain the old sampling density) forever.
const int32 NewNearRadius = FMath::Max(1, Settings->DecorationNearRadiusChunks);
const int32 NewFarRadius = FMath::Max(1, Settings->DecorationRadiusChunks);
const int32 NewNearSpacing = FMath::Clamp(Settings->DecorationSpacingVoxels, 1, CHUNK_SIZE);
const int32 NewFarSpacing = FMath::Clamp(Settings->DecorationFarSpacingVoxels, 1, CHUNK_SIZE);
const int32 NewRegionSize = RegionSize();
const bool bDecorationGridConfigChanged =
NearGrid.Radius != NewNearRadius || FarGrid.Radius != NewFarRadius
|| NearGrid.Spacing != NewNearSpacing || FarGrid.Spacing != NewFarSpacing
|| LastRegionSize != NewRegionSize;
NearGrid.Tier = EDecoStreamTier::Near;
NearGrid.Radius = NewNearRadius;
NearGrid.Spacing = NewNearSpacing;
FarGrid.Tier = EDecoStreamTier::Far;
FarGrid.Radius = NewFarRadius;
FarGrid.Spacing = NewFarSpacing;
// Build the decoration palette ONCE for this update, PARTITIONED by tier. With biomes, concatenate every
// biome's deco list and tag each entry with its context-biome index; the worker resolves a column's biome
// and rolls only the entries it owns → borders follow the warped-Voronoi field, not the 8 m cell grid
// (Task 1, §8.5). Without biomes, fall back to the strate's single list tagged -1 (always matches). Each
// entry routes to NearGrid/FarGrid by its StreamTier, so each grid marches only its own subset.
NearGrid.Entries.Reset(); NearGrid.EntryBiome.Reset();
FarGrid.Entries.Reset(); FarGrid.EntryBiome.Reset();
auto AddEntry = [&](const FStrateDecoration& D, int32 ci)
{
FDecoGrid& G = (D.StreamTier == EDecoStreamTier::Near) ? NearGrid : FarGrid;
G.Entries.Add(D);
G.EntryBiome.Add(ci);
};
if (CurrentCtx.Def)
{
if (CurrentCtx.BiomeCtx.IsValid())
{
for (int32 ci = 0; ci < CurrentCtx.BiomeCtx.Biomes.Num(); ++ci)
{
const int32 StrateBiomeIdx = CurrentCtx.BiomeCtx.Biomes[ci].Index;
const UVoxelBiomeDefinition* Bio =
CurrentCtx.Def->Biomes.IsValidIndex(StrateBiomeIdx) ? CurrentCtx.Def->Biomes[StrateBiomeIdx] : nullptr;
// A biome with no decorations inherits the strate's list (still tagged with THIS biome's
// index so it only fires inside that biome's columns — no cross-biome bleed).
const TArray<FStrateDecoration>& Src =
(Bio && Bio->Decorations.Num() > 0) ? Bio->Decorations : CurrentCtx.Def->Decorations;
for (const FStrateDecoration& D : Src) { AddEntry(D, ci); }
}
}
else
{
for (const FStrateDecoration& D : CurrentCtx.Def->Decorations) { AddEntry(D, -1); } // -1 → matches ColBiome -1
}
}
// Strate or grid configuration change → wipe + force a full rebuild. This also invalidates
// every old region build before the next stationary-player update can reuse it.
if (StrateIndex != LastStrateIndex || bDecorationGridConfigChanged)
{
ClearAllDecorations();
LastStrateIndex = StrateIndex;
LastDecoCell = FIntPoint(INT32_MIN, INT32_MIN);
}
LastNearRadius = NewNearRadius;
LastFarRadius = NewFarRadius;
LastNearSpacing = NewNearSpacing;
LastFarSpacing = NewFarSpacing;
LastRegionSize = NewRegionSize;
if (PlayerCell != LastDecoCell)
{
RebuildDesiredCells(NearGrid, PlayerCell);
RebuildDesiredCells(FarGrid, PlayerCell);
LastDecoCell = PlayerCell;
}
// Both grids share ONE concurrency budget; throttle each against the other's current in-flight count.
const int32 MaxConc = Settings->MaxConcurrentDecorationTasks;
LaunchDecoTasks(NearGrid, PlayerCell, FarGrid.InFlightCells.Num(), MaxConc);
LaunchDecoTasks(FarGrid, PlayerCell, NearGrid.InFlightCells.Num(), MaxConc);
ProcessDecoResults(PlayerCell);
}
bool UVoxelContentManager::MeasurePlacementCostForValidation(
const FVector& PlayerWorldPos, int32 SampleCellCount, int32 FrameSampleCount,
FDecorationPlacementCostAudit& OutAudit, FString& OutError)
{
OutAudit = FDecorationPlacementCostAudit();
OutError.Reset();
if (!Settings || !Generator || !StrateManager || !Owner.IsValid()
|| SampleCellCount < 1 || SampleCellCount > 256
|| FrameSampleCount < 1 || FrameSampleCount > 256)
{
OutError = TEXT("Decoration cost audit requires an initialized manager and bounded sample counts.");
return false;
}
if (!WaitForDecorationTasks(FPlatformTime::Seconds() + 10.0))
{
OutError = TEXT("Decoration cost audit could not drain prior worker tasks.");
return false;
}
struct FRestoreDecorationConcurrency
{
UVoxelSettings* Settings = nullptr;
int32 Original = 0;
~FRestoreDecorationConcurrency()
{
if (Settings != nullptr) Settings->MaxConcurrentDecorationTasks = Original;
}
} Restore{Settings, Settings->MaxConcurrentDecorationTasks};
// Resolve the current route and palette once, while preventing test work from reaching HISMs.
Settings->MaxConcurrentDecorationTasks = 0;
UpdateDecorations(PlayerWorldPos);
if (CurrentCtx.Def == nullptr)
{
OutError = TEXT("Decoration cost audit position is outside the resolved strate layout.");
return false;
}
OutAudit.bNativeSurface = CurrentCtx.bNativeSurface;
OutAudit.bNativeFloatingIslands = CurrentCtx.bNativeFloatingIslands;
OutAudit.NearEntries = NearGrid.Entries.Num();
OutAudit.FarEntries = FarGrid.Entries.Num();
auto MakeAuditEntries = [&](const FDecoGrid& Grid, TArray<FStrateDecoration>& Entries,
TArray<int32>& EntryBiome) -> bool
{
if (!Grid.Entries.IsEmpty())
{
Entries = Grid.Entries;
EntryBiome = Grid.EntryBiome;
return false;
}
OutAudit.bSyntheticWorkload = true;
const int32 BiomeCount = CurrentCtx.BiomeCtx.IsValid()
? CurrentCtx.BiomeCtx.Biomes.Num() : 0;
const int32 EntryCount = FMath::Max(1, BiomeCount);
Entries.Reserve(EntryCount);
EntryBiome.Reserve(EntryCount);
for (int32 Index = 0; Index < EntryCount; ++Index)
{
FStrateDecoration Entry;
Entry.Profile.ActorClass = AActor::StaticClass();
Entry.Profile.SurfacePlacement = ESurfaceType::Floor;
Entry.SpawnDensity = 1.0f;
Entry.MaxPerChunk = 4;
Entries.Add(MoveTemp(Entry));
EntryBiome.Add(BiomeCount > 0 ? Index : -1);
}
return true;
};
TArray<FStrateDecoration> NearEntriesAudit, FarEntriesAudit;
TArray<int32> NearEntryBiomeAudit, FarEntryBiomeAudit;
const bool bNearSynthetic = MakeAuditEntries(NearGrid, NearEntriesAudit, NearEntryBiomeAudit);
const bool bFarSynthetic = MakeAuditEntries(FarGrid, FarEntriesAudit, FarEntryBiomeAudit);
AActor* OwnerActor = Owner.Get();
const FTransform OwnerXf = OwnerActor->GetActorTransform();
const FVector LocalPlayer = OwnerXf.InverseTransformPosition(PlayerWorldPos);
const float CellWorldCm = static_cast<float>(DECO_CELL_VOXELS) * CurrentCtx.VoxelSizeCm;
const FIntPoint PlayerCell(FMath::FloorToInt(LocalPlayer.X / CellWorldCm),
FMath::FloorToInt(LocalPlayer.Y / CellWorldCm));
const int32 GridSide = FMath::CeilToInt(FMath::Sqrt(static_cast<float>(SampleCellCount)));
const int32 Step = FMath::Max(1, Settings->DecorationMarchStepVoxels);
const int32 MaxCrossings = FMath::Max(1, Settings->DecorationMaxCrossingsPerColumn);
const float ColumnDepth = static_cast<float>(FMath::Max(8, Settings->DecorationColumnDepthVoxels));
TArray<double> CellMilliseconds, FullScanCellMilliseconds;
CellMilliseconds.Reserve(SampleCellCount);
int64 TotalColumns = 0;
int64 TotalDensityQueries = 0;
int64 TotalSurfaceQueries = 0;
for (int32 Sample = 0; Sample < SampleCellCount; ++Sample)
{
const int32 X = Sample % GridSide - GridSide / 2;
const int32 Y = Sample / GridSide - GridSide / 2;
const FIntPoint Cell(PlayerCell.X + X, PlayerCell.Y + Y);
double CellElapsed = 0.0, FullScanCellElapsed = 0.0;
for (const auto& GridAudit : {
std::make_pair(&NearGrid, &NearEntriesAudit),
std::make_pair(&FarGrid, &FarEntriesAudit)})
{
const FDecoGrid& Grid = *GridAudit.first;
const TArray<FStrateDecoration>& Entries = *GridAudit.second;
const TArray<int32>& EntryBiome = Grid.Tier == EDecoStreamTier::Near
? NearEntryBiomeAudit : FarEntryBiomeAudit;
if (Entries.IsEmpty()) continue;
TArray<FDecoSpawn> Spawns;
FPlacementBuildCounters Counters;
TArray<FDecoSpawn> FullScanSpawns;
const double FullStart = FPlatformTime::Seconds();
BuildCellSpawns(Generator, OwnerXf, Cell, CurrentCtx, Entries, EntryBiome,
static_cast<uint32>(Seed), Grid.Spacing, static_cast<float>(Step),
MaxCrossings, ColumnDepth, FullScanSpawns, nullptr, false);
FullScanCellElapsed += (FPlatformTime::Seconds()-FullStart)*1000.0;
const double CandidateStart = FPlatformTime::Seconds();
BuildCellSpawns(Generator, OwnerXf, Cell, CurrentCtx, Entries, EntryBiome,
static_cast<uint32>(Seed), Grid.Spacing, static_cast<float>(Step),
MaxCrossings, ColumnDepth, Spawns, &Counters);
CellElapsed += (FPlatformTime::Seconds()-CandidateStart)*1000.0;
if (Spawns.Num()!=FullScanSpawns.Num())
{ OutError=TEXT("Feature-query placement count changed from full scan."); return false; }
for (int32 Index=0;Index<Spawns.Num();++Index)
{
if (Spawns[Index].EntryIdx!=FullScanSpawns[Index].EntryIdx
|| !Spawns[Index].Xf.Equals(FullScanSpawns[Index].Xf,0.0f))
{ OutError=TEXT("Feature-query placement transform changed from full scan."); return false; }
++OutAudit.ExactPlacementComparisons;
}
TotalColumns += Counters.Columns;
TotalDensityQueries += Counters.DensityQueries;
TotalSurfaceQueries += Counters.SurfaceQueries;
OutAudit.Placements += Spawns.Num();
const bool bSyntheticGrid = Grid.Tier == EDecoStreamTier::Near
? bNearSynthetic : bFarSynthetic;
if (bSyntheticGrid)
{
for (const FDecoSpawn& Spawn : Spawns)
{
const FVector LocalSpawn = OwnerXf.InverseTransformPosition(Spawn.Xf.GetLocation());
if (CurrentCtx.bNativeSurface && CurrentCtx.NativeSurface.IsValid())
{
float GroundZ = 0.0f;
bool bHasWater = false;
float WaterZ = -FLT_MAX;
if (QueryNativeSurfaceAnchors(CurrentCtx.StrateIndex,
FVector2D(LocalSpawn.X, LocalSpawn.Y), GroundZ, bHasWater, WaterZ))
{
OutAudit.MaximumAnchorErrorCm = FMath::Max(
OutAudit.MaximumAnchorErrorCm,
FMath::Abs(static_cast<double>(LocalSpawn.Z - GroundZ)));
++OutAudit.AnchorChecks;
}
}
else
{
float Density = 1.0f;
const bool bSampled = CurrentCtx.bNativeFloatingIslands
&& CurrentCtx.NativeIslands.IsValid()
? CurrentCtx.NativeIslands->GetDensity(CurrentCtx.StrateIndex,
LocalSpawn.X / CurrentCtx.VoxelSizeCm,
LocalSpawn.Y / CurrentCtx.VoxelSizeCm,
LocalSpawn.Z / CurrentCtx.VoxelSizeCm, Density)
: (Density = Generator->GetDensityAt(
LocalSpawn.X / CurrentCtx.VoxelSizeCm * CurrentCtx.ActiveToOwnerVoxelScale,
LocalSpawn.Y / CurrentCtx.VoxelSizeCm * CurrentCtx.ActiveToOwnerVoxelScale,
LocalSpawn.Z / CurrentCtx.VoxelSizeCm * CurrentCtx.ActiveToOwnerVoxelScale), true);
if (bSampled)
{
OutAudit.MaximumDensityResidual = FMath::Max(
OutAudit.MaximumDensityResidual,
FMath::Abs(static_cast<double>(Density)));
++OutAudit.AnchorChecks;
}
}
}
}
}
CellMilliseconds.Add(CellElapsed);
FullScanCellMilliseconds.Add(FullScanCellElapsed);
OutAudit.ActiveBatchMilliseconds += CellElapsed;
OutAudit.FullScanActiveBatchMilliseconds += FullScanCellElapsed;
}
auto MedianAndP95 = [](TArray<double> Samples, double& OutMedian, double& OutP95)
{
Samples.Sort();
if (Samples.IsEmpty())
{
OutMedian = 0.0;
OutP95 = 0.0;
return;
}
OutMedian = Samples[Samples.Num() / 2];
const int32 P95Index = FMath::Clamp(FMath::CeilToInt(Samples.Num() * 0.95f) - 1,
0, Samples.Num() - 1);
OutP95 = Samples[P95Index];
};
OutAudit.SampledCells = SampleCellCount;
OutAudit.ColumnsPerCell = static_cast<int32>(TotalColumns / SampleCellCount);
OutAudit.DensityQueriesPerCell = static_cast<double>(TotalDensityQueries) / SampleCellCount;
OutAudit.SurfaceQueriesPerCell = static_cast<double>(TotalSurfaceQueries) / SampleCellCount;
MedianAndP95(MoveTemp(CellMilliseconds), OutAudit.CellMedianMilliseconds,
OutAudit.CellP95Milliseconds);
double UnusedMedian = 0.0;
MedianAndP95(MoveTemp(FullScanCellMilliseconds),UnusedMedian,OutAudit.FullScanCellP95Milliseconds);
// Deliberately populated fixture alongside the unmodified authored palette. No actors are spawned.
// Fixture volontairement peuplee, a cote de la palette auteur intacte. Aucun acteur cree.
UStaticMesh* FixtureMesh=nullptr;
for (const auto* Entries : {&NearEntriesAudit,&FarEntriesAudit})
for (const auto& Entry : *Entries)
if (!FixtureMesh && Entry.Profile.InstancedMesh) FixtureMesh=Entry.Profile.InstancedMesh;
if (!FixtureMesh)
FixtureMesh=LoadObject<UStaticMesh>(nullptr,TEXT("/Engine/BasicShapes/Cube.Cube"));
TMap<FIntPoint,FDecoRegionBuild> FixtureBuilds;
TArray<FStrateDecoration> FixtureEntries;
TArray<int32> FixtureBiomes;
const int32 BiomeCount = CurrentCtx.BiomeCtx.IsValid() ? CurrentCtx.BiomeCtx.Biomes.Num() : 0;
for (int32 Biome=0;Biome<FMath::Max(1,BiomeCount);++Biome)
for (int32 Wet=0;Wet<2;++Wet)
{
FStrateDecoration Entry;
Entry.Profile.ActorClass=AActor::StaticClass();
Entry.Profile.InstancedMesh=FixtureMesh;
Entry.Profile.SurfacePlacement=ESurfaceType::Floor;
Entry.Profile.bPlaceBelowWater=Wet!=0;
Entry.SpawnDensity=1.0f;
Entry.MaxPerChunk=64;
FixtureEntries.Add(Entry);
FixtureBiomes.Add(BiomeCount ? Biome : -1);
}
TArray<double> PopulatedTimes, FullPopulatedTimes;
for (int32 Sample=0;Sample<SampleCellCount;++Sample)
{
const FIntPoint Cell(PlayerCell.X+Sample%GridSide-GridSide/2,
PlayerCell.Y+Sample/GridSide-GridSide/2);
TArray<FDecoSpawn> FullSpawns, Spawns;
const double FullStart=FPlatformTime::Seconds();
BuildCellSpawns(Generator,OwnerXf,Cell,CurrentCtx,FixtureEntries,FixtureBiomes,
static_cast<uint32>(Seed),4,static_cast<float>(Step),MaxCrossings,ColumnDepth,
FullSpawns,nullptr,false);
const double FullElapsed=(FPlatformTime::Seconds()-FullStart)*1000.0;
const double CandidateStart=FPlatformTime::Seconds();
BuildCellSpawns(Generator,OwnerXf,Cell,CurrentCtx,FixtureEntries,FixtureBiomes,
static_cast<uint32>(Seed),4,static_cast<float>(Step),MaxCrossings,ColumnDepth,Spawns);
const double Elapsed=(FPlatformTime::Seconds()-CandidateStart)*1000.0;
OutAudit.PopulatedBatchMilliseconds+=Elapsed;
OutAudit.FullScanPopulatedBatchMilliseconds+=FullElapsed;
if (Spawns.Num()!=FullSpawns.Num())
{ OutError=TEXT("Populated fixture count differs from full scan."); return false; }
const double MergeStart=FPlatformTime::Seconds();
if (FixtureMesh && !Spawns.IsEmpty())
{
auto& Build=FixtureBuilds.FindOrAdd(CellToRegion(Cell,RegionSize()));
auto& Bucket=Build.MeshBuckets.FindOrAdd(FixtureMesh);
for (const auto& Spawn : Spawns)
{
Bucket.Profile=FixtureEntries[Spawn.EntryIdx].Profile;
Bucket.Xforms.Add(Spawn.Xf);
}
}
OutAudit.PopulatedMergeMilliseconds+=(FPlatformTime::Seconds()-MergeStart)*1000.0;
for (int32 Index=0;Index<Spawns.Num();++Index)
{
if (!Spawns[Index].Xf.Equals(FullSpawns[Index].Xf,0.0f))
{ OutError=TEXT("Populated fixture anchor differs from full scan."); return false; }
++OutAudit.ExactPlacementComparisons;
const FVector Local=OwnerXf.InverseTransformPosition(Spawns[Index].Xf.GetLocation());
float Density=0.0f;
if (CurrentCtx.bNativeFloatingIslands && CurrentCtx.NativeIslands.IsValid()
&& CurrentCtx.NativeIslands->GetDensity(CurrentCtx.StrateIndex,
Local.X/CurrentCtx.VoxelSizeCm,Local.Y/CurrentCtx.VoxelSizeCm,
Local.Z/CurrentCtx.VoxelSizeCm,Density))
{
OutAudit.MaximumDensityResidual=FMath::Max(OutAudit.MaximumDensityResidual,
FMath::Abs(static_cast<double>(Density)));
++OutAudit.AnchorChecks;
if (FMath::Abs(Density)>0.25f)
{ OutError=TEXT("Populated fixture is not anchored on the canonical field."); return false; }
}
}
if (!Spawns.IsEmpty())
{
++OutAudit.PopulatedCells;
OutAudit.PopulatedPlacements+=Spawns.Num();
PopulatedTimes.Add(Elapsed);
FullPopulatedTimes.Add(FullElapsed);
}
}
if ((CurrentCtx.bNativeSurface||CurrentCtx.bNativeFloatingIslands) && OutAudit.PopulatedPlacements==0)
{ OutError=TEXT("Populated content fixture produced no placements."); return false; }
if (FixtureMesh && OutAudit.PopulatedPlacements>0)
{
// Exercise actual region application, then close this isolated fixture immediately.
// Appliquer une vraie region puis detruire immediatement cette fixture isolee.
FDecoGrid FixtureGrid;
const double ApplyStart=FPlatformTime::Seconds();
// Fixture footprints are isolated from live landmark suppression. / Isoler les empreintes de fixture.
for (auto& Pair : FixtureBuilds) ApplyRegion(FixtureGrid,Pair.Key,Pair.Value,false);
OutAudit.PopulatedApplyMilliseconds=(FPlatformTime::Seconds()-ApplyStart)*1000.0;
for (const auto& Pair : FixtureGrid.Regions)
for (const auto& WeakHISM : Pair.Value.Instances)
if (const auto* HISM=WeakHISM.Get()) OutAudit.AppliedInstances+=HISM->GetInstanceCount();
for (const auto& Pair : FixtureBuilds) ClearDecorationRegion(FixtureGrid,Pair.Key);
if (OutAudit.AppliedInstances!=OutAudit.PopulatedPlacements)
{
OutError=FString::Printf(TEXT("Populated HISM fixture applied %d of %d placements (%d regions, mesh %s)."),
OutAudit.AppliedInstances,OutAudit.PopulatedPlacements,FixtureBuilds.Num(),*FixtureMesh->GetPathName());
return false;
}
}
MedianAndP95(MoveTemp(PopulatedTimes),UnusedMedian,OutAudit.PopulatedCellP95Milliseconds);
MedianAndP95(MoveTemp(FullPopulatedTimes),UnusedMedian,OutAudit.FullScanPopulatedCellP95Milliseconds);
// The game-thread update cost is sampled with task dispatch disabled; worker-cell cost is reported
// separately above, so the frame figure is not polluted by waiting for asynchronous work.
UpdateDecorations(PlayerWorldPos);
TArray<double> UpdateMilliseconds;
UpdateMilliseconds.Reserve(FrameSampleCount);
for (int32 Frame = 0; Frame < FrameSampleCount; ++Frame)
{
const double UpdateStart = FPlatformTime::Seconds();
UpdateDecorations(PlayerWorldPos);
UpdateMilliseconds.Add((FPlatformTime::Seconds() - UpdateStart) * 1000.0);
}
MedianAndP95(MoveTemp(UpdateMilliseconds), OutAudit.UpdateMedianMilliseconds,
OutAudit.UpdateP95Milliseconds);
return true;
}
void UVoxelContentManager::RebuildDesiredCells(FDecoGrid& G, const FIntPoint& PlayerCell)
{
// REGION-granular streaming, per grid. Decoration cells are grouped into RxR regions; a region is the
// load/unload unit and shares ONE HISM per mesh, so the render thread walks ~R^2 fewer components. A
// region, once desired, marches ALL of its cells (so it is self-contained and NEVER re-streamed in
// place while it stays in range — same no-flicker guarantee the per-cell grid had, now per region). The
// radius is G.Radius (this grid's tier), so Near and Far stream to different distances independently.
const int32 FarR = G.Radius;
const int32 R = RegionSize();
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
const FIntPoint RMin(FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R),
FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R));
const FIntPoint RMax(FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R),
FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R));
TSet<FIntPoint> DesiredRegions;
constexpr int64 MaxRegionReserve = 4096;
const int64 WidthX = static_cast<int64>(RMax.X) - RMin.X + 1;
const int64 WidthY = static_cast<int64>(RMax.Y) - RMin.Y + 1;
const int64 ReserveCount = WidthY > 0 && WidthX > MaxRegionReserve / WidthY
? MaxRegionReserve : FMath::Max<int64>(0, WidthX * WidthY);
DesiredRegions.Reserve(static_cast<int32>(ReserveCount));
for (int64 ry = RMin.Y; ry <= RMax.Y; ++ry)
for (int64 rx = RMin.X; rx <= RMax.X; ++rx)
{
DesiredRegions.Add(FIntPoint(static_cast<int32>(rx), static_cast<int32>(ry)));
}
// Unload loaded regions no longer desired (plain DestroyComponent — no per-instance removal).
{
TArray<FIntPoint> Loaded; G.Regions.GetKeys(Loaded);
for (const FIntPoint& K : Loaded)
{
if (!DesiredRegions.Contains(K)) ClearDecorationRegion(G, K);
}
}
TArray<FIntPoint> DesiredRegionList;
DesiredRegionList.Reserve(DesiredRegions.Num());
for (const FIntPoint& Region : DesiredRegions)
{
DesiredRegionList.Add(Region);
}
DesiredRegionList.Sort([](const FIntPoint& A, const FIntPoint& B)
{
return (A.Y != B.Y) ? (A.Y < B.Y) : (A.X < B.X);
});
// Start a build for each desired region that isn't already loaded or building. In-progress builds are
// LEFT to finish even if they fell out of range (their cell tasks are already off-thread); the apply
// step discards a completed build that is no longer desired (see ProcessDecoResults). Each new build
// enqueues all RxR of its cells once — a building region is never re-queued (no duplicate launches).
for (const FIntPoint& Region : DesiredRegionList)
{
if (G.Regions.Contains(Region)) continue; // already applied → leave it (no re-stream)
if (G.Builds.Contains(Region)) continue; // already marching its cells
FDecoRegionBuild& Build = G.Builds.Add(Region);
Build.BuildId = G.NextBuildId++;
Build.CellsRemaining = R * R;
const int32 BaseX = Region.X * R, BaseY = Region.Y * R;
for (int32 cy = 0; cy < R; ++cy)
for (int32 cx = 0; cx < R; ++cx)
{
G.PendingLaunch.Add(FIntPoint(BaseX + cx, BaseY + cy));
}
}
// Nearest-first so the region under the player fills in before the fringe. Stale entries (cells whose
// build was already discarded) are cheaply skipped at launch, so PendingLaunch self-cleans as it drains.
G.PendingLaunch.Sort([PlayerCell](const FIntPoint& A, const FIntPoint& B)
{
const int32 AD = CellChebyshev(A, PlayerCell);
const int32 BD = CellChebyshev(B, PlayerCell);
if (AD != BD) return AD < BD;
return (A.Y != B.Y) ? (A.Y < B.Y) : (A.X < B.X);
});
}
void UVoxelContentManager::LaunchDecoTasks(FDecoGrid& G, const FIntPoint& PlayerCell, int32 OtherInFlight, int32 MaxConc)
{
if (!CurrentCtx.Def || !Generator) return;
if (MaxConc <= 0)
{
// Decorations disabled at runtime — drop THIS grid's queued/pending build state so nothing is stranded.
G.PendingLaunch.Reset();
G.Builds.Reset();
G.Completed.Reset();
return;
}
AActor* OwnerActor = Owner.Get();
if (!OwnerActor) return;
const FTransform OwnerXf = OwnerActor->GetActorTransform();
const int32 R = RegionSize();
const int32 Spacing = G.Spacing; // fine (Near) or coarse (Far) — the per-grid column grid
const float Step = (float)FMath::Max(1, Settings->DecorationMarchStepVoxels);
const int32 MaxCross = FMath::Max(1, Settings->DecorationMaxCrossingsPerColumn);
const float ColDepth = (float)FMath::Max(8, Settings->DecorationColumnDepthVoxels);
const EDecoStreamTier GridTier = G.Tier; // stamped on each result so it routes back to this grid
// Throttle against the COMBINED in-flight count (this grid + the other) so both grids share MaxConc.
// Drain from the head by INDEX — RemoveAt(0) per pop shifted the whole array every time (O(N) each,
// quadratic on a long queue); now it's one compaction at the end. A cell still in flight from a
// PREVIOUS build (its build was dropped while the task was airborne — e.g. the MaxConc==0 reset path)
// is DEFERRED instead of dropped: dropping it would leave the NEW build waiting forever for a cell
// that never reports (a permanently blank, never-reapplied region).
int32 Head = 0;
TArray<FIntPoint> Deferred;
while (Head < G.PendingLaunch.Num() && (G.InFlightCells.Num() + OtherInFlight) < MaxConc)
{
const FIntPoint Cell = G.PendingLaunch[Head++];
if (G.InFlightCells.Contains(Cell)) { Deferred.Add(Cell); continue; }
// The cell's region build drives completion. If it's gone (region applied or discarded since this
// cell was queued), drop the cell — no range check here: a region intentionally marches all its
// cells (some sit just past FarR), and discarding the build is the only "no longer wanted" signal.
const FIntPoint Region = CellToRegion(Cell, R);
FDecoRegionBuild* Build = G.Builds.Find(Region);
if (!Build) continue;
const uint32 BuildId = Build->BuildId;
// This grid's palette (its tier's entries, flattened + biome-tagged) is built ONCE per update in
// UpdateDecorations; the per-COLUMN biome pick happens on the worker. Snapshot the flat list +
// tags for this cell's task (the biome context rides in Ctx).
if (G.Entries.Num() == 0)
{
MarkCellDone(G, Region, Cell, BuildId); // empty cell still counts toward the region's completion
continue;
}
TArray<FStrateDecoration> EntriesCopy = G.Entries; // snapshot for the worker + the spawner
TArray<int32> EntryBiomeCopy = G.EntryBiome; // parallel: ctx-biome owner per entry
const FDecoContext Ctx = CurrentCtx; // PODs only used on the worker
const uint32 LocalSeed = (uint32)Seed;
UVoxelGenerator* Gen = Generator;
G.InFlightCells.Add(Cell);
ActiveDecorationTasks.fetch_add(1, std::memory_order_relaxed);
UE::Tasks::Launch(TEXT("DecoMarch"),
[this, Gen, OwnerXf, Cell, Ctx, LocalSeed, Spacing, Step, MaxCross, ColDepth, BuildId, GridTier,
Entries = MoveTemp(EntriesCopy), EntryBiome = MoveTemp(EntryBiomeCopy)]() mutable
{
struct FGuard
{
UVoxelContentManager* Owner;
~FGuard() { Owner->ActiveDecorationTasks.fetch_sub(1, std::memory_order_release); }
} Guard{this};
if (bShuttingDown.load(std::memory_order_relaxed)) return;
FDecoCellResult Result;
Result.Cell = Cell;
Result.BuildId = BuildId;
Result.Grid = GridTier;
Result.Entries = MoveTemp(Entries);
BuildCellSpawns(Gen, OwnerXf, Cell, Ctx, Result.Entries, EntryBiome, LocalSeed,
Spacing, Step, MaxCross, ColDepth, Result.Spawns);
if (!bShuttingDown.load(std::memory_order_relaxed))
{
DecoResults.Enqueue(MoveTemp(Result));
}
}, UE::Tasks::ETaskPriority::BackgroundNormal);
}
if (Head > 0) { G.PendingLaunch.RemoveAt(0, Head); }
G.PendingLaunch.Append(Deferred); // retry next update, once the old task frees the cell
}
// ---- WORKER THREAD: find each column's surface points → spawn commands. ----
void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTransform& OwnerXf,
const FIntPoint& Cell, const FDecoContext& Ctx,
const TArray<FStrateDecoration>& Entries,
const TArray<int32>& EntryBiome, uint32 InSeed,
int32 Spacing, float Step, int32 MaxCrossings, float ColumnDepth,
TArray<FDecoSpawn>& OutSpawns,
FPlacementBuildCounters* OutCounters, bool bUseFeatureQueries)
{
if (OutCounters != nullptr) *OutCounters = FPlacementBuildCounters();
if (!Gen || Entries.Num() == 0) return;
const int32 PerAxis = FMath::Max(1, CHUNK_SIZE / Spacing);
const int32 CellOriginVX = Cell.X * CHUNK_SIZE;
const int32 CellOriginVY = Cell.Y * CHUNK_SIZE;
const auto FeatureQueries = bUseFeatureQueries ? VF_ContentFeatureQueries(
Ctx.bNativeSurface ? Ctx.NativeSurface.Get() : nullptr,
Ctx.bNativeFloatingIslands ? Ctx.NativeIslands.Get() : nullptr,Ctx.StrateIndex,
CellOriginVX-1.0f,CellOriginVY-1.0f,Ctx.BottomVoxelZ-1.0f,
CellOriginVX+CHUNK_SIZE+1.0f,CellOriginVY+CHUNK_SIZE+1.0f,Ctx.TopVoxelZ+1.0f)
: std::vector<VoxelForgeCore::WorldAssemblyFeatureQueryV1>{};
const VoxelForgeCore::ScopedWorldAssemblyFeatureQueriesV1 FeatureScope(FeatureQueries);
TArray<int32> EntryCount; EntryCount.Init(0, Entries.Num());
int32 TotalActors = 0;
// Per-entry slope-gate cosines, hoisted out of PlaceAtCrossing (they were recomputed per crossing
// × entry). Same cos of the same angle → bit-identical gating. Sentinel < 0 = gate disabled
// (default angles), so the common case still costs no trig and never rejects.
TArray<float> CosMaxSlope, CosMinSlope;
CosMaxSlope.SetNumUninitialized(Entries.Num());
CosMinSlope.SetNumUninitialized(Entries.Num());
for (int32 e = 0; e < Entries.Num(); ++e)
{
CosMaxSlope[e] = (Entries[e].Profile.MaxSlopeAngle < 89.99f)
? VoxelMath::DetCos(FMath::DegreesToRadians(Entries[e].Profile.MaxSlopeAngle)) : -1.0f;
CosMinSlope[e] = (Entries[e].Profile.MinSlopeAngle > 0.01f)
? VoxelMath::DetCos(FMath::DegreesToRadians(Entries[e].Profile.MinSlopeAngle)) : -1.0f;
}
// Per-COLUMN biome cache: ResolveBiomeSampleAt's noise-heavy cell classification is box-validated
// (one rebuild per chunk footprint), so resolving the dominant biome at every column in this cell is
// cheap. The cache is local to this worker task (determinism-safe — pure function of XY/seed/Ctx).
FChunkBiomeCache BiomeCache;
const bool bHasBiomes = Ctx.BiomeCtx.IsValid();
auto D = [&](float VX, float VY, float VZ)
{
if (OutCounters != nullptr) ++OutCounters->DensityQueries;
if (Ctx.bNativeFloatingIslands && Ctx.NativeIslands.IsValid())
{
float NativeDensity = 1.0f;
if (Ctx.NativeIslands->GetDensity(Ctx.StrateIndex, VX, VY, VZ, NativeDensity))
return NativeDensity;
return 1.0f;
}
return Gen->GetDensityAt(VX * Ctx.ActiveToOwnerVoxelScale,
VY * Ctx.ActiveToOwnerVoxelScale, VZ * Ctx.ActiveToOwnerVoxelScale);
};
auto GetSurface = [&](float VX, float VY, float& OutGround, float& OutCap)
{
if (OutCounters != nullptr) ++OutCounters->SurfaceQueries;
if (Ctx.bNativeSurface && Ctx.NativeSurface.IsValid())
{
return Ctx.NativeSurface->GetHeights(
Ctx.StrateIndex, VX, VY, OutGround, OutCap);
}
float OwnerGround = 0.0f;
float OwnerCap = 0.0f;
if (!Gen->GetSurfaceHeightAt(VX * Ctx.ActiveToOwnerVoxelScale,
VY * Ctx.ActiveToOwnerVoxelScale, Ctx.RepChunkZ, OwnerGround, OwnerCap))
{
return false;
}
OutGround = OwnerGround / Ctx.ActiveToOwnerVoxelScale;
OutCap = OwnerCap / Ctx.ActiveToOwnerVoxelScale;
return true;
};
// Build a companion satellite's transform from its profile + per-instance hash at a snapped surface point
// (voxel XY/Z + outward normal). Shared by level-1 and level-2 companions.
auto MakeCompanionXf = [&](const FPlacementProfile& P, uint32 IHash,
float sVX, float sVY, float sZ, const FVector& sN) -> FTransform
{
const FVector Local(sVX * Ctx.VoxelSizeCm, sVY * Ctx.VoxelSizeCm, sZ * Ctx.VoxelSizeCm);
const FVector Pos = OwnerXf.TransformPosition(Local) + sN * P.SurfaceOffset + P.LocationOffset;
FQuat Q = P.bAlignToSurface ? FRotationMatrix::MakeFromZ(sN).ToQuat() : FQuat::Identity;
Q = Q * P.RotationOffset.Quaternion();
if (!P.RandomRotation.IsNearlyZero())
{
const float rp = (VoxelHash::ToFloat01(VoxelHash::Mix(IHash ^ 0x1111A1u)) - 0.5f) * P.RandomRotation.Pitch;
const float ry = (VoxelHash::ToFloat01(VoxelHash::Mix(IHash ^ 0x2222B2u)) - 0.5f) * P.RandomRotation.Yaw;
const float rr = (VoxelHash::ToFloat01(VoxelHash::Mix(IHash ^ 0x3333C3u)) - 0.5f) * P.RandomRotation.Roll;
Q = Q * FRotator(rp, ry, rr).Quaternion();
}
const float Sc = FMath::Lerp(P.MinScale, P.MaxScale, VoxelHash::ToFloat01(VoxelHash::Mix(IHash ^ 0x5CA1E000u)));
return FTransform(Q, Pos, FVector(Sc));
};
// Shared: roll every decoration entry at one surface point (voxel XY, voxel Z, outward world normal)
// and append the passing ones to OutSpawns. CrossingIdx salts the hash so stacked surfaces differ.
// ColBiome = the column's dominant context-biome index (-1 when biomes are off); an entry is rolled
// only if it belongs to that biome (EntryBiome[EntryIdx] == ColBiome) → organic, per-column borders.
auto PlaceAtCrossing = [&](float VX, float VY, int32 gx, int32 gy, float ZC,
const FVector& NormalWorld, int32 CrossingIdx, int32 ColBiome)
{
const bool bFloor = NormalWorld.Z > 0.5f;
const bool bCeiling = NormalWorld.Z < -0.5f;
const bool bWall = !bFloor && !bCeiling;
const FVector LocalPos(VX * Ctx.VoxelSizeCm, VY * Ctx.VoxelSizeCm, ZC * Ctx.VoxelSizeCm);
const FVector PosWorld = OwnerXf.TransformPosition(LocalPos);
const bool bBelowWater = Ctx.bHasWater && (LocalPos.Z < Ctx.WaterLocalZ);
for (int32 EntryIdx = 0; EntryIdx < Entries.Num(); ++EntryIdx)
{
// Per-column biome gate: only this column's dominant biome owns its entries. -1-tagged
// entries (no biome field) match the -1 ColBiome, so the legacy single-list path is intact.
if (EntryBiome[EntryIdx] != ColBiome) continue;
const FStrateDecoration& Deco = Entries[EntryIdx];
const bool bInstanced = (Deco.Profile.InstancedMesh != nullptr);
if (!bInstanced && !Deco.Profile.ActorClass) continue;
if (Deco.SpawnDensity <= 0.0f) continue;
if (EntryCount[EntryIdx] >= Deco.MaxPerChunk) continue;
if (!bInstanced && TotalActors >= GMaxDecorationActorsPerCell) continue;
bool bMatches = true;
switch (Deco.Profile.SurfacePlacement)
{
case ESurfaceType::Floor: bMatches = bFloor; break;
case ESurfaceType::Wall: bMatches = bWall; break;
case ESurfaceType::Ceiling: bMatches = bCeiling; break;
default: bMatches = true; break;
}
if (!bMatches) continue;
// Strict-wall overhang gate: a "wall" point also covers surfaces that lean slightly downward
// (N.Z in [-0.5, 0)). For props flagged wall-only-upright, drop those so overhangs don't take
// wall decals. Applies whenever the point IS a wall (independent of Floor/Wall/Any setting).
if (bWall && Deco.Profile.bWallExcludeOverhangs && NormalWorld.Z < 0.0f) continue;
// Surface-tilt gates: tilt = acos(|N.Z|) (0 = flat, 90 = vertical). |N.Z| < cos(MaxSlope) ⇔
// tilt > MaxSlope (skip steeper); |N.Z| > cos(MinSlope) ⇔ tilt < MinSlope (skip flatter).
// Cosines are precomputed per entry above; < 0 = gate disabled (default angles).
if (CosMaxSlope[EntryIdx] >= 0.0f && FMath::Abs(NormalWorld.Z) < CosMaxSlope[EntryIdx]) continue;
if (CosMinSlope[EntryIdx] >= 0.0f && FMath::Abs(NormalWorld.Z) > CosMinSlope[EntryIdx]) continue;
const uint32 H = DecoHash(Cell.X, Cell.Y, gx, gy, CrossingIdx, EntryIdx, InSeed, 0xDEC0u);
if (VoxelHash::ToFloat01(H) > Deco.SpawnDensity) continue;
if (Deco.Profile.bRequireWaterRelative && Ctx.bHasWater)
{
if (bBelowWater != Deco.Profile.bPlaceBelowWater) continue;
}
// F7 aware placement: relational conditions (relief/moisture/biome-border). Opt-in per entry —
// skipped entirely when the list is empty. Worker-safe pure query; uses the cell's biome context.
if (Deco.Profile.Conditions.Num() > 0 &&
!Gen->EvaluateTerrainConditions(Deco.Profile.Conditions,
VX * Ctx.ActiveToOwnerVoxelScale, VY * Ctx.ActiveToOwnerVoxelScale,
Ctx.BiomeCtx)) continue;
const FVector SpawnPos = PosWorld + NormalWorld * Deco.Profile.SurfaceOffset + Deco.Profile.LocationOffset;
// Rotation: optional surface-align → fixed offset → per-axis hash random (same model as landmarks).
// RandomRotation.Yaw defaults to 360 for decoration (see FStrateDecoration ctor) = full random
// heading, reproducing the legacy random-yaw look; the exact per-instance yaw values reshuffle
// once (different hash mix) but the distribution is identical.
FQuat BaseQ = Deco.Profile.bAlignToSurface
? FRotationMatrix::MakeFromZ(NormalWorld).ToQuat()
: FQuat::Identity;
BaseQ = BaseQ * Deco.Profile.RotationOffset.Quaternion();
if (!Deco.Profile.RandomRotation.IsNearlyZero())
{
const float rp = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x1111A1u)) - 0.5f) * Deco.Profile.RandomRotation.Pitch;
const float ry = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x2222B2u)) - 0.5f) * Deco.Profile.RandomRotation.Yaw;
const float rr = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x3333C3u)) - 0.5f) * Deco.Profile.RandomRotation.Roll;
BaseQ = BaseQ * FRotator(rp, ry, rr).Quaternion();
}
const float ScaleT = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x5CA1E000u));
const float Scale = FMath::Lerp(Deco.Profile.MinScale, Deco.Profile.MaxScale, ScaleT);
FDecoSpawn& Out = OutSpawns.AddDefaulted_GetRef();
Out.EntryIdx = EntryIdx;
Out.bInstanced = bInstanced;
Out.Xf = FTransform(BaseQ, SpawnPos, FVector(Scale));
++EntryCount[EntryIdx];
if (!bInstanced) ++TotalActors;
// ---- Companions (F7 relational placement): deterministic cluster satellites around this parent.
// Level 1 re-snaps to the real surface at its own XY; level-2 SubCompanions inherit their L1
// satellite's point (no re-snap → cheap). A per-parent budget caps the total so nesting can't blow
// up. Pure function of the parent hash H → no "did a tree spawn here?" search.
int32 CompBudget = GMaxCompanionsPerParent;
for (int32 ci = 0; ci < Deco.Companions.Num() && CompBudget > 0; ++ci)
{
const FDecoCompanion& Comp = Deco.Companions[ci];
const bool bCompInst = (Comp.Profile.InstancedMesh != nullptr);
if (!bCompInst && !Comp.Profile.ActorClass) continue;
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
const int64 Span = FMath::Max<int64>(0,
static_cast<int64>(Comp.CountMax) - Comp.CountMin);
const uint64 CountRange = static_cast<uint64>(Span) + 1u;
const float CountOffset = VoxelHash::ToFloat01(
VoxelHash::Mix(CH ^ 0x1234u)) * static_cast<float>(CountRange);
const int64 CountWide = static_cast<int64>(Comp.CountMin)
+ static_cast<int64>(CountOffset);
const int32 Count = static_cast<int32>(FMath::Clamp<int64>(
CountWide, MIN_int32, MAX_int32));
for (int32 ii = 0; ii < Count; ++ii)
{
if (CompBudget <= 0) break;
if (!bCompInst && TotalActors >= GMaxDecorationActorsPerCell) break; // actor budget
const uint32 IH = VoxelHash::Mix(CH ^ ((uint32)ii * 0x85EBCA77u + 0x2545F491u));
const float Ang = VoxelHash::ToFloat01(IH) * 2.0f * PI;
const float RadVox = FMath::Lerp(Comp.RadiusMinVox, Comp.RadiusMaxVox,
VoxelHash::ToFloat01(VoxelHash::Mix(IH ^ 0x77u)));
float SinAng = 0.0f, CosAng = 0.0f;
VoxelMath::DetSinCos(SinAng, CosAng, Ang);
const float SatVX = VX + CosAng * RadVox; // voxel, actor-local
const float SatVY = VY + SinAng * RadVox;
// Optional per-satellite gating (relief/moisture/biome-border at ITS own XY).
if (Comp.Profile.Conditions.Num() > 0 &&
!Gen->EvaluateTerrainConditions(Comp.Profile.Conditions,
SatVX * Ctx.ActiveToOwnerVoxelScale,
SatVY * Ctx.ActiveToOwnerVoxelScale, Ctx.BiomeCtx)) continue;
// Surface: SNAP to the real ground/ceiling at the satellite XY (kills floaters); or inherit.
float SatZ; FVector SatN;
if (Comp.bSnapToSurface)
{
if (!FindLandmarkColumn(Gen, OwnerXf, Ctx, SatVX, SatVY, Comp.Profile.SurfacePlacement,
Step, ColumnDepth, SatZ, SatN)) continue; // no surface → no floater
}
else { SatZ = ZC; SatN = NormalWorld; }
FDecoSpawn& CO = OutSpawns.AddDefaulted_GetRef();
CO.EntryIdx = EntryIdx;
CO.CompanionIdx = ci;
CO.bInstanced = bCompInst;
CO.Xf = MakeCompanionXf(Comp.Profile, IH, SatVX, SatVY, SatZ, SatN);
if (!bCompInst) ++TotalActors;
--CompBudget;
// ---- Level 2: SubCompanions ON this satellite (moss on a rock). Inherit the L1 satellite's
// snapped point (SatZ, SatN) — no re-snap, so nesting stays cheap. Small disk around it.
for (int32 si = 0; si < Comp.SubCompanions.Num() && CompBudget > 0; ++si)
{
const FDecoSubCompanion& Sub = Comp.SubCompanions[si];
const bool bSubInst = (Sub.Profile.InstancedMesh != nullptr);
if (!bSubInst && !Sub.Profile.ActorClass) continue;
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
const int64 SubSpan = FMath::Max<int64>(0,
static_cast<int64>(Sub.CountMax) - Sub.CountMin);
const uint64 SubCountRange = static_cast<uint64>(SubSpan) + 1u;
const float SubCountOffset = VoxelHash::ToFloat01(
VoxelHash::Mix(SH ^ 0x1234u)) * static_cast<float>(SubCountRange);
const int64 SubCountWide = static_cast<int64>(Sub.CountMin)
+ static_cast<int64>(SubCountOffset);
const int32 SubCount = static_cast<int32>(FMath::Clamp<int64>(
SubCountWide, MIN_int32, MAX_int32));
for (int32 sj = 0; sj < SubCount; ++sj)
{
if (CompBudget <= 0) break;
if (!bSubInst && TotalActors >= GMaxDecorationActorsPerCell) break;
const uint32 JH = VoxelHash::Mix(SH ^ ((uint32)sj * 0x85EBCA77u + 0x165667B1u));
const float SAng = VoxelHash::ToFloat01(JH) * 2.0f * PI;
const float SRadVox = FMath::Lerp(Sub.RadiusMinVox, Sub.RadiusMaxVox,
VoxelHash::ToFloat01(VoxelHash::Mix(JH ^ 0x77u)));
float SinSAng = 0.0f, CosSAng = 0.0f;
VoxelMath::DetSinCos(SinSAng, CosSAng, SAng);
const float SubVX = SatVX + CosSAng * SRadVox;
const float SubVY = SatVY + SinSAng * SRadVox;
if (Sub.Profile.Conditions.Num() > 0 &&
!Gen->EvaluateTerrainConditions(Sub.Profile.Conditions,
SubVX * Ctx.ActiveToOwnerVoxelScale,
SubVY * Ctx.ActiveToOwnerVoxelScale, Ctx.BiomeCtx)) continue;
FDecoSpawn& SO = OutSpawns.AddDefaulted_GetRef();
SO.EntryIdx = EntryIdx;
SO.CompanionIdx = ci;
SO.SubIdx = si;
SO.bInstanced = bSubInst;
SO.Xf = MakeCompanionXf(Sub.Profile, JH, SubVX, SubVY, SatZ, SatN);
if (!bSubInst) ++TotalActors;
--CompBudget;
}
}
}
}
}
};
for (int32 gy = 0; gy < PerAxis; ++gy)
for (int32 gx = 0; gx < PerAxis; ++gx)
{
if (OutCounters != nullptr) ++OutCounters->Columns;
// INTEGER active-voxel jitter: native Core and legacy routes both stay on their field's column grid.
// Gigue entière en voxels actifs : les routes Core et legacy restent sur la grille de colonnes du champ.
const uint32 HJ = DecoHash(Cell.X, Cell.Y, gx, gy, -1, 0, InSeed, 0x10C0u);
const int32 JX = FMath::Min(Spacing - 1, (int32)(VoxelHash::ToFloat01(HJ) * (float)Spacing));
const int32 JY = FMath::Min(Spacing - 1, (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(HJ ^ 0x68BC21EBu)) * (float)Spacing));
const float VX = (float)(CellOriginVX + gx * Spacing + JX);
const float VY = (float)(CellOriginVY + gy * Spacing + JY);
// Resolve the column's biome ONCE (constant over the column's whole Z range). -1 when the strate
// has no biome field → matches the -1-tagged legacy entries.
int32 ColBiome = -1;
if (bHasBiomes)
{
const FBiomeSample BS = Gen->ResolveBiomeSampleAt(
VX * Ctx.ActiveToOwnerVoxelScale, VY * Ctx.ActiveToOwnerVoxelScale,
Ctx.RepChunkZ, Ctx.BiomeCtx, BiomeCache);
ColBiome = BS.DominantIndex; // index into Ctx.BiomeCtx.Biomes == the entry tag
// SOFTEN THE BORDER: a hard dominant pick still switches deco sets on a crisp line. In the
// blend band (NeighborWeight rises 0 → ~0.5 toward the shared border) flip a hash-decided
// fraction of columns to the NEIGHBOUR biome, so the two deco sets DITHER across the seam
// instead of snapping. Deterministic (pure hash of cell/column/seed) → no flicker/perf cost.
if (BS.NeighborWeight > 0.0f && BS.NeighborIndex >= 0)
{
const uint32 HBlend = DecoHash(Cell.X, Cell.Y, gx, gy, -7, 0, InSeed, 0xB1E2u);
if (VoxelHash::ToFloat01(HBlend) < BS.NeighborWeight) { ColBiome = BS.NeighborIndex; }
}
}
if (Ctx.bSurfaceWorld)
{
// HEIGHTFIELD ORACLE — O(1)/column instead of marching the whole band. Query the surface
// (+ 4 neighbours for the gradient normals), then verify with ONE density sample so we skip
// columns carved away by passages / the (0,0) spine / player diffs (the oracle is the raw
// heightfield and doesn't know about carving).
float hC, cC, hXp, cXp, hXm, cXm, hYp, cYp, hYm, cYm;
if (!GetSurface(VX, VY, hC, cC)) continue;
GetSurface(VX + 1.0f, VY, hXp, cXp);
GetSurface(VX - 1.0f, VY, hXm, cXm);
GetSurface(VX, VY + 1.0f, hYp, cYp);
GetSurface(VX, VY - 1.0f, hYm, cYm);
// Floor (terrain top): outward normal = (-dH/dx, -dH/dy, 1).
if (hC >= Ctx.BottomVoxelZ && hC <= Ctx.TopVoxelZ
&& (Ctx.bNativeSurface || D(VX, VY, hC) <= 0.5f))
{
const float dHdx = (hXp - hXm) * 0.5f;
const float dHdy = (hYp - hYm) * 0.5f;
FVector N = OwnerXf.TransformVectorNoScale(FVector(-dHdx, -dHdy, 1.0f)).GetSafeNormal();
if (N.IsNearlyZero()) N = FVector::UpVector;
PlaceAtCrossing(VX, VY, gx, gy, hC, N, 0, ColBiome);
}
// Sky-cap ceiling underside: outward normal = (dC/dx, dC/dy, -1). Only if open space below.
if (cC > hC + 1.0f && cC <= Ctx.TopVoxelZ
&& (Ctx.bNativeSurface || D(VX, VY, cC) <= 0.5f))
{
const float dCdx = (cXp - cXm) * 0.5f;
const float dCdy = (cYp - cYm) * 0.5f;
FVector N = OwnerXf.TransformVectorNoScale(FVector(dCdx, dCdy, -1.0f)).GetSafeNormal();
if (N.IsNearlyZero()) N = FVector::DownVector;
PlaceAtCrossing(VX, VY, gx, gy, cC, N, 1, ColBiome);
}
continue;
}
// NON-SURFACE archetypes (caves/shafts/islands): ray-march the density column for crossings.
float MarchTop = Ctx.TopVoxelZ, MarchBottom = Ctx.BottomVoxelZ;
if (bUseFeatureQueries && Ctx.bNativeFloatingIslands && Ctx.NativeIslands.IsValid())
if (const auto* Entry = Ctx.NativeIslands->FindField(Ctx.StrateIndex))
if (!VF_ConstrainContentMarch(Entry->SolidSupportBounds,VX,VY,Step,MarchTop,MarchBottom))
continue;
float PrevD = D(VX, VY, MarchTop);
int32 Crossings = 0;
bool bSeenAir = (PrevD >= 0.0f); // GetDensityAt >= 0 == air
float SolidRun = 0.0f; // contiguous solid voxels since the last open air
for (float Z = MarchTop - Step; Z >= MarchBottom && Crossings < MaxCrossings; Z -= Step)
{
const float Dz = D(VX, VY, Z);
if ((PrevD >= 0.0f) != (Dz >= 0.0f)) // straddles IsoLevel 0 (air ↔ solid)
{
// Bisection-refine the crossing Z between Z (lo, Dz) and Z+Step (hi, PrevD).
float ZLo = Z, DLo = Dz, ZHi = Z + Step, DHi = PrevD;
for (int32 It = 0; It < 4; ++It)
{
const float ZM = 0.5f * (ZLo + ZHi);
const float DM = D(VX, VY, ZM);
if ((DM >= 0.0f) == (DHi >= 0.0f)) { ZHi = ZM; DHi = DM; }
else { ZLo = ZM; DLo = DM; }
}
const float Denom = (DLo - DHi);
const float T = (FMath::Abs(Denom) > KINDA_SMALL_NUMBER) ? (DLo / Denom) : 0.5f;
const float ZC = ZLo + (ZHi - ZLo) * T;
const FVector LocalGrad(
D(VX + 1.0f, VY, ZC) - D(VX - 1.0f, VY, ZC),
D(VX, VY + 1.0f, ZC) - D(VX, VY - 1.0f, ZC),
D(VX, VY, ZC + 1.0f) - D(VX, VY, ZC - 1.0f));
FVector NormalWorld = OwnerXf.TransformVectorNoScale(LocalGrad).GetSafeNormal();
if (NormalWorld.IsNearlyZero()) NormalWorld = FVector::UpVector;
PlaceAtCrossing(VX, VY, gx, gy, ZC, NormalWorld, Crossings, ColBiome);
++Crossings;
}
// Once past into the open space, stop after a long bedrock run below it (skips solid rock to
// the strate floor; caves reset on each air gap so layered floors are still found).
if (Dz >= 0.0f) { bSeenAir = true; SolidRun = 0.0f; }
else { SolidRun += Step; }
if (bSeenAir && SolidRun > ColumnDepth) break;
PrevD = Dz;
}
}
}
// ---- GAME THREAD: drain finished marches → merge into region builds, apply completed regions budgeted. ----
void UVoxelContentManager::ProcessDecoResults(const FIntPoint& PlayerCell)
{
// Drain every finished cell march and route it to its grid by Result.Grid, folding it into that grid's
// region build. Merging is cheap (transform appends) so it isn't budgeted; the expensive HISM build is
// budgeted below at region granularity.
FDecoCellResult R;
while (DecoResults.Dequeue(R))
{
FDecoGrid& G = (R.Grid == EDecoStreamTier::Near) ? NearGrid : FarGrid;
G.InFlightCells.Remove(R.Cell); // free the concurrency slot regardless of whether it still matters
MergeCellResult(G, R);
}
// Apply completed regions across BOTH grids under ONE shared frame budget (one batched HISM-per-mesh
// build per region). A region whose build finished but is no longer desired (player moved on while it
// marched) is discarded instead of applied — keeps an out-of-range region from flashing in for a frame.
const int32 R_ = RegionSize();
const int32 Budget = FMath::Max(1, Settings->MaxDecorationCellsPerFrame);
int32 Applied = 0;
for (FDecoGrid* GP : { &NearGrid, &FarGrid })
{
FDecoGrid& G = *GP;
G.Completed.Sort([](const FIntPoint& A, const FIntPoint& B)
{
return (A.Y != B.Y) ? (A.Y < B.Y) : (A.X < B.X);
});
while (G.Completed.Num() > 0 && Applied < Budget)
{
const FIntPoint Region = G.Completed[0];
G.Completed.RemoveAt(0);
FDecoRegionBuild* Build = G.Builds.Find(Region);
if (!Build) continue; // already cleared
if (!IsRegionDesired(Region, PlayerCell, G.Radius, R_))
{
G.Builds.Remove(Region); // wandered out of range while building → drop it unbuilt
continue;
}
ApplyRegion(G, Region, *Build);
G.Builds.Remove(Region);
++Applied;
}
if (Applied >= Budget) break;
}
}
// Fold one finished cell's spawns into its region build, then mark the cell accounted for. A result whose
// region build is gone or whose BuildId no longer matches (region was cleared + re-marched) is discarded.
void UVoxelContentManager::MergeCellResult(FDecoGrid& G, const FDecoCellResult& Result)
{
const FIntPoint Region = CellToRegion(Result.Cell, RegionSize());
FDecoRegionBuild* Build = G.Builds.Find(Region);
if (!Build || Build->BuildId != Result.BuildId)
{
return;
}
// Already counted this cell (a duplicate task for the same cell+build landed first) → drop this
// result whole, or we'd append its spawns twice (double decorations at the same spots).
if (Build->AccountedCells.Contains(Result.Cell))
{
return;
}
for (const FDecoSpawn& S : Result.Spawns)
{
if (!Result.Entries.IsValidIndex(S.EntryIdx)) continue;
const FStrateDecoration& Deco = Result.Entries[S.EntryIdx];
// Resolve which profile owns this spawn: the entry itself (CompanionIdx<0), a level-1 companion, or a
// level-2 sub-companion (F7). Each carries its own mesh/actor + render tuning.
const FPlacementProfile* ProfPtr = &Deco.Profile;
if (S.CompanionIdx >= 0 && Deco.Companions.IsValidIndex(S.CompanionIdx))
{
const FDecoCompanion& Comp = Deco.Companions[S.CompanionIdx];
ProfPtr = (S.SubIdx >= 0 && Comp.SubCompanions.IsValidIndex(S.SubIdx))
? &Comp.SubCompanions[S.SubIdx].Profile : &Comp.Profile;
}
const FPlacementProfile& Prof = *ProfPtr;
if (S.bInstanced)
{
if (!Prof.InstancedMesh) continue;
// Bucket by MESH so cells (and biomes) sharing a mesh collapse into one region HISM. The first
// contributor in deterministic source order sets the render tuning (cull/shadow/scale) for the
// whole region's instances. Worker completion order must not choose that representative.
FRegionMeshBucket& Bucket = Build->MeshBuckets.FindOrAdd(Prof.InstancedMesh);
const bool bEarlierSource =
S.EntryIdx < Bucket.SourceEntryIdx
|| (S.EntryIdx == Bucket.SourceEntryIdx && S.CompanionIdx < Bucket.SourceCompanionIdx)
|| (S.EntryIdx == Bucket.SourceEntryIdx && S.CompanionIdx == Bucket.SourceCompanionIdx
&& S.SubIdx < Bucket.SourceSubIdx);
if (Bucket.Xforms.Num() == 0 || bEarlierSource)
{
Bucket.Profile = Prof;
Bucket.SourceEntryIdx = S.EntryIdx;
Bucket.SourceCompanionIdx = S.CompanionIdx;
Bucket.SourceSubIdx = S.SubIdx;
}
Bucket.Xforms.Add(S.Xf);
}
else if (Prof.ActorClass)
{
FRegionActorSpawn& A = Build->ActorSpawns.AddDefaulted_GetRef();
A.ActorClass = Prof.ActorClass;
A.Xf = S.Xf;
}
}
MarkCellDone(G, Region, Result.Cell, Result.BuildId);
}
// Account one cell against its region — IDEMPOTENT per cell, so a duplicate task for the same cell can't
// double-decrement and apply the region early (which left a permanently-empty chunk until a regen). Queues
// the region for apply once every distinct cell has reported.
void UVoxelContentManager::MarkCellDone(FDecoGrid& G, const FIntPoint& Region, const FIntPoint& Cell, uint32 BuildId)
{
FDecoRegionBuild* Build = G.Builds.Find(Region);
if (!Build || Build->BuildId != BuildId) return;
bool bAlreadyAccounted = false;
Build->AccountedCells.Add(Cell, &bAlreadyAccounted);
if (bAlreadyAccounted) return; // this cell already counted → ignore the duplicate
if (--Build->CellsRemaining <= 0)
{
G.Completed.Add(Region); // ready for budgeted apply in ProcessDecoResults
}
}
// Build the region's components: one HISM per mesh (all cells merged → one batched AddInstances), actors
// spawned inline. Moves the region into G.Regions; the build is removed by the caller.
void UVoxelContentManager::ApplyRegion(FDecoGrid& G, const FIntPoint& Region, FDecoRegionBuild& Build,
bool bApplyLandmarkFootprints)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_DecoApply); // total game-thread cost to apply one region
AActor* OwnerActor = Owner.Get();
if (!OwnerActor) return;
UWorld* World = OwnerActor->GetWorld();
if (!World) return;
FDecoRegionContent& Content = G.Regions.Add(Region);
Build.ActorSpawns.Sort([](const FRegionActorSpawn& A, const FRegionActorSpawn& B)
{
const FString AClass = A.ActorClass ? A.ActorClass->GetPathName() : FString();
const FString BClass = B.ActorClass ? B.ActorClass->GetPathName() : FString();
const int32 ClassCompare = FCString::Strcmp(*AClass, *BClass);
if (ClassCompare != 0) return ClassCompare < 0;
return VF_DecoTransformLess(A.Xf, B.Xf);
});
// Non-instanced actors — spawn each (no batch path). Decorations live only in the player's strate
// (the march is strate-bounded), so their lights are always legitimately visible — no extra culling.
for (const FRegionActorSpawn& A : Build.ActorSpawns)
{
if (!A.ActorClass) continue;
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = OwnerActor;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
if (AActor* NewActor = World->SpawnActor<AActor>(A.ActorClass, A.Xf, SpawnParams))
{
Content.Actors.Add(NewActor);
}
}
// One HISM per mesh for the ENTIRE region — the render-thread win: hundreds of per-cell components
// collapse to a handful per region, so InitViews walks far fewer primitives every frame.
TArray<TWeakObjectPtr<UStaticMesh>> MeshKeys;
Build.MeshBuckets.GetKeys(MeshKeys);
MeshKeys.Sort([](const TWeakObjectPtr<UStaticMesh>& A, const TWeakObjectPtr<UStaticMesh>& B)
{
const UStaticMesh* AMesh = A.Get();
const UStaticMesh* BMesh = B.Get();
const FString APath = AMesh ? AMesh->GetPathName() : FString();
const FString BPath = BMesh ? BMesh->GetPathName() : FString();
return FCString::Strcmp(*APath, *BPath) < 0;
});
for (const TWeakObjectPtr<UStaticMesh>& MeshKey : MeshKeys)
{
FRegionMeshBucket* BucketPtr = Build.MeshBuckets.Find(MeshKey);
if (!BucketPtr) continue;
FRegionMeshBucket& Bucket = *BucketPtr;
UStaticMesh* Mesh = MeshKey.Get();
if (!Mesh || Bucket.Xforms.Num() == 0) continue;
const FPlacementProfile& Prof = Bucket.Profile;
Bucket.Xforms.Sort(VF_DecoTransformLess);
UHierarchicalInstancedStaticMeshComponent* HISM =
NewObject<UHierarchicalInstancedStaticMeshComponent>(OwnerActor);
HISM->SetStaticMesh(Mesh);
// STATIC, not Movable: decorations are placed once and never move, so Static lets the renderer
// CACHE their mesh draw commands (they drop out of the per-frame dynamic-primitive gather — the
// dominant render-thread cost once component count was solved) AND lets VSM cache their shadows.
// Mirrors the terrain tile + root mobility rationale (VoxelWorld.cpp). Must precede RegisterComponent.
HISM->SetMobility(EComponentMobility::Static);
HISM->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Per-entry render tuning — what makes dense groundcover affordable. Set BEFORE RegisterComponent
// so the render proxy is created once with the final state (no rebuild):
// • CullDistance bounds GPU cost — grass is drawn only near the player even when placed thickly.
// • bCastShadow off removes the dominant cost of dense instanced foliage.
HISM->SetCastShadow(Prof.bCastShadow);
if (Prof.CullDistance > 0.0f)
{
const int32 End = FMath::Max(1, (int32)Prof.CullDistance);
const int32 Start = FMath::Max(1, (int32)(Prof.CullDistance * 0.8f));
HISM->SetCullDistances(Start, End); // fade band 0.8x→1.0x, then gone
}
{
// Scene-proxy creation. Now amortised over a whole region rather than churned per cell.
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_DecoHISMRegister);
HISM->RegisterComponent();
HISM->AttachToComponent(OwnerActor->GetRootComponent(),
FAttachmentTransformRules::KeepRelativeTransform);
}
{
// Cluster-tree build over the WHOLE region's instances in one shot (scales with instance
// count, but built once per region instead of once per cell).
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_DecoAddInstances);
HISM->AddInstances(Bucket.Xforms, /*bShouldReturnIndices=*/false, /*bWorldSpace=*/true);
}
Content.Instances.Add(HISM);
}
// Landmark footprints (F7): a suppressing landmark cleared whatever decorations existed when it spawned,
// but this region just streamed in FRESH → re-clear under any loaded suppressing landmark overlapping it
// (the per-HISM spatial query early-outs for the non-overlapping majority). Keeps temple floors clear as
// you leave and return.
if (bApplyLandmarkFootprints && Content.Instances.Num() > 0)
{
for (const TPair<FIntVector, FLandmarkInstance>& LP : LandmarkInstances)
{
if (LP.Value.SuppressRadiusWorld > 0.0f)
{
RemoveInstancesInContent(Content, LP.Value.SuppressCenter, LP.Value.SuppressRadiusWorld);
}
}
}
}
void UVoxelContentManager::ClearDecorationRegion(FDecoGrid& G, const FIntPoint& Region)
{
FDecoRegionContent* Content = G.Regions.Find(Region);
if (!Content) return;
for (const TWeakObjectPtr<AActor>& A : Content->Actors)
{
if (AActor* Act = A.Get()) { Act->Destroy(); }
}
for (const TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>& C : Content->Instances)
{
if (UHierarchicalInstancedStaticMeshComponent* Comp = C.Get()) { Comp->DestroyComponent(); }
}
G.Regions.Remove(Region);
}
void UVoxelContentManager::ClearAllDecorations()
{
for (FDecoGrid* GP : { &NearGrid, &FarGrid })
{
FDecoGrid& G = *GP;
TArray<FIntPoint> Keys; G.Regions.GetKeys(Keys);
for (const FIntPoint& K : Keys) ClearDecorationRegion(G, K);
ResetGridBuildState(G); // abandon any in-progress builds
}
// Drain any results already enqueued by in-flight tasks. No epoch bump needed: their BuildIds are now
// gone from the grids' Builds, so any straggler result is discarded on merge; new builds get fresh BuildIds.
DrainDecoResults();
LastDecoCell = FIntPoint(INT32_MIN, INT32_MIN);
}
void UVoxelContentManager::InvalidateDecorationBuilds()
{
// A terrain edit changes the worker's surface oracle. Clear both the applied regions and the
// in-progress build maps as one operation; any late result then fails the BuildId lookup and cannot
// resurrect pre-edit content. The next UpdateDecorations rebuilds the desired set even if the player
// has not moved.
ClearAllDecorations();
}
//=============================================================================
// LANDMARKS — rare large objects on a coarse hash lattice (the "mini-suns")
//=============================================================================
// Cost scales with the NUMBER of landmarks in range, not the area: cell = SpacingChunks chunks, so a huge
// StreamRadiusChunks is only ~(radius/spacing)² candidates. Placement is synchronous (so few candidates it
// never hitches) and deterministic (hash of cell+entry+seed → pop-free). Strate-bounded like decorations.
// Single-column surface find for a landmark. SurfaceWorld → height oracle (floor TerrainZ / ceiling CeilSurf
// by Surf); else ray-march the strate band top-down for the first crossing whose orientation matches Surf.
bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const FTransform& OwnerXf,
const FDecoContext& Ctx, float VX, float VY, ESurfaceType Surf,
float Step, float ColDepth, float& OutZ, FVector& OutNormal)
{
if (!Gen) return false;
const auto FeatureQueries = VF_ContentFeatureQueries(
Ctx.bNativeSurface ? Ctx.NativeSurface.Get() : nullptr,
Ctx.bNativeFloatingIslands ? Ctx.NativeIslands.Get() : nullptr,Ctx.StrateIndex,
VX-1.0f,VY-1.0f,Ctx.BottomVoxelZ-1.0f,VX+1.0f,VY+1.0f,Ctx.TopVoxelZ+1.0f);
const VoxelForgeCore::ScopedWorldAssemblyFeatureQueriesV1 FeatureScope(FeatureQueries);
const auto Density = [&](float SampleX, float SampleY, float SampleZ)
{
if (Ctx.bNativeFloatingIslands && Ctx.NativeIslands.IsValid())
{
float NativeDensity = 1.0f;
if (Ctx.NativeIslands->GetDensity(
Ctx.StrateIndex, SampleX, SampleY, SampleZ, NativeDensity))
{
return NativeDensity;
}
return 1.0f;
}
return Gen->GetDensityAt(SampleX * Ctx.ActiveToOwnerVoxelScale,
SampleY * Ctx.ActiveToOwnerVoxelScale, SampleZ * Ctx.ActiveToOwnerVoxelScale);
};
if (Ctx.bSurfaceWorld)
{
float hC, cC;
auto GetSurface = [&](float SampleX, float SampleY, float& OutGround, float& OutCap)
{
if (Ctx.bNativeSurface && Ctx.NativeSurface.IsValid())
{
return Ctx.NativeSurface->GetHeights(
Ctx.StrateIndex, SampleX, SampleY, OutGround, OutCap);
}
float OwnerGround = 0.0f;
float OwnerCap = 0.0f;
if (!Gen->GetSurfaceHeightAt(SampleX * Ctx.ActiveToOwnerVoxelScale,
SampleY * Ctx.ActiveToOwnerVoxelScale, Ctx.RepChunkZ,
OwnerGround, OwnerCap))
{
return false;
}
OutGround = OwnerGround / Ctx.ActiveToOwnerVoxelScale;
OutCap = OwnerCap / Ctx.ActiveToOwnerVoxelScale;
return true;
};
if (!GetSurface(VX, VY, hC, cC)) return false;
if (Surf == ESurfaceType::Ceiling)
{
if (!(cC > hC + 1.0f && cC <= Ctx.TopVoxelZ)) return false;
float d, cXp, cXm, cYp, cYm;
GetSurface(VX + 1.0f, VY, d, cXp);
GetSurface(VX - 1.0f, VY, d, cXm);
GetSurface(VX, VY + 1.0f, d, cYp);
GetSurface(VX, VY - 1.0f, d, cYm);
FVector N = OwnerXf.TransformVectorNoScale(
FVector((cXp - cXm) * 0.5f, (cYp - cYm) * 0.5f, -1.0f)).GetSafeNormal();
if (N.IsNearlyZero()) N = FVector::DownVector;
OutZ = cC; OutNormal = N; return true;
}
// Floor / Wall / Any → the terrain top.
if (!(hC >= Ctx.BottomVoxelZ && hC <= Ctx.TopVoxelZ)) return false;
if (!Ctx.bNativeSurface && Density(VX, VY, hC) > 0.5f) return false;
float d, hXp, hXm, hYp, hYm;
GetSurface(VX + 1.0f, VY, hXp, d);
GetSurface(VX - 1.0f, VY, hXm, d);
GetSurface(VX, VY + 1.0f, hYp, d);
GetSurface(VX, VY - 1.0f, hYm, d);
FVector N = OwnerXf.TransformVectorNoScale(
FVector(-(hXp - hXm) * 0.5f, -(hYp - hYm) * 0.5f, 1.0f)).GetSafeNormal();
if (N.IsNearlyZero()) N = FVector::UpVector;
OutZ = hC; OutNormal = N; return true;
}
// Cave/shaft/island archetypes: march the column from the top for the first matching crossing.
// Bounded by ColDepth like the decoration march (this runs SYNCHRONOUSLY on the game thread):
// once past open air, a solid run longer than ColDepth means bedrock down to the strate floor —
// stop instead of paying GetDensityAt across the whole remaining band.
float MarchTop = Ctx.TopVoxelZ, MarchBottom = Ctx.BottomVoxelZ;
if (Ctx.bNativeFloatingIslands && Ctx.NativeIslands.IsValid())
if (const auto* Entry = Ctx.NativeIslands->FindField(Ctx.StrateIndex))
if (!VF_ConstrainContentMarch(Entry->SolidSupportBounds,VX,VY,Step,MarchTop,MarchBottom))
return false;
float PrevD = Density(VX, VY, MarchTop);
bool bSeenAir = (PrevD >= 0.0f);
float SolidRun = 0.0f;
for (float Z = MarchTop - Step; Z >= MarchBottom; Z -= Step)
{
const float Dz = Density(VX, VY, Z);
if ((PrevD >= 0.0f) != (Dz >= 0.0f)) // air ↔ solid crossing
{
float ZLo = Z, ZHi = Z + Step, DHi = PrevD, DLo = Dz;
for (int32 It = 0; It < 4; ++It)
{
const float ZM = 0.5f * (ZLo + ZHi);
const float DM = Density(VX, VY, ZM);
if ((DM >= 0.0f) == (DHi >= 0.0f)) { ZHi = ZM; DHi = DM; }
else { ZLo = ZM; DLo = DM; }
}
const float Denom = (DLo - DHi);
const float T = (FMath::Abs(Denom) > KINDA_SMALL_NUMBER) ? (DLo / Denom) : 0.5f;
const float ZC = ZLo + (ZHi - ZLo) * T;
const FVector LocalGrad(
Density(VX + 1.0f, VY, ZC) - Density(VX - 1.0f, VY, ZC),
Density(VX, VY + 1.0f, ZC) - Density(VX, VY - 1.0f, ZC),
Density(VX, VY, ZC + 1.0f) - Density(VX, VY, ZC - 1.0f));
FVector N = OwnerXf.TransformVectorNoScale(LocalGrad).GetSafeNormal();
if (N.IsNearlyZero()) N = FVector::UpVector;
const bool bFloor = N.Z > 0.5f;
const bool bCeiling = N.Z < -0.5f;
const bool bWall = !bFloor && !bCeiling;
const bool bMatch =
(Surf == ESurfaceType::Floor && bFloor) ||
(Surf == ESurfaceType::Ceiling && bCeiling) ||
(Surf == ESurfaceType::Wall && bWall) ||
(Surf == ESurfaceType::Any);
if (bMatch) { OutZ = ZC; OutNormal = N; return true; }
}
if (Dz >= 0.0f) { bSeenAir = true; SolidRun = 0.0f; }
else { SolidRun += Step; }
if (bSeenAir && SolidRun > ColDepth) break; // long bedrock below open space → nothing deeper
PrevD = Dz;
}
return false;
}
bool UVoxelContentManager::SpawnFromProfile(const FPlacementProfile& P, uint32 H, const FDecoContext& Ctx,
const FTransform& OwnerXf, AActor* OwnerActor,
float LocalX, float LocalY, float Step, float ColDepth,
FTransform& OutXf, FLandmarkInstance& Out)
{
if (!Generator) return false;
const float VX = LocalX / Ctx.VoxelSizeCm;
const float VY = LocalY / Ctx.VoxelSizeCm;
// Biome filter (resolved at the candidate XY, same field the density/deco paths use).
if (P.RequiredBiome)
{
const UVoxelBiomeDefinition* Bio = Generator->GetDominantBiomeAt(
VX * Ctx.ActiveToOwnerVoxelScale, VY * Ctx.ActiveToOwnerVoxelScale, Ctx.RepChunkZ);
if (Bio != P.RequiredBiome) return false; // leaves Out empty → evaluated, nothing placed
}
float ZC; FVector N;
if (!FindLandmarkColumn(Generator, OwnerXf, Ctx, VX, VY, P.SurfacePlacement, Step, ColDepth, ZC, N))
return false;
// Surface-tilt gates (acos(|N.Z|); guarded so defaults cost no trig).
if (P.MaxSlopeAngle < 89.99f &&
FMath::Abs(N.Z) < VoxelMath::DetCos(FMath::DegreesToRadians(P.MaxSlopeAngle))) return false;
if (P.MinSlopeAngle > 0.01f &&
FMath::Abs(N.Z) > VoxelMath::DetCos(FMath::DegreesToRadians(P.MinSlopeAngle))) return false;
const FVector LocalPos(LocalX, LocalY, ZC * Ctx.VoxelSizeCm);
if (P.bRequireWaterRelative && Ctx.bHasWater)
{
const bool bBelowWater = (LocalPos.Z < Ctx.WaterLocalZ);
if (bBelowWater != P.bPlaceBelowWater) return false;
}
// F7 aware placement: relational conditions (relief/moisture/biome-border), evaluated at the candidate
// XY. Opt-in — empty list is free. "A monument only on high mesas / near a biome edge" lives here.
if (P.Conditions.Num() > 0 &&
!Generator->EvaluateTerrainConditions(P.Conditions,
VX * Ctx.ActiveToOwnerVoxelScale, VY * Ctx.ActiveToOwnerVoxelScale,
Ctx.BiomeCtx)) return false;
// Rotation: optional surface-align → fixed offset → per-axis hash random.
FQuat Q = P.bAlignToSurface ? FRotationMatrix::MakeFromZ(N).ToQuat() : FQuat::Identity;
Q = Q * P.RotationOffset.Quaternion();
if (!P.RandomRotation.IsNearlyZero())
{
const float rp = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x1111A1u)) - 0.5f) * P.RandomRotation.Pitch;
const float ry = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x2222B2u)) - 0.5f) * P.RandomRotation.Yaw;
const float rr = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x3333C3u)) - 0.5f) * P.RandomRotation.Roll;
Q = Q * FRotator(rp, ry, rr).Quaternion();
}
const float ScaleT = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x5CA1E777u));
const float Scale = FMath::Lerp(P.MinScale, P.MaxScale, ScaleT);
// World-space position + XYZ offset (e.g. +Z lifts a sun off the sky-cap into the cavern).
const FVector WorldPos = OwnerXf.TransformPosition(LocalPos) + P.LocationOffset;
OutXf = FTransform(Q, WorldPos, FVector(Scale));
if (P.ActorClass)
{
UWorld* World = OwnerActor->GetWorld();
if (!World) return false;
FActorSpawnParameters SP;
SP.Owner = OwnerActor;
SP.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
if (AActor* A = World->SpawnActor<AActor>(P.ActorClass, OutXf, SP)) { Out.Actor = A; }
return true;
}
if (P.InstancedMesh)
{
UStaticMeshComponent* C = NewObject<UStaticMeshComponent>(OwnerActor);
C->SetStaticMesh(P.InstancedMesh);
C->SetMobility(EComponentMobility::Static); // placed once, never moves → cached draw + VSM shadow
C->SetCollisionEnabled(ECollisionEnabled::NoCollision);
C->SetCastShadow(P.bCastShadow);
if (P.CullDistance > 0.0f) { C->SetCullDistance(P.CullDistance); } // 0 = never cull (far sun)
C->SetWorldTransform(OutXf);
C->RegisterComponent();
C->AttachToComponent(OwnerActor->GetRootComponent(), FAttachmentTransformRules::KeepWorldTransform);
Out.Component = C;
return true;
}
return false;
}
void UVoxelContentManager::SpawnLandmarkInstance(const FStrateLandmark& L, uint32 H, const FDecoContext& Ctx,
const FTransform& OwnerXf, AActor* OwnerActor,
float LocalX, float LocalY, float Step, float ColDepth,
FLandmarkInstance& Out)
{
FTransform Xf;
if (!SpawnFromProfile(L.Profile, H, Ctx, OwnerXf, OwnerActor, LocalX, LocalY, Step, ColDepth, Xf, Out))
return; // Out stays empty → evaluated, nothing placed
// Mini-sun light orb (landmark-only): record world-space data for the terrain material's raymarched
// shadows. Distances convert active voxels→cm; the emitter radius scales with the instance too.
if (L.bIsLightOrb)
{
const float Scale = Xf.GetScale3D().X;
Out.bIsOrb = true;
Out.Orb.WorldPos = Xf.GetLocation();
Out.Orb.Color = L.OrbColor;
Out.Orb.Intensity = L.OrbIntensity;
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
Out.Orb.RadiusWorld = L.OrbRadiusVoxels * VoxelSizeCm * Scale;
Out.Orb.FalloffWorld = L.OrbFalloffVoxels * VoxelSizeCm;
Out.Orb.MaxShadowDistWorld = L.OrbMaxShadowDistanceVoxels * VoxelSizeCm;
}
}
void UVoxelContentManager::GetActiveOrbs(TArray<FVoxelActiveOrb>& OutOrbs) const
{
OutOrbs.Reset();
for (const TPair<FIntVector, FLandmarkInstance>& Pair : LandmarkInstances)
{
if (Pair.Value.bIsOrb) { OutOrbs.Add(Pair.Value.Orb); }
}
}
void UVoxelContentManager::DestroyLandmarkInstance(FLandmarkInstance& Inst)
{
if (AActor* A = Inst.Actor.Get()) { A->Destroy(); }
if (UStaticMeshComponent* C = Inst.Component.Get()) { C->DestroyComponent(); }
Inst.Actor = nullptr;
Inst.Component = nullptr;
}
void UVoxelContentManager::ClearAllLandmarks()
{
for (TPair<FIntVector, FLandmarkInstance>& Pair : LandmarkInstances) { DestroyLandmarkInstance(Pair.Value); }
LandmarkInstances.Reset();
}
int32 UVoxelContentManager::RemoveInstancesInContent(FDecoRegionContent& Content, const FVector& Center, float Radius)
{
int32 Removed = 0;
for (TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>& WP : Content.Instances)
{
UHierarchicalInstancedStaticMeshComponent* HISM = WP.Get();
if (!HISM || HISM->GetInstanceCount() == 0) continue;
// Fast bounds-tested spatial query → the vast majority of region HISMs return empty immediately.
TArray<int32> Hits = HISM->GetInstancesOverlappingSphere(Center, Radius, /*bSphereInWorldSpace*/ true);
if (Hits.Num() > 0)
{
HISM->RemoveInstances(Hits); // handles index shifting; marks render state dirty
Removed += Hits.Num();
}
}
return Removed;
}
int32 UVoxelContentManager::RemoveDecorationsInSphere(const FVector& WorldCenter, float WorldRadius)
{
if (WorldRadius <= 0.0f) return 0;
int32 Removed = 0;
for (FDecoGrid* G : { &NearGrid, &FarGrid })
{
for (TPair<FIntPoint, FDecoRegionContent>& RP : G->Regions)
{
Removed += RemoveInstancesInContent(RP.Value, WorldCenter, WorldRadius);
}
}
return Removed;
}
void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
{
if (!StrateManager || !Generator || !Settings) return;
AActor* OwnerActor = Owner.Get();
if (!OwnerActor) return;
const FTransform OwnerXf = OwnerActor->GetActorTransform();
const FVector LocalPlayer = OwnerXf.InverseTransformPosition(PlayerWorldPos);
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(OwnerActor);
const float ManagerLocalZ = VoxelWorld
? VoxelWorld->ToStrateManagerLocalZ(LocalPlayer.Z) : LocalPlayer.Z;
float TopZ, BotZ;
const bool bInStrate = StrateManager->GetStrateUnrealZRange(ManagerLocalZ, TopZ, BotZ);
const int32 StrateIndex = bInStrate ? StrateManager->GetStrateIndex(ManagerLocalZ) : INT32_MIN;
if (!bInStrate)
{
if (LandmarkInstances.Num() > 0) { ClearAllLandmarks(); }
LastLandmarkStrate = INT32_MIN;
return;
}
if (StrateIndex != LastLandmarkStrate)
{
ClearAllLandmarks();
LastLandmarkStrate = StrateIndex;
}
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
const float ChunkWorld = static_cast<float>(CHUNK_SIZE) * VoxelSizeCm;
// Shared strate context (a strate is a horizontal slab → same everywhere this update).
FDecoContext Ctx;
Ctx.StrateIndex = StrateIndex;
Ctx.VoxelSizeCm = VoxelSizeCm;
Ctx.ActiveToOwnerVoxelScale = VoxelSizeCm / VOXEL_SIZE;
Ctx.TopVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ) / VoxelSizeCm;
Ctx.BottomVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(BotZ) : BotZ) / VoxelSizeCm;
Ctx.RepChunkZ = FMath::FloorToInt(((TopZ + BotZ) * 0.5f / VOXEL_SIZE) / (float)CHUNK_SIZE);
const FIntVector RepChunk(FMath::FloorToInt(LocalPlayer.X / ChunkWorld),
FMath::FloorToInt(LocalPlayer.Y / ChunkWorld), Ctx.RepChunkZ);
const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(RepChunk);
if (!Def || Def->Landmarks.Num() == 0)
{
if (LandmarkInstances.Num() > 0) { ClearAllLandmarks(); }
return;
}
Ctx.Def = Def;
Ctx.bSurfaceWorld = (StrateManager->GetGeneratorTypeForChunk(RepChunk) == ECaveGeneratorType::SurfaceWorld);
Ctx.NativeSurface = NativeSurfaceSnapshot;
Ctx.bNativeSurface = VoxelWorld != nullptr && VoxelWorld->bUseVoxelForgeCore
&& Ctx.bSurfaceWorld && Ctx.NativeSurface.IsValid()
&& Ctx.NativeSurface->FindPlan(StrateIndex) != nullptr;
{
bool bHasNativePlan = false;
bool bNativeWaterEnabled = false;
float NativeWaterLocalZ = -FLT_MAX;
if (Ctx.bNativeSurface
&& Ctx.NativeSurface->GetWater(StrateIndex, bHasNativePlan,
bNativeWaterEnabled, NativeWaterLocalZ)
&& bHasNativePlan)
{
Ctx.bHasWater = bNativeWaterEnabled;
Ctx.WaterLocalZ = bNativeWaterEnabled ? NativeWaterLocalZ : -FLT_MAX;
}
else
{
const float Wv = StrateManager->GetWaterLevelWorldZForChunk(RepChunk);
Ctx.bHasWater = (Wv != -FLT_MAX);
Ctx.WaterLocalZ = Ctx.bHasWater
? (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(Wv * VOXEL_SIZE) : Wv * VOXEL_SIZE)
: -FLT_MAX;
}
}
// Landmarks use the same per-column biome gates as decorations. Keep the complete POD biome
// context in the worker/game-thread placement context; a null/default context would silently
// bypass the authored RequiredBiome/Conditions decisions for landmarks.
Ctx.BiomeCtx = StrateManager->GetBiomeContextForChunk(RepChunk);
const float Step = (float)FMath::Max(1, Settings->DecorationMarchStepVoxels);
const float ColDepth = (float)FMath::Max(8, Settings->DecorationColumnDepthVoxels);
const uint32 LocalSeed = (uint32)Seed;
// ---- Gather candidates (a small set) across every entry + anchor mode. Local XY (actor-space cm) is
// enough for the exclusion test — the surface-find only moves Z. ----
// bSpawnable = within the entry's real stream radius. POP-FREE EXCLUSION: we gather each entry in
// (radius + MaxExcl) so every conflictor of an in-range candidate is present regardless of player
// position; the extra "ring" candidates only SUPPRESS (never spawn), so a candidate's fate is a pure
// function of (seed, layout) → no edge-of-radius flicker.
struct FCand { FIntVector Key; int32 EntryIdx; uint32 H; float LocalX, LocalY; float ExclWorld; int32 Priority; bool bSpawnable; };
TArray<FCand> Cands;
bool bAnyExclusion = false; // pure scatter (all radii 0) skips the O(n²) resolve → same cost as before
float MaxExclChunks = 0.0f;
for (const FStrateLandmark& LE : Def->Landmarks) { MaxExclChunks = FMath::Max(MaxExclChunks, FMath::Max(0.0f, LE.ExclusionRadiusChunks)); }
const float MaxExclWorld = MaxExclChunks * ChunkWorld;
for (int32 EntryIdx = 0; EntryIdx < Def->Landmarks.Num(); ++EntryIdx)
{
const FStrateLandmark& L = Def->Landmarks[EntryIdx];
if (!L.Profile.ActorClass && !L.Profile.InstancedMesh) continue;
const int32 RadiusChunks = FMath::Max(1, L.StreamRadiusChunks);
const float RadiusWorld = (float)RadiusChunks * ChunkWorld;
const float GatherWorld = RadiusWorld + MaxExclWorld; // widened so all conflictors are gathered
const float ExclWorld = FMath::Max(0.0f, L.ExclusionRadiusChunks) * ChunkWorld;
if (ExclWorld > 0.0f) bAnyExclusion = true;
if (L.AnchorMode == ELandmarkAnchor::HashLattice)
{
const float SpacingChunks = FMath::Max(1.0f, L.SpacingChunks);
const float CellWorld = SpacingChunks * ChunkWorld; // lattice cell size in cm
const float JitterRange = FMath::Clamp(L.JitterFraction, 0.0f, 1.0f);
const FIntPoint PlayerLCell(FMath::FloorToInt(LocalPlayer.X / CellWorld),
FMath::FloorToInt(LocalPlayer.Y / CellWorld));
const int32 CellRange = FMath::CeilToInt(((float)RadiusChunks + MaxExclChunks) / SpacingChunks);
for (int32 dy = -CellRange; dy <= CellRange; ++dy)
for (int32 dx = -CellRange; dx <= CellRange; ++dx)
{
const FIntPoint LCell(PlayerLCell.X + dx, PlayerLCell.Y + dy);
// Existence roll (salt 0x1A2D5u kept from the original landmark path → positions unchanged).
const uint32 H = DecoHash(LCell.X, LCell.Y, 0, 0, 0, EntryIdx, LocalSeed, 0x1A2D5u);
if (VoxelHash::ToFloat01(H) > L.SpawnProbability) continue;
const float jx = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x51A3F1u)) - 0.5f) * JitterRange;
const float jy = (VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x7C2B93u)) - 0.5f) * JitterRange;
const float LocalX = ((float)LCell.X + 0.5f + jx) * CellWorld;
const float LocalY = ((float)LCell.Y + 0.5f + jy) * CellWorld;
const float ddx = LocalX - LocalPlayer.X, ddy = LocalY - LocalPlayer.Y;
const float DistSq = ddx * ddx + ddy * ddy;
if (DistSq > GatherWorld * GatherWorld) continue;
Cands.Add({ FIntVector(LCell.X, LCell.Y, EntryIdx), EntryIdx, H, LocalX, LocalY, ExclWorld, L.Priority,
DistSq <= RadiusWorld * RadiusWorld });
}
}
else // PassageMouth — enumerate the finite passage list, keep endpoints that land in THIS strate.
{
const TArray<FVoxelPassage>& Passages = StrateManager->GetPassages();
for (int32 pi = 0; pi < Passages.Num(); ++pi)
{
const FVoxelPassage& Pg = Passages[pi];
for (int32 side = 0; side < 2; ++side)
{
const bool bDescent = (side == 0); // 0 = this strate is the passage's UPPER (hole DOWN)
if (bDescent && !(L.bAtDescentMouths && Pg.UpperStrateIndex == StrateIndex)) continue;
if (!bDescent && !(L.bAtArrivalMouths && Pg.LowerStrateIndex == StrateIndex)) continue;
// Passage endpoints are GLOBAL VOXEL coords (same space as the SDF path) → local cm.
const FVector MouthVox = bDescent ? Pg.UpperPoint : Pg.LowerPoint;
const float LocalX = (MouthVox.X / Ctx.ActiveToOwnerVoxelScale) * Ctx.VoxelSizeCm;
const float LocalY = (MouthVox.Y / Ctx.ActiveToOwnerVoxelScale) * Ctx.VoxelSizeCm;
const float ddx = LocalX - LocalPlayer.X, ddy = LocalY - LocalPlayer.Y;
const float DistSq = ddx * ddx + ddy * ddy;
if (DistSq > GatherWorld * GatherWorld) continue;
const uint32 H = DecoHash(pi, side, 0, 0, 0, EntryIdx, LocalSeed, 0x5E7C9u);
if (VoxelHash::ToFloat01(H) > L.MouthProbability) continue;
Cands.Add({ FIntVector(pi, side, EntryIdx), EntryIdx, H, LocalX, LocalY, ExclWorld, L.Priority,
DistSq <= RadiusWorld * RadiusWorld });
}
}
}
}
// ---- Resolve exclusion (only if any entry opts in): a candidate is suppressed when a HIGHER-RANKED one's
// disk covers it. Rank = (Priority, then hash) → deterministic. v1 resolves within the in-range set. ----
TSet<FIntVector> Desired;
Desired.Reserve(Cands.Num());
for (int32 i = 0; i < Cands.Num(); ++i)
{
const FCand& C = Cands[i];
if (!C.bSpawnable) continue; // ring-only conflictor (gathered for pop-free resolve, never spawned)
if (bAnyExclusion)
{
bool bSuppressed = false;
for (int32 j = 0; j < Cands.Num(); ++j)
{
if (j == i) continue;
const FCand& D = Cands[j];
if (D.ExclWorld <= 0.0f) continue;
const bool bDOutranks = (D.Priority != C.Priority) ? (D.Priority > C.Priority) : (D.H > C.H);
if (!bDOutranks) continue;
const float dxl = D.LocalX - C.LocalX, dyl = D.LocalY - C.LocalY;
if (dxl * dxl + dyl * dyl < D.ExclWorld * D.ExclWorld) { bSuppressed = true; break; }
}
if (bSuppressed) continue;
}
Desired.Add(C.Key);
if (LandmarkInstances.Contains(C.Key)) continue; // already evaluated (spawned OR empty)
FLandmarkInstance Inst;
SpawnLandmarkInstance(Def->Landmarks[C.EntryIdx], C.H, Ctx, OwnerXf, OwnerActor,
C.LocalX, C.LocalY, Step, ColDepth, Inst);
// Decoration footprint (F7): clear groundcover under a placed suppressing landmark now, and remember
// the footprint so a deco region streaming in near it re-clears too (see ApplyRegion).
const FStrateLandmark& LE = Def->Landmarks[C.EntryIdx];
if (LE.bSuppressDecorationsUnder && LE.SuppressRadiusChunks > 0.0f
&& (Inst.Actor.IsValid() || Inst.Component.IsValid()))
{
const FVector WP = Inst.Actor.IsValid() ? Inst.Actor.Get()->GetActorLocation()
: Inst.Component.Get()->GetComponentLocation();
Inst.SuppressCenter = WP;
Inst.SuppressRadiusWorld = LE.SuppressRadiusChunks * ChunkWorld;
RemoveDecorationsInSphere(WP, Inst.SuppressRadiusWorld);
}
LandmarkInstances.Add(C.Key, Inst); // stored even if empty → never re-evaluated while in range
}
// Drop instances no longer desired (player moved away, list shrank, lost an exclusion conflict).
for (auto It = LandmarkInstances.CreateIterator(); It; ++It)
{
if (Desired.Contains(It.Key())) continue;
DestroyLandmarkInstance(It.Value());
It.RemoveCurrent();
}
}
//=============================================================================
// WATER — tile-driven, level-0 only
//=============================================================================
void UVoxelContentManager::UpdateWater(const FVector& PlayerWorldPos)
{
if (!StrateManager || !PlaneMesh) return;
AActor* OwnerActor = Owner.Get();
if (!OwnerActor) return;
const FTransform Xf = OwnerActor->GetActorTransform();
const FVector LocalPos = Xf.InverseTransformPosition(PlayerWorldPos);
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(OwnerActor);
const float LocalToManagerScale = VoxelWorld
? VOXEL_SIZE / VoxelWorld->GetActiveVoxelSizeCm() : 1.0f;
FVector ManagerLocalPos = LocalPos * LocalToManagerScale;
if (VoxelWorld)
{
ManagerLocalPos.Z = VoxelWorld->ToStrateManagerLocalZ(LocalPos.Z);
}
// Player chunk (voxel→chunk) for the strate / water-level lookup.
const float ManagerChunkWorld = static_cast<float>(CHUNK_SIZE) * VOXEL_SIZE;
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
const float ChunkWorld = static_cast<float>(CHUNK_SIZE) * VoxelSizeCm;
const FIntVector PlayerChunk(
FMath::FloorToInt(ManagerLocalPos.X / ManagerChunkWorld),
FMath::FloorToInt(ManagerLocalPos.Y / ManagerChunkWorld),
FMath::FloorToInt(ManagerLocalPos.Z / ManagerChunkWorld));
float WaterLocalZ = -FLT_MAX;
const int32 StrateIndex = StrateManager->GetStrateIndex(ManagerLocalPos.Z);
bool bHasNativePlan = false;
bool bNativeWaterEnabled = false;
float NativeWaterLocalZ = -FLT_MAX;
if (VoxelWorld != nullptr && VoxelWorld->bUseVoxelForgeCore
&& NativeSurfaceSnapshot.IsValid()
&& NativeSurfaceSnapshot->GetWater(StrateIndex, bHasNativePlan,
bNativeWaterEnabled, NativeWaterLocalZ)
&& bHasNativePlan)
{
if (bNativeWaterEnabled)
{
WaterLocalZ = NativeWaterLocalZ;
}
}
else
{
const float WaterVoxelZ = StrateManager->GetWaterLevelWorldZForChunk(PlayerChunk);
if (WaterVoxelZ != -FLT_MAX)
{
const float ManagerWaterLocalZ = WaterVoxelZ * VOXEL_SIZE;
WaterLocalZ = VoxelWorld
? VoxelWorld->FromStrateManagerLocalZ(ManagerWaterLocalZ) : ManagerWaterLocalZ;
}
}
// No water in this strate means the plane stays hidden. / Sans eau dans cette strate, le plan reste masque.
if (WaterLocalZ == -FLT_MAX)
{
if (WaterPlane) { WaterPlane->SetVisibility(false); }
LastWaterZ = -FLT_MAX;
LastWaterCell = FIntPoint(INT32_MIN, INT32_MIN);
return;
}
// Snap the plane centre to a coarse cell so it only repositions when the player crosses it
// (avoids per-frame churn; a uniform plane sliding is invisible anyway as long as the water
// material pans on WORLD position rather than mesh UVs).
const FIntPoint Cell(
FMath::FloorToInt(LocalPos.X / ChunkWorld),
FMath::FloorToInt(LocalPos.Y / ChunkWorld));
if (WaterPlane && WaterPlane->IsVisible() && WaterLocalZ == LastWaterZ && Cell == LastWaterCell)
return; // nothing changed
if (!WaterPlane)
{
WaterPlane = NewObject<UStaticMeshComponent>(OwnerActor);
WaterPlane->SetStaticMesh(PlaneMesh);
WaterPlane->SetCollisionEnabled(ECollisionEnabled::NoCollision);
WaterPlane->SetCastShadow(false);
WaterPlane->SetMobility(EComponentMobility::Movable); // it follows the player
WaterPlane->RegisterComponent();
WaterPlane->AttachToComponent(OwnerActor->GetRootComponent(),
FAttachmentTransformRules::KeepRelativeTransform);
}
// Cover the whole view to the horizon (one draw, so be generous). Engine plane is 100 uu.
const float CoverWorld = Settings
? (float)FMath::Max(4, Settings->ViewDistanceXY + 4) * 2.0f * ChunkWorld
: 200000.0f;
WaterPlane->SetWorldScale3D(FVector(CoverWorld / 100.0f, CoverWorld / 100.0f, 1.0f));
const FVector LocalCenter(((float)Cell.X + 0.5f) * ChunkWorld,
((float)Cell.Y + 0.5f) * ChunkWorld,
WaterLocalZ);
WaterPlane->SetWorldLocation(Xf.TransformPosition(LocalCenter));
WaterPlane->SetVisibility(true);
if (const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(PlayerChunk))
{
if (Def->WaterMaterial) { WaterPlane->SetMaterial(0, Def->WaterMaterial); }
}
LastWaterZ = WaterLocalZ;
LastWaterCell = Cell;
}
//=============================================================================
// CLEAR ALL
//=============================================================================
void UVoxelContentManager::ClearAll()
{
ClearAllDecorations();
ClearAllLandmarks();
if (WaterPlane) { WaterPlane->DestroyComponent(); WaterPlane = nullptr; }
LastWaterZ = -FLT_MAX;
LastWaterCell = FIntPoint(INT32_MIN, INT32_MIN);
// Force a full decoration + landmark rebuild on the next update.
LastDecoCell = FIntPoint(INT32_MIN, INT32_MIN);
LastStrateIndex = INT32_MIN;
LastLandmarkStrate = INT32_MIN;
}
//=============================================================================
// DIAGNOSTIC — decoration streaming state under a point
//=============================================================================
void UVoxelContentManager::QueryDecoDebugAt(const FVector& LocalPos, bool& bApplied, int32& InstanceCount,
bool& bBuilding, int32& CellsAccounted, int32& CellsTotal,
int32& LiveMarchSpawns, int32& InstancesInCell) const
{
bApplied = false; InstanceCount = 0; bBuilding = false; CellsAccounted = 0;
LiveMarchSpawns = -1; InstancesInCell = 0;
const int32 R = RegionSize();
CellsTotal = R * R;
// World-LOCAL XY → deco cell → region (same math the scatter uses).
const AVoxelWorld* VoxelWorld = Cast<AVoxelWorld>(Owner.Get());
const float VoxelSizeCm = VoxelWorld ? VoxelWorld->GetActiveVoxelSizeCm() : VOXEL_SIZE;
const float CellWorld = static_cast<float>(DECO_CELL_VOXELS) * VoxelSizeCm;
const FIntPoint Cell(FMath::FloorToInt(LocalPos.X / CellWorld),
FMath::FloorToInt(LocalPos.Y / CellWorld));
const FIntPoint Region = CellToRegion(Cell, R);
// Actor-LOCAL XY footprint of the probed cell (instances are stored component-local; the HISM sits at
// the actor transform with identity relative, so component-local == actor-local cell space).
const float CellMinX = (float)Cell.X * CellWorld, CellMaxX = CellMinX + CellWorld;
const float CellMinY = (float)Cell.Y * CellWorld, CellMaxY = CellMinY + CellWorld;
// Probe BOTH grids: a point is covered by a Far region always, plus a Near region when close. Aggregate
// applied instances + per-cell counts + building state across the two.
for (const FDecoGrid* GP : { &NearGrid, &FarGrid })
{
const FDecoGrid& G = *GP;
if (const FDecoRegionContent* Content = G.Regions.Find(Region))
{
bApplied = true;
for (const TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>& C : Content->Instances)
{
const UHierarchicalInstancedStaticMeshComponent* Comp = C.Get();
if (!Comp) continue;
const int32 N = Comp->GetInstanceCount();
InstanceCount += N;
// Count the ones actually inside the probed cell → tells a blank cell apart from a blank region.
for (int32 i = 0; i < N; ++i)
{
FTransform Xf;
if (!Comp->GetInstanceTransform(i, Xf, /*bWorldSpace=*/false)) continue;
const FVector P = Xf.GetLocation();
if (P.X >= CellMinX && P.X < CellMaxX && P.Y >= CellMinY && P.Y < CellMaxY)
{
++InstancesInCell;
}
}
}
}
if (const FDecoRegionBuild* Build = G.Builds.Find(Region))
{
bBuilding = true;
CellsAccounted += Build->AccountedCells.Num();
}
}
// PER-CELL decisive probe: re-run the march for THIS cell synchronously with the current strate context
// (set each UpdateDecorations), for BOTH grids' palettes summed. Same inputs the worker uses →
// byte-identical result, so it reports exactly what the scatter decides for this cell right now. Only
// valid when the probed point shares the player's current strate (CurrentCtx reflects that); else -1.
AActor* OwnerActor = Owner.Get();
if (Generator && Settings && OwnerActor && CurrentCtx.Def)
{
const float Step = (float)FMath::Max(1, Settings->DecorationMarchStepVoxels);
const int32 MaxCross = FMath::Max(1, Settings->DecorationMaxCrossingsPerColumn);
const float ColDepth = (float)FMath::Max(8, Settings->DecorationColumnDepthVoxels);
int32 Total = 0; bool bAny = false;
for (const FDecoGrid* GP : { &NearGrid, &FarGrid })
{
const FDecoGrid& G = *GP;
if (G.Entries.Num() == 0) continue;
bAny = true;
TArray<FDecoSpawn> Spawns;
BuildCellSpawns(Generator, OwnerActor->GetActorTransform(), Cell, CurrentCtx,
G.Entries, G.EntryBiome, (uint32)Seed,
G.Spacing, Step, MaxCross, ColDepth, Spawns);
Total += Spawns.Num();
}
if (bAny) { LiveMarchSpawns = Total; }
}
}