wip(assembler): round S1 -- one Core voxel-size source; per-tile feature lists (islands)

AVoxelWorld::CoreVoxelSizeCm (default 25, 32 voxels/tile) flows through plan, field,
mesher, content, water, atmosphere, edits and collision. Staged-owner BuildHost passes at
20 cm (SurfaceWorld + islands, W1/W8/W16, anchors, BP edits, collision) with a fixed-25cm
negative control; default-size hashes match round 4; Core off stays legacy 12.5 cm.
Reusable BuildWorldAssemblyFeatureTileListV1; wired into FloatingIslands only so far.
256-island stress: tile p95 112.5 -> 2.0 ms, 0 mismatches, dropped-feature control fails.
Content placement measured (no populated cells): island ground search 7.25 ms/cell vs
legacy 0.29 ms -- open. Outside-review evaluation in Round5/REPORT.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-18 18:48:38 +02:00
1 parent 111cf3d262
commit d6e69420e8
25 files changed
+2136 -250

No files matched your search

+330 -78
View File
@@ -21,10 +21,12 @@
#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;
@@ -33,21 +35,18 @@ struct FCoreSurfaceSnapshot
return Plans.Find(StrateIndex);
}
bool GetHeights(int32 StrateIndex, float LegacyVoxelX, float LegacyVoxelY,
float& OutGroundLegacyVoxels, float& OutCapLegacyVoxels) const
bool GetHeights(int32 StrateIndex, float CoreVoxelX, float CoreVoxelY,
float& OutGroundCoreVoxels, float& OutCapCoreVoxels) const
{
const auto* Plan = FindPlan(StrateIndex);
if (!Plan || !VoxelMath::IsFinite(LegacyVoxelX) || !VoxelMath::IsFinite(LegacyVoxelY))
if (!Plan || !VoxelMath::IsFinite(CoreVoxelX) || !VoxelMath::IsFinite(CoreVoxelY))
{
return false;
}
// One 12.5 cm legacy voxel equals 128 subvoxels, half a 25 cm Core voxel.
// Un voxel historique de 12,5 cm vaut 128 sous-voxels, soit la moitie d'un voxel Core de 25 cm.
constexpr double LegacyVoxelToSubvoxels = 128.0;
constexpr double SubvoxelsPerLegacyVoxel = LegacyVoxelToSubvoxels;
constexpr double MaxInput = static_cast<double>(MAX_int64) / LegacyVoxelToSubvoxels;
if (FMath::Abs(static_cast<double>(LegacyVoxelX)) > MaxInput
|| FMath::Abs(static_cast<double>(LegacyVoxelY)) > MaxInput)
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;
}
@@ -58,9 +57,9 @@ struct FCoreSurfaceSnapshot
return false;
}
const int64 XSubvoxels = static_cast<int64>(std::llround(
static_cast<double>(LegacyVoxelX) * LegacyVoxelToSubvoxels));
static_cast<double>(CoreVoxelX) * SubvoxelsPerCoreVoxel));
const int64 YSubvoxels = static_cast<int64>(std::llround(
static_cast<double>(LegacyVoxelY) * LegacyVoxelToSubvoxels));
static_cast<double>(CoreVoxelY) * SubvoxelsPerCoreVoxel));
const auto Sample = Heightfield.point(
XSubvoxels, YSubvoxels, 0);
int64 SurfaceHeightSubvoxels = Sample.groundHeightSubvoxels;
@@ -103,9 +102,9 @@ struct FCoreSurfaceSnapshot
PreviousDensity = Density;
}
}
OutGroundLegacyVoxels = static_cast<float>(SurfaceHeightSubvoxels / SubvoxelsPerLegacyVoxel);
OutCapLegacyVoxels = static_cast<float>(Sample.capHeightSubvoxels / SubvoxelsPerLegacyVoxel);
return VoxelMath::IsFinite(OutGroundLegacyVoxels) && VoxelMath::IsFinite(OutCapLegacyVoxels);
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
@@ -122,7 +121,8 @@ struct FCoreSurfaceSnapshot
OutEnabled = Plan->waterEnabled;
if (OutEnabled)
{
OutLocalCm = static_cast<float>(Plan->waterSurfaceSubvoxels) * (25.0f / 256.0f);
OutLocalCm = static_cast<float>(Plan->waterSurfaceSubvoxels
* CoreVoxelSizeCm / VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
}
return true;
}
@@ -137,6 +137,7 @@ struct FCoreFloatingIslandFieldSnapshot
struct FCoreFloatingIslandSnapshot
{
double CoreVoxelSizeCm = 2.0 * VOXEL_SIZE;
TMap<int32, TSharedPtr<const FCoreFloatingIslandFieldSnapshot, ESPMode::ThreadSafe>> Fields;
const FCoreFloatingIslandFieldSnapshot* FindField(int32 StrateIndex) const
@@ -145,32 +146,30 @@ struct FCoreFloatingIslandSnapshot
return Found != nullptr && Found->IsValid() ? Found->Get() : nullptr;
}
bool GetDensity(int32 StrateIndex, float LegacyVoxelX, float LegacyVoxelY,
float LegacyVoxelZ, float& OutDensity) const
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(LegacyVoxelX) || !FMath::IsFinite(LegacyVoxelY)
|| !FMath::IsFinite(LegacyVoxelZ))
|| !FMath::IsFinite(CoreVoxelX) || !FMath::IsFinite(CoreVoxelY)
|| !FMath::IsFinite(CoreVoxelZ))
{
return false;
}
// A legacy voxel is 128 subvoxels at 12.5 cm; Core field distances are converted back to legacy units.
// Un voxel historique vaut 128 sous-voxels a 12,5 cm ; la distance Core revient en unites historiques.
constexpr double LegacyVoxelSubvoxels = 128.0;
constexpr double MaximumInput = static_cast<double>(MAX_int64) / LegacyVoxelSubvoxels;
if (FMath::Abs(static_cast<double>(LegacyVoxelX)) > MaximumInput
|| FMath::Abs(static_cast<double>(LegacyVoxelY)) > MaximumInput
|| FMath::Abs(static_cast<double>(LegacyVoxelZ)) > MaximumInput)
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>(LegacyVoxelX) * LegacyVoxelSubvoxels)),
static_cast<int64>(std::llround(static_cast<double>(LegacyVoxelY) * LegacyVoxelSubvoxels)),
static_cast<int64>(std::llround(static_cast<double>(LegacyVoxelZ) * LegacyVoxelSubvoxels)));
OutDensity = static_cast<float>(static_cast<double>(Density) / LegacyVoxelSubvoxels);
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);
}
@@ -185,8 +184,8 @@ struct FCoreFloatingIslandSnapshot
{
return false;
}
constexpr double SubvoxelsPerCoreVoxel = 256.0;
constexpr double CentimetersPerCoreVoxel = 25.0;
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)
@@ -301,6 +300,8 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
{
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)
{
@@ -312,6 +313,8 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
{
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 =
@@ -363,15 +366,16 @@ bool UVoxelContentManager::QueryNativeSurfaceAnchors(
return false;
}
float GroundLegacyVoxels = 0.0f;
float CapLegacyVoxels = 0.0f;
const float VoxelSizeCm = VoxelWorld->GetActiveVoxelSizeCm();
float GroundCoreVoxels = 0.0f;
float CapCoreVoxels = 0.0f;
if (!NativeSurfaceSnapshot->GetHeights(StrateIndex,
LocalXYCm.X / VOXEL_SIZE, LocalXYCm.Y / VOXEL_SIZE,
GroundLegacyVoxels, CapLegacyVoxels))
LocalXYCm.X / VoxelSizeCm, LocalXYCm.Y / VoxelSizeCm,
GroundCoreVoxels, CapCoreVoxels))
{
return false;
}
OutGroundLocalZCm = GroundLegacyVoxels * VOXEL_SIZE;
OutGroundLocalZCm = GroundCoreVoxels * VoxelSizeCm;
bool bHasPlan = false;
if (!NativeSurfaceSnapshot->GetWater(StrateIndex, bHasPlan,
bOutHasWater, OutWaterLocalZCm)
@@ -553,8 +557,8 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
if (!OwnerActor) return;
const FTransform OwnerXf = OwnerActor->GetActorTransform();
// All strate/water/density queries are in actor-LOCAL space (StrateManager assumes actor origin;
// the mesher builds geometry from voxel*VOXEL_SIZE in local space). Bring the player local.
// 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
@@ -573,7 +577,8 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
return;
}
const float CellWorld = (float)DECO_CELL_VOXELS * VOXEL_SIZE;
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));
@@ -581,8 +586,10 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
// Shared per-update strate context (a strate is a horizontal slab → same for every cell).
CurrentCtx = FDecoContext();
CurrentCtx.StrateIndex = StrateIndex;
CurrentCtx.TopVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ) / VOXEL_SIZE;
CurrentCtx.BottomVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(BotZ) : BotZ) / VOXEL_SIZE;
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);
@@ -706,6 +713,208 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos)
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;
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);
const double CellStart = FPlatformTime::Seconds();
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;
BuildCellSpawns(Generator, OwnerXf, Cell, CurrentCtx, Entries, EntryBiome,
static_cast<uint32>(Seed), Grid.Spacing, static_cast<float>(Step),
MaxCrossings, ColumnDepth, Spawns, &Counters);
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((FPlatformTime::Seconds() - CellStart) * 1000.0);
}
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);
// 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
@@ -882,8 +1091,10 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
const TArray<FStrateDecoration>& Entries,
const TArray<int32>& EntryBiome, uint32 InSeed,
int32 Spacing, float Step, int32 MaxCrossings, float ColumnDepth,
TArray<FDecoSpawn>& OutSpawns)
TArray<FDecoSpawn>& OutSpawns,
FPlacementBuildCounters* OutCounters)
{
if (OutCounters != nullptr) *OutCounters = FPlacementBuildCounters();
if (!Gen || Entries.Num() == 0) return;
const int32 PerAxis = FMath::Max(1, CHUNK_SIZE / Spacing);
@@ -912,9 +1123,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
// 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;
@@ -922,16 +1133,27 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
return NativeDensity;
return 1.0f;
}
return Gen->GetDensityAt(VX, VY, VZ);
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);
}
return Gen->GetSurfaceHeightAt(VX, VY, Ctx.RepChunkZ, 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
@@ -939,7 +1161,7 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
auto MakeCompanionXf = [&](const FPlacementProfile& P, uint32 IHash,
float sVX, float sVY, float sZ, const FVector& sN) -> FTransform
{
const FVector Local(sVX * VOXEL_SIZE, sVY * VOXEL_SIZE, sZ * VOXEL_SIZE);
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();
@@ -965,7 +1187,7 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
const bool bCeiling = NormalWorld.Z < -0.5f;
const bool bWall = !bFloor && !bCeiling;
const FVector LocalPos(VX * VOXEL_SIZE, VY * VOXEL_SIZE, ZC * VOXEL_SIZE);
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);
@@ -1014,7 +1236,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
// 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, VY, Ctx.BiomeCtx)) continue;
!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).
@@ -1074,7 +1298,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
// Optional per-satellite gating (relief/moisture/biome-border at ITS own XY).
if (Comp.Profile.Conditions.Num() > 0 &&
!Gen->EvaluateTerrainConditions(Comp.Profile.Conditions, SatVX, SatVY, Ctx.BiomeCtx)) continue;
!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;
@@ -1121,7 +1347,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
const float SubVY = SatVY + SinSAng * SRadVox;
if (Sub.Profile.Conditions.Num() > 0 &&
!Gen->EvaluateTerrainConditions(Sub.Profile.Conditions, SubVX, SubVY, Ctx.BiomeCtx)) continue;
!Gen->EvaluateTerrainConditions(Sub.Profile.Conditions,
SubVX * Ctx.ActiveToOwnerVoxelScale,
SubVY * Ctx.ActiveToOwnerVoxelScale, Ctx.BiomeCtx)) continue;
FDecoSpawn& SO = OutSpawns.AddDefaulted_GetRef();
SO.EntryIdx = EntryIdx;
@@ -1141,9 +1369,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
for (int32 gy = 0; gy < PerAxis; ++gy)
for (int32 gx = 0; gx < PerAxis; ++gx)
{
// INTEGER voxel jitter: columns land on integer XY so the generator's surface-column cache
// (T1.a, §8.10) applies — FRACTIONAL XY recomputes the noise-heavy heightfield+biome on EVERY
// sample. A 12.5 cm grid offset is imperceptible; random yaw/scale still breaks up the regularity.
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));
@@ -1155,7 +1383,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
int32 ColBiome = -1;
if (bHasBiomes)
{
const FBiomeSample BS = Gen->ResolveBiomeSampleAt(VX, VY, Ctx.RepChunkZ, Ctx.BiomeCtx, BiomeCache);
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
@@ -1563,7 +1793,8 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const
}
return 1.0f;
}
return Gen->GetDensityAt(SampleX, SampleY, SampleZ);
return Gen->GetDensityAt(SampleX * Ctx.ActiveToOwnerVoxelScale,
SampleY * Ctx.ActiveToOwnerVoxelScale, SampleZ * Ctx.ActiveToOwnerVoxelScale);
};
if (Ctx.bSurfaceWorld)
@@ -1576,8 +1807,17 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const
return Ctx.NativeSurface->GetHeights(
Ctx.StrateIndex, SampleX, SampleY, OutGround, OutCap);
}
return Gen->GetSurfaceHeightAt(
SampleX, SampleY, Ctx.RepChunkZ, 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;
@@ -1665,13 +1905,14 @@ bool UVoxelContentManager::SpawnFromProfile(const FPlacementProfile& P, uint32 H
FTransform& OutXf, FLandmarkInstance& Out)
{
if (!Generator) return false;
const float VX = LocalX / VOXEL_SIZE;
const float VY = LocalY / VOXEL_SIZE;
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, VY, Ctx.RepChunkZ);
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
}
@@ -1685,7 +1926,7 @@ bool UVoxelContentManager::SpawnFromProfile(const FPlacementProfile& P, uint32 H
if (P.MinSlopeAngle > 0.01f &&
FMath::Abs(N.Z) > VoxelMath::DetCos(FMath::DegreesToRadians(P.MinSlopeAngle))) return false;
const FVector LocalPos(LocalX, LocalY, ZC * VOXEL_SIZE);
const FVector LocalPos(LocalX, LocalY, ZC * Ctx.VoxelSizeCm);
if (P.bRequireWaterRelative && Ctx.bHasWater)
{
const bool bBelowWater = (LocalPos.Z < Ctx.WaterLocalZ);
@@ -1695,7 +1936,9 @@ bool UVoxelContentManager::SpawnFromProfile(const FPlacementProfile& P, uint32 H
// 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, VY, Ctx.BiomeCtx)) return false;
!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;
@@ -1752,7 +1995,7 @@ void UVoxelContentManager::SpawnLandmarkInstance(const FStrateLandmark& L, uint3
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 voxels→cm (×VOXEL_SIZE); the emitter radius scales with the instance too.
// shadows. Distances convert active voxels→cm; the emitter radius scales with the instance too.
if (L.bIsLightOrb)
{
const float Scale = Xf.GetScale3D().X;
@@ -1760,9 +2003,11 @@ void UVoxelContentManager::SpawnLandmarkInstance(const FStrateLandmark& L, uint3
Out.Orb.WorldPos = Xf.GetLocation();
Out.Orb.Color = L.OrbColor;
Out.Orb.Intensity = L.OrbIntensity;
Out.Orb.RadiusWorld = L.OrbRadiusVoxels * VOXEL_SIZE * Scale;
Out.Orb.FalloffWorld = L.OrbFalloffVoxels * VOXEL_SIZE;
Out.Orb.MaxShadowDistWorld = L.OrbMaxShadowDistanceVoxels * VOXEL_SIZE;
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;
}
}
@@ -1849,13 +2094,16 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
LastLandmarkStrate = StrateIndex;
}
const float ChunkWorld = (float)CHUNK_SIZE * VOXEL_SIZE; // one chunk footprint in cm
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.TopVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(TopZ) : TopZ) / VOXEL_SIZE;
Ctx.BottomVoxelZ = (VoxelWorld ? VoxelWorld->FromStrateManagerLocalZ(BotZ) : BotZ) / VOXEL_SIZE;
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);
@@ -1971,8 +2219,8 @@ void UVoxelContentManager::UpdateLandmarks(const FVector& PlayerWorldPos)
// 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 * VOXEL_SIZE;
const float LocalY = MouthVox.Y * VOXEL_SIZE;
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;
@@ -2066,11 +2314,13 @@ void UVoxelContentManager::UpdateWater(const FVector& PlayerWorldPos)
}
// Player chunk (voxel→chunk) for the strate / water-level lookup.
const float ChunkWorld = (float)CHUNK_SIZE * VOXEL_SIZE; // 1 chunk footprint in cm
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 / ChunkWorld),
FMath::FloorToInt(ManagerLocalPos.Y / ChunkWorld),
FMath::FloorToInt(ManagerLocalPos.Z / ChunkWorld));
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);
@@ -2185,7 +2435,9 @@ void UVoxelContentManager::QueryDecoDebugAt(const FVector& LocalPos, bool& bAppl
CellsTotal = R * R;
// World-LOCAL XY → deco cell → region (same math the scatter uses).
const float CellWorld = (float)DECO_CELL_VOXELS * VOXEL_SIZE;
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);