wip(assembler): round S2 (Sol) -- shared per-tile feature queries for every Core family; island placement 30x faster
Island-only worker context replaced by a shared worker-local candidate domain (WorldAssemblyFeatureTileListV1 / ScopedWorldAssemblyFeatureQueriesV1) serving SurfaceWorld, FloatingIslands and far-shell queries; a per-domain cost hint keeps the full scan on small plans. Content placement prepares the same queries and skips proven air: owner island cell p95 7.07 -> 0.24 ms; populated fixture 572 exact placements, 3.27 s -> 29 ms. Full/candidate hashes equal with dropped-feature controls on both families; S1 default W1/W8/W16 hashes reproduced; Core-off and M2 fixtures pass. Default contract for future families in ARCHITECTURE 8.23. Run on gpt-6.1-sol medium. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
d6e69420e8
commit
6ddb834b1c
19 files changed
+1849
-132
No files matched your search
@@ -1326,6 +1326,44 @@ figures measure anchor-search work and steady update overhead, not spawn/asset-i
|
||||
See Standalone/Research/WorldAssemblerM3/Round5/REPORT.md for the test matrix, owner-plan hash proof,
|
||||
and content-placement measurements.
|
||||
|
||||
### 8.23 Shared assembler query domains — World Assembler S2 / M3 Round 6
|
||||
|
||||
Meshing, far-shell field queries and placement share `ScopedWorldAssemblyFeatureQueriesV1`.
|
||||
Each family prepares conservative candidates for an inclusive domain, including normal halos.
|
||||
Scopes borrow immutable recipe/plan pointers from the owning world/snapshot, live on the worker,
|
||||
restore the previous scope when nested, and fall back to full evaluation outside their domain.
|
||||
Queues, generation epochs, edit locks, streaming policy and canonical field arithmetic are unchanged.
|
||||
`candidateEvaluationUseful` is a cost hint resolved once per domain: the measured one-feature
|
||||
SurfaceWorld and four-island plans prefer the full evaluator. Larger plans use candidates. Validation
|
||||
can force candidates to prove the bounds even for a small plan; this never changes recipe identity.
|
||||
|
||||
The default family contract for caves, shafts, pits, passages and rivers is:
|
||||
|
||||
1. Declare conservative inclusive influence bounds, including blend, displacement and warp.
|
||||
Unknown influence retains the feature. Keep canonical operation order.
|
||||
2. If remote features can determine exact positive density, provide sound value-side certificates
|
||||
or evaluate them. Geometric AABBs alone cannot preserve arbitrary density values; island root
|
||||
ellipsoids retain S1's slope and smooth-min certificate. Optional union-group data belongs to the
|
||||
family proof, not to the engine's notion of Euclidean distance.
|
||||
3. Families with a proven air base may declare conservative solid-support bounds. The shared column
|
||||
union skips only proven air; solid-base caves and unknown support retain their ordinary march.
|
||||
Preserve the original sampling phase, crossing count, bisection and final interpolation.
|
||||
4. Audit full-scan versus candidates, halos and outside-domain fallback with equal hashes and a
|
||||
deliberately removed needed feature before enabling a new proof.
|
||||
|
||||
SurfaceWorld's biome-site search remains its bounded local lattice. Cached XY column evaluation
|
||||
uses finite feature candidates; types 1/3 affect every Z, while types 2/4 are neutral outside their
|
||||
validated boxes. Shelf source-column queries outside the scope use the full column evaluator.
|
||||
Core meshing prepares both native families, including cross-strate tiles. FarShells remains derived
|
||||
repair geometry with its existing connector/stratum filtering; its canonical field calls benefit
|
||||
from the same scope wherever their samples lie inside the domain.
|
||||
|
||||
Content snapshots declare support bounds once. Decoration cells and landmark columns prepare the
|
||||
shared domain with a gradient halo, and retain exact crossings. Core-off retains its legacy march.
|
||||
Validation compares complete placement transforms and canonical anchor residuals, and applies then
|
||||
clears an isolated populated HISM fixture under BuildHost null RHI. These CPU costs do not measure
|
||||
rendering or GPU upload. See the Round 6 report for results and limits.
|
||||
|
||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||
|
||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||
|
||||
+19
-5
@@ -882,11 +882,25 @@ uses the owner/Core ratio as well. Edits use sequenced `applyEditTransaction` ca
|
||||
Core work drains, then regenerate Core's affected keys under a new epoch. Existing streaming anchors
|
||||
participate; the headless move proof covers `CollisionOnly` policy.
|
||||
|
||||
The reusable `BuildWorldAssemblyFeatureTileListV1` accepts conservative inclusive feature bounds for
|
||||
future caves, shafts, pits, and passages. Floating Islands builds one `FloatingIslandTileFeatureListV1`
|
||||
per tile and the scoped worker context makes `FloatingIslandFieldV1::densityAt` evaluate only that
|
||||
candidate list. Candidate order stays canonical, the field hash remains bit-identical, and a dropped-
|
||||
feature negative control is part of the standalone Round 5 audit.
|
||||
The reusable `BuildWorldAssemblyFeatureTileListV1` accepts conservative inclusive feature bounds.
|
||||
`ScopedWorldAssemblyFeatureQueriesV1` replaces the island-only ShapeWorld worker context: meshing,
|
||||
derived shells, decoration cells and landmark columns share its nestable worker scope. Recipe/plan
|
||||
identity and an inclusive domain guard each query; outside queries fall back to the full evaluator.
|
||||
See ARCHITECTURE §8.23 and WorldAssemblerM3/Round6/REPORT.md.
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `WorldAssemblyFeatureTileListV1`, `WorldAssemblyFeatureGroupBoundsV1`, `WorldAssemblyFeatureQueryV1` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Stable-order candidates, optional exact value-side group certificates and borrowed recipe/plan identity. Old island names remain aliases. |
|
||||
| `ScopedWorldAssemblyFeatureQueriesV1::{find,ScopedWorldAssemblyFeatureQueriesV1}`, `WorldAssemblyFeatureTileListV1::candidateEvaluationUseful` | `Standalone/Core/src/WorldAssemblerInternal.h` / public header | Worker-local domain, cost hint resolved once per query, forced validation, nested restoration and full-scan fallback; replaces ShapeWorld's island context. |
|
||||
| `Heightfield2D::buildFeatureTileList`, `world_assembly_detail::FeatureRange` | `Standalone/Core/src/WorldAssemblerInternal.h` | XY bounds for column writers; exact no-op bounds for finite boxes/shelves; canonical operation order. |
|
||||
| `FloatingIslandFieldV1::{solidSupportBounds,buildFeatureTileList,densitySubvoxels}` | `Standalone/Core/src/FloatingIslandsInternal.h` | Finite support declarations, root-aware distance certificates, automatic shared-scope consumption. |
|
||||
| `WorldAssemblyFeatureColumnRangeV1`, `BuildWorldAssemblyFeatureColumnRangeV1` | Public Core header / `Standalone/Core/src/WorldAssemblerInternal.h` | Conservative column support union for air-base families; invalid bounds are unknown. |
|
||||
| `VF_ContentFeatureQueries`, `VF_ConstrainContentMarch` | `Private/VoxelContentManager.cpp` | Immutable queries prepared per cell/landmark; skips proven air while preserving march phase and exact crossing refinement. |
|
||||
| `UVoxelContentManager::MeasurePlacementCostForValidation` | `Private/VoxelContentManager.cpp` | Authored/populated full-versus-candidate transforms, canonical anchor checks, batch and isolated HISM application cost. |
|
||||
| `UVoxelContentManager::ApplyRegion` | `Private/VoxelContentManager.cpp` / `Public/VoxelContentManager.h` | Normal region application retains landmark footprints; its default-true footprint flag isolates the temporary populated validation fixture. |
|
||||
| `RunCoreSharedFeatureCostAudit`, `RunCoreContentPlacementCostAudit` | `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` | Paired owner-plan tile/shell timings and hashes, placement metrics and transient BuildHost plan exports. |
|
||||
| `TileGenerationOptions::useAssemblyFeatureQueries`, `TileGenerationStats::{featureListMilliseconds,farShellMilliseconds}` | Public Core header / `Standalone/Core/src/VoxelForgeCore.cpp` | Default-on candidate route, full-scan validation control and stage timing; no identity change. |
|
||||
| `SharedFeatureListAudit.cpp`, `BuildSharedFeatureListAudit.ps1` | `Standalone/Research/WorldAssemblerM3/Round6/` | Owner-plan/stress hashes, shared-scope probes and deliberately dropped needed features. |
|
||||
|
||||
`-coreworldtest=1` in the BuildHost editor commandlet validates initial tiles, expected collision
|
||||
bodies/empty tiles, anchor movement, a carve transaction, travel to the B landing and native surface
|
||||
|
||||
@@ -133,6 +133,7 @@ struct FCoreFloatingIslandFieldSnapshot
|
||||
VoxelForgeCore::FloatingIslandRecipeV1 Recipe;
|
||||
VoxelForgeCore::FloatingIslandPlanV1 Plan;
|
||||
TUniquePtr<VoxelForgeCore::FloatingIslandFieldV1> Field;
|
||||
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> SolidSupportBounds;
|
||||
};
|
||||
|
||||
struct FCoreFloatingIslandSnapshot
|
||||
@@ -231,6 +232,49 @@ struct FCoreFloatingIslandSnapshot
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
@@ -330,6 +374,7 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt
|
||||
IslandPlan.strateIndex);
|
||||
continue;
|
||||
}
|
||||
Entry->SolidSupportBounds = Entry->Field->solidSupportBounds();
|
||||
IslandSnapshot->Fields.Add(IslandPlan.strateIndex, Entry);
|
||||
}
|
||||
if (!IslandSnapshot->Fields.IsEmpty())
|
||||
@@ -799,7 +844,7 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
|
||||
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;
|
||||
TArray<double> CellMilliseconds, FullScanCellMilliseconds;
|
||||
CellMilliseconds.Reserve(SampleCellCount);
|
||||
int64 TotalColumns = 0;
|
||||
int64 TotalDensityQueries = 0;
|
||||
@@ -810,7 +855,7 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
|
||||
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();
|
||||
double CellElapsed = 0.0, FullScanCellElapsed = 0.0;
|
||||
for (const auto& GridAudit : {
|
||||
std::make_pair(&NearGrid, &NearEntriesAudit),
|
||||
std::make_pair(&FarGrid, &FarEntriesAudit)})
|
||||
@@ -822,9 +867,26 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
|
||||
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;
|
||||
@@ -875,7 +937,10 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
|
||||
}
|
||||
}
|
||||
}
|
||||
CellMilliseconds.Add((FPlatformTime::Seconds() - CellStart) * 1000.0);
|
||||
CellMilliseconds.Add(CellElapsed);
|
||||
FullScanCellMilliseconds.Add(FullScanCellElapsed);
|
||||
OutAudit.ActiveBatchMilliseconds += CellElapsed;
|
||||
OutAudit.FullScanActiveBatchMilliseconds += FullScanCellElapsed;
|
||||
}
|
||||
|
||||
auto MedianAndP95 = [](TArray<double> Samples, double& OutMedian, double& OutP95)
|
||||
@@ -899,6 +964,117 @@ bool UVoxelContentManager::MeasurePlacementCostForValidation(
|
||||
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);
|
||||
@@ -1092,7 +1268,7 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const TArray<int32>& EntryBiome, uint32 InSeed,
|
||||
int32 Spacing, float Step, int32 MaxCrossings, float ColumnDepth,
|
||||
TArray<FDecoSpawn>& OutSpawns,
|
||||
FPlacementBuildCounters* OutCounters)
|
||||
FPlacementBuildCounters* OutCounters, bool bUseFeatureQueries)
|
||||
{
|
||||
if (OutCounters != nullptr) *OutCounters = FPlacementBuildCounters();
|
||||
if (!Gen || Entries.Num() == 0) return;
|
||||
@@ -1101,6 +1277,14 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
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;
|
||||
|
||||
@@ -1437,12 +1621,17 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
}
|
||||
|
||||
// NON-SURFACE archetypes (caves/shafts/islands): ray-march the density column for crossings.
|
||||
float PrevD = D(VX, VY, Ctx.TopVoxelZ);
|
||||
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 = Ctx.TopVoxelZ - Step; Z >= Ctx.BottomVoxelZ && Crossings < MaxCrossings; Z -= Step)
|
||||
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)
|
||||
@@ -1615,7 +1804,8 @@ void UVoxelContentManager::MarkCellDone(FDecoGrid& G, const FIntPoint& Region, c
|
||||
|
||||
// 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)
|
||||
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
|
||||
|
||||
@@ -1714,7 +1904,7 @@ void UVoxelContentManager::ApplyRegion(FDecoGrid& G, const FIntPoint& Region, FD
|
||||
// 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 (Content.Instances.Num() > 0)
|
||||
if (bApplyLandmarkFootprints && Content.Instances.Num() > 0)
|
||||
{
|
||||
for (const TPair<FIntVector, FLandmarkInstance>& LP : LandmarkInstances)
|
||||
{
|
||||
@@ -1781,6 +1971,11 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const
|
||||
{
|
||||
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())
|
||||
@@ -1852,10 +2047,15 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const
|
||||
// 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 PrevD = Density(VX, VY, Ctx.TopVoxelZ);
|
||||
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 = Ctx.TopVoxelZ - Step; Z >= Ctx.BottomVoxelZ; Z -= Step)
|
||||
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
|
||||
|
||||
@@ -137,6 +137,19 @@ public:
|
||||
double MaximumDensityResidual = 0.0;
|
||||
double DensityQueriesPerCell = 0.0;
|
||||
double SurfaceQueriesPerCell = 0.0;
|
||||
double FullScanCellP95Milliseconds = 0.0;
|
||||
double ActiveBatchMilliseconds = 0.0;
|
||||
double FullScanActiveBatchMilliseconds = 0.0;
|
||||
double PopulatedBatchMilliseconds = 0.0;
|
||||
double FullScanPopulatedBatchMilliseconds = 0.0;
|
||||
double PopulatedMergeMilliseconds = 0.0;
|
||||
double PopulatedApplyMilliseconds = 0.0;
|
||||
int32 AppliedInstances = 0;
|
||||
double PopulatedCellP95Milliseconds = 0.0;
|
||||
double FullScanPopulatedCellP95Milliseconds = 0.0;
|
||||
int32 PopulatedPlacements = 0;
|
||||
int32 PopulatedCells = 0;
|
||||
int32 ExactPlacementComparisons = 0;
|
||||
double CellMedianMilliseconds = 0.0;
|
||||
double CellP95Milliseconds = 0.0;
|
||||
double UpdateMedianMilliseconds = 0.0;
|
||||
@@ -338,7 +351,7 @@ private:
|
||||
const TArray<int32>& EntryBiome, uint32 InSeed,
|
||||
int32 Spacing, float Step, int32 MaxCrossings, float ColumnDepth,
|
||||
TArray<FDecoSpawn>& OutSpawns,
|
||||
FPlacementBuildCounters* OutCounters = nullptr);
|
||||
FPlacementBuildCounters* OutCounters = nullptr, bool bUseFeatureQueries = true);
|
||||
|
||||
// Each step operates on ONE grid (G = NearGrid or FarGrid). LaunchDecoTasks throttles against the
|
||||
// COMBINED in-flight count (OtherInFlight = the other grid's in-flight cells) so the two grids share
|
||||
@@ -348,7 +361,8 @@ private:
|
||||
void ProcessDecoResults(const FIntPoint& PlayerCell);
|
||||
void MergeCellResult(FDecoGrid& G, const FDecoCellResult& Result); // fold one cell's spawns into its region build
|
||||
void MarkCellDone(FDecoGrid& G, const FIntPoint& Region, const FIntPoint& Cell, uint32 BuildId); // idempotent per-cell accounting
|
||||
void ApplyRegion(FDecoGrid& G, const FIntPoint& Region, FDecoRegionBuild& Build);
|
||||
void ApplyRegion(FDecoGrid& G, const FIntPoint& Region, FDecoRegionBuild& Build,
|
||||
bool bApplyLandmarkFootprints = true);
|
||||
void RebuildDesiredCells(FDecoGrid& G, const FIntPoint& PlayerCell);
|
||||
void ClearDecorationRegion(FDecoGrid& G, const FIntPoint& Region);
|
||||
void ClearAllDecorations();
|
||||
|
||||
@@ -15147,6 +15147,53 @@ bool RunCoreOffActorTest(const FExploreArguments& Arguments, FString& OutError)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Paired engine timings include preparation and derived shells, with canonical output hashes.
|
||||
// Mesures moteur appariees : preparation et coques derivees incluses, avec hashes canoniques.
|
||||
bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World,
|
||||
const std::vector<VoxelForgeCore::TileKey>& Keys,const TCHAR* Label,FString& OutError)
|
||||
{
|
||||
for (const int32 LOD : {0,1,7,8})
|
||||
{
|
||||
TArray<double> FullTimes,CandidateTimes,FullShellTimes,CandidateShellTimes,ListTimes;
|
||||
for (const auto& Key : Keys)
|
||||
{
|
||||
if (Key.lod!=LOD || FullTimes.Num()>=8) continue;
|
||||
VoxelForgeCore::TileGenerationOptions Options;
|
||||
Options.hashDensity=true;
|
||||
Options.useAssemblyFeatureQueries=false;
|
||||
std::string Error;
|
||||
// Warm both routes before measuring: column/page caches must not favor the second route.
|
||||
// Chauffer les deux parcours : les caches ne doivent pas favoriser le second.
|
||||
if (!World.generateTile(Key,Options,Error)) return false;
|
||||
Options.useAssemblyFeatureQueries=true;
|
||||
if (!World.generateTile(Key,Options,Error)) return false;
|
||||
Options.useAssemblyFeatureQueries=false;
|
||||
const double FullStart=FPlatformTime::Seconds();
|
||||
auto Full=World.generateTile(Key,Options,Error);
|
||||
FullTimes.Add((FPlatformTime::Seconds()-FullStart)*1000.0);
|
||||
Options.useAssemblyFeatureQueries=true;
|
||||
const double CandidateStart=FPlatformTime::Seconds();
|
||||
auto Candidate=World.generateTile(Key,Options,Error);
|
||||
CandidateTimes.Add((FPlatformTime::Seconds()-CandidateStart)*1000.0);
|
||||
if (!Full||!Candidate||Full->densityHash!=Candidate->densityHash
|
||||
||Full->meshHash!=Candidate->meshHash||Full->payloadHash!=Candidate->payloadHash
|
||||
||Full->topologyHash!=Candidate->topologyHash)
|
||||
{ OutError=TEXT("Shared feature full/candidate tile hashes differ."); return false; }
|
||||
FullShellTimes.Add(Full->diagnostics.farShellMilliseconds);
|
||||
CandidateShellTimes.Add(Candidate->diagnostics.farShellMilliseconds);
|
||||
ListTimes.Add(Candidate->diagnostics.featureListMilliseconds);
|
||||
}
|
||||
const auto P95=[](TArray<double> Values)
|
||||
{ Values.Sort(); return Values.IsEmpty() ? 0.0 : Values[FMath::Clamp(
|
||||
FMath::CeilToInt(Values.Num()*0.95f)-1,0,Values.Num()-1)]; };
|
||||
UE_LOG(LogTemp,Display,
|
||||
TEXT("[VoxelForgeSharedFeatureCost] label=%s lod=%d tiles=%d hashes_identical=1 full_p95_ms=%.5f candidate_p95_ms=%.5f list_p95_ms=%.5f full_shell_p95_ms=%.5f candidate_shell_p95_ms=%.5f"),
|
||||
Label,LOD,FullTimes.Num(),P95(FullTimes),P95(CandidateTimes),P95(ListTimes),
|
||||
P95(FullShellTimes),P95(CandidateShellTimes));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunCoreContentPlacementCostAudit(AVoxelWorld* Actor, const FVector& CoreLocalProbeCm,
|
||||
const TCHAR* Label, FString& OutError);
|
||||
|
||||
@@ -15382,6 +15429,7 @@ bool RunCoreFloatingIslandActorIntegrationTest(
|
||||
return false;
|
||||
}
|
||||
ReferenceWorld.retainTileRegions(Desired);
|
||||
if (!RunCoreSharedFeatureCostAudit(ReferenceWorld,Desired,TEXT("islands"),OutError)) return false;
|
||||
VoxelForgeCore::TileGenerationOptions Options;
|
||||
Options.hashDensity = true;
|
||||
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
|
||||
@@ -16272,6 +16320,15 @@ bool RunCoreContentPlacementCostAudit(AVoxelWorld* Actor, const FVector& CoreLoc
|
||||
const float CoreVoxelSizeCm = Actor->CoreVoxelSizeCm;
|
||||
const FTransform OwnerXf = Actor->GetActorTransform();
|
||||
|
||||
FString AssemblyJson, AssemblyHash;
|
||||
if (Actor->GetVoxelForgeCoreWorldAssembly(AssemblyJson,AssemblyHash))
|
||||
{
|
||||
const FString ExportDir=FPaths::Combine(FPaths::ProjectSavedDir(),TEXT("CoreWorldValidation/Output"));
|
||||
IFileManager::Get().MakeDirectory(*ExportDir,true);
|
||||
FFileHelper::SaveStringToFile(AssemblyJson,
|
||||
*FPaths::Combine(ExportDir,FString::Printf(TEXT("s2_assembly_%s.json"),Label)));
|
||||
}
|
||||
|
||||
UVoxelContentManager::FDecorationPlacementCostAudit CoreAudit;
|
||||
Actor->bUseVoxelForgeCore = true;
|
||||
const FVector CorePlayerWorld = OwnerXf.TransformPosition(CoreLocalProbeCm);
|
||||
@@ -16319,6 +16376,20 @@ bool RunCoreContentPlacementCostAudit(AVoxelWorld* Actor, const FVector& CoreLoc
|
||||
CoreAudit.CellP95Milliseconds, CoreAudit.UpdateMedianMilliseconds,
|
||||
CoreAudit.UpdateP95Milliseconds, CoreAudit.Placements, CoreAudit.AnchorChecks,
|
||||
CoreAudit.MaximumAnchorErrorCm, CoreAudit.MaximumDensityResidual);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeContentCostS2] label=%s authored_full_p95_ms=%.5f authored_candidate_p95_ms=%.5f authored_active_batch_full_ms=%.5f authored_active_batch_candidate_ms=%.5f populated_cells=%d populated_placements=%d populated_full_p95_ms=%.5f populated_candidate_p95_ms=%.5f exact_transform_comparisons=%d anchor_checks=%d max_density_residual=%.6f"),
|
||||
Label,CoreAudit.FullScanCellP95Milliseconds,CoreAudit.CellP95Milliseconds,
|
||||
CoreAudit.FullScanActiveBatchMilliseconds,CoreAudit.ActiveBatchMilliseconds,
|
||||
CoreAudit.PopulatedCells,CoreAudit.PopulatedPlacements,
|
||||
CoreAudit.FullScanPopulatedCellP95Milliseconds,CoreAudit.PopulatedCellP95Milliseconds,
|
||||
CoreAudit.ExactPlacementComparisons,CoreAudit.AnchorChecks,CoreAudit.MaximumDensityResidual);
|
||||
UE_LOG(LogTemp,Display,
|
||||
TEXT("[VoxelForgeContentActiveS2] label=%s cells=%d placements=%d full_batch_ms=%.5f candidate_batch_ms=%.5f merge_ms=%.5f apply_ms=%.5f applied_instances=%d full_total_cpu_ms=%.5f candidate_total_cpu_ms=%.5f rhi=null"),
|
||||
Label,CoreAudit.SampledCells,CoreAudit.PopulatedPlacements,
|
||||
CoreAudit.FullScanPopulatedBatchMilliseconds,CoreAudit.PopulatedBatchMilliseconds,
|
||||
CoreAudit.PopulatedMergeMilliseconds,CoreAudit.PopulatedApplyMilliseconds,CoreAudit.AppliedInstances,
|
||||
CoreAudit.FullScanPopulatedBatchMilliseconds+CoreAudit.PopulatedMergeMilliseconds+CoreAudit.PopulatedApplyMilliseconds,
|
||||
CoreAudit.PopulatedBatchMilliseconds+CoreAudit.PopulatedMergeMilliseconds+CoreAudit.PopulatedApplyMilliseconds);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeContentCost] label=%s route=legacy voxel_cm=%.3f tile_cell_cm=%.3f authored_entries=%d synthetic=%d cells=%d columns_per_cell=%d density_queries_per_cell=%.2f surface_queries_per_cell=%.2f cell_median_ms=%.5f cell_p95_ms=%.5f update_median_ms=%.5f update_p95_ms=%.5f placements=%d anchor_checks=%d max_anchor_error_cm=%.6f max_density_residual=%.6f"),
|
||||
Label, VOXEL_SIZE, CHUNK_SIZE * VOXEL_SIZE,
|
||||
@@ -16767,6 +16838,12 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!RunCoreSharedFeatureCostAudit(ReferenceWorld,Desired,TEXT("surface"),OutError)) return false;
|
||||
if (FParse::Param(FCommandLine::Get(),TEXT("corefeatureauditonly")))
|
||||
{
|
||||
UE_LOG(LogTemp,Display,TEXT("[VoxelForgeSharedFeatureCost] pass complete=1 warmed_routes=1"));
|
||||
return true;
|
||||
}
|
||||
VoxelForgeCore::TileGenerationOptions Options;
|
||||
Options.hashDensity = true;
|
||||
if (Arguments.CoreHybridMesherVersion == 2)
|
||||
|
||||
@@ -56,6 +56,18 @@ namespace VoxelForgeCore
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
const std::array<std::int64_t, 3>& tileMaximumSubvoxels);
|
||||
|
||||
struct WorldAssemblyFeatureColumnRangeV1
|
||||
{
|
||||
bool known = false;
|
||||
bool empty = false;
|
||||
std::int64_t minimumZSubvoxels = 0, maximumZSubvoxels = 0;
|
||||
};
|
||||
// Only families whose base is air may use this solid-support union to shorten a march.
|
||||
// Reserve aux familles a fond d'air : union conservative du support solide de la colonne.
|
||||
VFCORE_API WorldAssemblyFeatureColumnRangeV1 BuildWorldAssemblyFeatureColumnRangeV1(
|
||||
std::span<const WorldAssemblyFeatureBoundsV1> solidSupportBounds,
|
||||
std::int64_t xSubvoxels, std::int64_t ySubvoxels);
|
||||
|
||||
struct VFCORE_API TileKeyHash
|
||||
{
|
||||
std::size_t operator()(const TileKey& key) const noexcept;
|
||||
@@ -114,6 +126,8 @@ namespace VoxelForgeCore
|
||||
double classificationMilliseconds = 0.0;
|
||||
double columnComputeMilliseconds = 0.0;
|
||||
double meshingMilliseconds = 0.0;
|
||||
double featureListMilliseconds = 0.0;
|
||||
double farShellMilliseconds = 0.0;
|
||||
bool tileWasClassified = false;
|
||||
bool meshingSkipped = false;
|
||||
bool featureRefinedResolution = false;
|
||||
@@ -386,25 +400,60 @@ namespace VoxelForgeCore
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
|
||||
struct FloatingIslandFeatureGroupBoundsV1
|
||||
struct WorldAssemblyFeatureGroupBoundsV1
|
||||
{
|
||||
std::array<std::int64_t, 3> minimumSubvoxels{};
|
||||
std::array<std::int64_t, 3> maximumSubvoxels{};
|
||||
};
|
||||
|
||||
struct FloatingIslandTileFeatureListV1
|
||||
struct WorldAssemblyFeatureTileListV1
|
||||
{
|
||||
// Flat stable-order IDs for island and connector features that touch this tile.
|
||||
// Identifiants ordonnes des features qui touchent ce carreau.
|
||||
// Stable recipe-order indices for features that touch this query domain.
|
||||
// Indices dans l'ordre de recette des features touchant ce domaine.
|
||||
std::vector<std::uint32_t> featureIndices;
|
||||
// Cheap conservative group bounds preserve exact positive SDF distances while distant
|
||||
// feature evaluators are skipped. Bornes legeres pour garder les distances SDF exactes.
|
||||
std::vector<FloatingIslandFeatureGroupBoundsV1> groupBounds;
|
||||
// Optional ordered-union group certificates preserve exact positive density values.
|
||||
// Certificats optionnels de groupes d'union pour conserver les valeurs positives exactes.
|
||||
std::vector<WorldAssemblyFeatureGroupBoundsV1> groupBounds;
|
||||
// Groups are ordered by a tile-wide conservative distance lower bound. Samples stop at
|
||||
// the first group that cannot beat their current density. Ordre par distance minimale.
|
||||
std::vector<std::uint32_t> groupSearchOrder;
|
||||
std::vector<std::int64_t> groupDistanceLowerBoundsSubvoxels;
|
||||
bool groupSearchComplete = true;
|
||||
// Performance hint only: the shared engine may prefer the exact full scan on small plans.
|
||||
// Indication de cout seulement : le moteur peut preferer le parcours complet exact.
|
||||
bool candidateEvaluationUseful = true;
|
||||
// Exact-domain guard: outside queries always fall back to the canonical full scan.
|
||||
// Hors du domaine prepare, revenir au parcours complet canonique.
|
||||
std::array<std::int64_t, 3> minimumSubvoxels{};
|
||||
std::array<std::int64_t, 3> maximumSubvoxels{};
|
||||
};
|
||||
|
||||
using FloatingIslandTileFeatureListV1 = WorldAssemblyFeatureTileListV1;
|
||||
using FloatingIslandFeatureGroupBoundsV1 = WorldAssemblyFeatureGroupBoundsV1;
|
||||
|
||||
struct WorldAssemblyFeatureQueryV1
|
||||
{
|
||||
const void* recipe = nullptr;
|
||||
const void* plan = nullptr;
|
||||
WorldAssemblyFeatureTileListV1 features;
|
||||
};
|
||||
|
||||
// One worker-local, nestable scope for meshing, shells and placement in every family.
|
||||
// Une portee locale au worker, emboitable, pour toutes les familles et tous les consommateurs.
|
||||
class VFCORE_API ScopedWorldAssemblyFeatureQueriesV1
|
||||
{
|
||||
public:
|
||||
explicit ScopedWorldAssemblyFeatureQueriesV1(std::span<const WorldAssemblyFeatureQueryV1> queries,
|
||||
bool forceCandidatesForValidation = false) noexcept;
|
||||
~ScopedWorldAssemblyFeatureQueriesV1();
|
||||
ScopedWorldAssemblyFeatureQueriesV1(const ScopedWorldAssemblyFeatureQueriesV1&) = delete;
|
||||
ScopedWorldAssemblyFeatureQueriesV1& operator=(const ScopedWorldAssemblyFeatureQueriesV1&) = delete;
|
||||
static const WorldAssemblyFeatureTileListV1* find(const void* recipe, const void* plan,
|
||||
std::int64_t x, std::int64_t y, std::int64_t z, bool columnOnly = false) noexcept;
|
||||
private:
|
||||
std::span<const WorldAssemblyFeatureQueryV1> queries_;
|
||||
bool forceCandidates_ = false;
|
||||
const ScopedWorldAssemblyFeatureQueriesV1* previous_ = nullptr;
|
||||
};
|
||||
|
||||
struct VFCORE_API FloatingIslandManifestV1
|
||||
@@ -510,6 +559,7 @@ namespace VoxelForgeCore
|
||||
FloatingIslandTileFeatureListV1 buildFeatureTileList(
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
const std::array<std::int64_t, 3>& tileMaximumSubvoxels) const;
|
||||
std::vector<WorldAssemblyFeatureBoundsV1> solidSupportBounds() const;
|
||||
std::int64_t densitySubvoxelsForTile(std::int64_t xSubvoxels,
|
||||
std::int64_t ySubvoxels, std::int64_t zSubvoxels,
|
||||
const FloatingIslandTileFeatureListV1& featureList) const noexcept;
|
||||
@@ -601,6 +651,9 @@ namespace VoxelForgeCore
|
||||
bool isValid() const noexcept;
|
||||
HeightfieldPointSample point(std::int64_t xSubvoxels, std::int64_t ySubvoxels,
|
||||
std::int64_t zSubvoxels) const noexcept;
|
||||
WorldAssemblyFeatureTileListV1 buildFeatureTileList(
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
const std::array<std::int64_t, 3>& tileMaximumSubvoxels) const;
|
||||
HeightfieldBoxClass classifyLatticeBox(std::int64_t minimumXSubvoxels,
|
||||
std::int64_t minimumYSubvoxels, std::int64_t minimumZSubvoxels,
|
||||
std::int64_t maximumXSubvoxels, std::int64_t maximumYSubvoxels,
|
||||
@@ -928,6 +981,9 @@ namespace VoxelForgeCore
|
||||
bool skipUv = false;
|
||||
bool skipColors = false;
|
||||
bool skipMaterialGroups = false;
|
||||
// Validation control only; never participates in recipe/world identity.
|
||||
// Controle de validation seulement, sans effet sur l'identite du monde.
|
||||
bool useAssemblyFeatureQueries = true;
|
||||
};
|
||||
|
||||
class VFCORE_API World
|
||||
|
||||
@@ -1645,18 +1645,18 @@ bool FloatingIslandFieldV1::isValid() const noexcept
|
||||
std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x,
|
||||
std::int64_t y, std::int64_t z) const noexcept
|
||||
{
|
||||
// The selected evaluator validates once; do not scan the plan twice per scoped sample.
|
||||
// L'evaluateur choisi valide une seule fois; ne pas reparcourir le plan a chaque echantillon.
|
||||
if (const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find(recipe_, plan_, x, y, z))
|
||||
return densitySubvoxelsForTile(x, y, z, *list);
|
||||
if (!isValid() || !floating_island_detail::ValidScalar(x)
|
||||
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
||||
return floating_island_detail::MaxDensity;
|
||||
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
||||
}
|
||||
|
||||
FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
const std::array<std::int64_t, 3>& tileMaximumSubvoxels) const
|
||||
std::vector<WorldAssemblyFeatureBoundsV1> FloatingIslandFieldV1::solidSupportBounds() const
|
||||
{
|
||||
FloatingIslandTileFeatureListV1 result;
|
||||
if (!isValid()) return result;
|
||||
std::vector<WorldAssemblyFeatureBoundsV1> features;
|
||||
features.reserve(plan_->islands.size() + plan_->clusterEdges.size());
|
||||
for (const auto& island : plan_->islands)
|
||||
@@ -1666,6 +1666,22 @@ FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
||||
features.push_back({edge.stableId, edge.influenceMinimumSubvoxels,
|
||||
edge.influenceMaximumSubvoxels});
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
const std::array<std::int64_t, 3>& tileMaximumSubvoxels) const
|
||||
{
|
||||
FloatingIslandTileFeatureListV1 result;
|
||||
// The measured four-island plan is cheaper to scan; larger plans amortize group certificates.
|
||||
// Le plan mesure de quatre iles coute moins en parcours complet; les grands plans amortissent les bornes.
|
||||
result.candidateEvaluationUseful=plan_->islands.size()>4;
|
||||
result.minimumSubvoxels = tileMinimumSubvoxels;
|
||||
result.maximumSubvoxels = tileMaximumSubvoxels;
|
||||
if (!isValid()) return result;
|
||||
const auto features = solidSupportBounds();
|
||||
|
||||
result.featureIndices = BuildWorldAssemblyFeatureTileListV1(features,
|
||||
tileMinimumSubvoxels, tileMaximumSubvoxels);
|
||||
const std::size_t islandCount = plan_->islands.size();
|
||||
|
||||
@@ -32,43 +32,6 @@
|
||||
class FStandaloneShapeWorld
|
||||
{
|
||||
public:
|
||||
struct FNativeIslandTileFeatureContext
|
||||
{
|
||||
const VoxelForgeCore::FloatingIslandPlanV1* Plan = nullptr;
|
||||
const VoxelForgeCore::FloatingIslandTileFeatureListV1* FeatureList = nullptr;
|
||||
};
|
||||
class FScopedNativeIslandTileFeatureContext
|
||||
{
|
||||
public:
|
||||
FScopedNativeIslandTileFeatureContext(const VoxelForgeCore::FloatingIslandPlanV1* plan,
|
||||
const VoxelForgeCore::FloatingIslandTileFeatureListV1* featureList)
|
||||
{
|
||||
Context.Plan = plan;
|
||||
Context.FeatureList = featureList;
|
||||
Previous = SetNativeIslandTileFeatureContext(plan && featureList ? &Context : nullptr);
|
||||
}
|
||||
~FScopedNativeIslandTileFeatureContext()
|
||||
{ RestoreNativeIslandTileFeatureContext(Previous); }
|
||||
FScopedNativeIslandTileFeatureContext(const FScopedNativeIslandTileFeatureContext&) = delete;
|
||||
FScopedNativeIslandTileFeatureContext& operator=(const FScopedNativeIslandTileFeatureContext&) = delete;
|
||||
|
||||
private:
|
||||
FNativeIslandTileFeatureContext Context{};
|
||||
const FNativeIslandTileFeatureContext* Previous = nullptr;
|
||||
};
|
||||
static const FNativeIslandTileFeatureContext* SetNativeIslandTileFeatureContext(
|
||||
const FNativeIslandTileFeatureContext* context)
|
||||
{
|
||||
auto& active = ActiveNativeIslandTileFeatureContext();
|
||||
const auto* previous = active;
|
||||
active = context;
|
||||
return previous;
|
||||
}
|
||||
static void RestoreNativeIslandTileFeatureContext(const FNativeIslandTileFeatureContext* context)
|
||||
{ ActiveNativeIslandTileFeatureContext() = context; }
|
||||
static const FNativeIslandTileFeatureContext* GetNativeIslandTileFeatureContext()
|
||||
{ return ActiveNativeIslandTileFeatureContext(); }
|
||||
|
||||
double GetCoreVoxelSizeCm() const
|
||||
{
|
||||
return Owner == nullptr || Owner->CoreVoxelSizeMicrons == 0
|
||||
@@ -1140,12 +1103,6 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static const FNativeIslandTileFeatureContext*& ActiveNativeIslandTileFeatureContext()
|
||||
{
|
||||
static thread_local const FNativeIslandTileFeatureContext* Context = nullptr;
|
||||
return Context;
|
||||
}
|
||||
|
||||
static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z)
|
||||
{
|
||||
constexpr uint64 Mask=0x1FFFFFull;
|
||||
@@ -1961,13 +1918,7 @@ private:
|
||||
const VoxelForgeCore::FloatingIslandFieldV1 Field(
|
||||
Plan->manifest.recipe,Plan->manifest.plan);
|
||||
if(!Field.isValid())return false;
|
||||
std::int64_t Density=0;
|
||||
const auto* TileContext=GetNativeIslandTileFeatureContext();
|
||||
if(TileContext&&TileContext->Plan==&Plan->manifest.plan&&TileContext->FeatureList)
|
||||
Density=Field.densitySubvoxelsForTile(ToFixed(X),ToFixed(Y),ToFixed(Z),
|
||||
*TileContext->FeatureList);
|
||||
else
|
||||
Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
||||
std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
||||
const std::int64_t Bottom=Plan->manifest.recipe.layoutMinimumZSubvoxels;
|
||||
const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels;
|
||||
const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels;
|
||||
|
||||
@@ -1154,68 +1154,51 @@ namespace VoxelForgeCore
|
||||
}
|
||||
}
|
||||
}
|
||||
const WorldAssemblyFloatingIslandPlanV1* IslandTilePlan=nullptr;
|
||||
FloatingIslandTileFeatureListV1 IslandTileFeatureList;
|
||||
if(bHasWorldAssembly)
|
||||
// Shared query domain includes the normal halo; all overlapping strate families participate.
|
||||
// Domaine partage avec marge de normales; inclure toutes les familles des strates traversees.
|
||||
const std::int64_t queryScale=WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<std::int64_t,3> queryMinimum={
|
||||
(static_cast<std::int64_t>(origin.X)-step)*queryScale,
|
||||
(static_cast<std::int64_t>(origin.Y)-step)*queryScale,
|
||||
(static_cast<std::int64_t>(origin.Z)-step)*queryScale};
|
||||
const std::array<std::int64_t,3> queryMaximum={
|
||||
(static_cast<std::int64_t>(origin.X)+extent+step)*queryScale,
|
||||
(static_cast<std::int64_t>(origin.Y)+extent+step)*queryScale,
|
||||
(static_cast<std::int64_t>(origin.Z)+extent+step)*queryScale};
|
||||
const auto FeatureListStart=std::chrono::steady_clock::now();
|
||||
std::vector<WorldAssemblyFeatureQueryV1> featureQueries;
|
||||
if(bHasWorldAssembly&&options.useAssemblyFeatureQueries)
|
||||
{
|
||||
for(const auto& plan:impl_->worldAssembly->plan.strates)
|
||||
{
|
||||
const Heightfield2D field(impl_->worldAssembly->recipe,plan);
|
||||
featureQueries.push_back({field.recipe(),field.plan(),
|
||||
field.buildFeatureTileList(queryMinimum,queryMaximum)});
|
||||
}
|
||||
for(const auto& entry:impl_->worldAssembly->floatingIslands)
|
||||
{
|
||||
const auto& scale=entry.manifest.recipe;
|
||||
const std::int64_t ownerToCoreNumerator=scale.ownerToCoreScaleNumerator;
|
||||
const std::int64_t ownerToCoreDenominator=scale.ownerToCoreScaleDenominator;
|
||||
if(ownerToCoreNumerator<=0||ownerToCoreDenominator<=0)continue;
|
||||
const std::int64_t coreVoxelsPerOwnerChunk=static_cast<std::int64_t>(CHUNK_SIZE)
|
||||
*ownerToCoreNumerator/ownerToCoreDenominator;
|
||||
if(coreVoxelsPerOwnerChunk<=0)continue;
|
||||
const std::int64_t stratumBottom=static_cast<std::int64_t>(entry.bottomChunkZ)
|
||||
*coreVoxelsPerOwnerChunk;
|
||||
const std::int64_t stratumTop=(static_cast<std::int64_t>(entry.topChunkZ)+1)
|
||||
*coreVoxelsPerOwnerChunk;
|
||||
if(static_cast<std::int64_t>(origin.Z)>stratumTop
|
||||
||static_cast<std::int64_t>(origin.Z)+extent<stratumBottom)continue;
|
||||
IslandTilePlan=&entry;
|
||||
break;
|
||||
}
|
||||
if(IslandTilePlan)
|
||||
{
|
||||
const FloatingIslandFieldV1 field(IslandTilePlan->manifest.recipe,
|
||||
IslandTilePlan->manifest.plan);
|
||||
const std::int64_t scale=WorldAssemblySubvoxelsPerVoxel;
|
||||
// Include the mesher's one-sample gradient halo on each face.
|
||||
// Inclure la marge d'un echantillon du maillage sur chaque face.
|
||||
const std::int64_t sampleHalo=static_cast<std::int64_t>(step)*scale;
|
||||
const std::array<std::int64_t,3> tileMinimum={
|
||||
static_cast<std::int64_t>(origin.X)*scale-sampleHalo,
|
||||
static_cast<std::int64_t>(origin.Y)*scale-sampleHalo,
|
||||
static_cast<std::int64_t>(origin.Z)*scale-sampleHalo};
|
||||
const std::array<std::int64_t,3> tileMaximum={
|
||||
(static_cast<std::int64_t>(origin.X)+extent)*scale+sampleHalo,
|
||||
(static_cast<std::int64_t>(origin.Y)+extent)*scale+sampleHalo,
|
||||
(static_cast<std::int64_t>(origin.Z)+extent)*scale+sampleHalo};
|
||||
IslandTileFeatureList=field.buildFeatureTileList(tileMinimum,tileMaximum);
|
||||
const FloatingIslandFieldV1 field(entry.manifest.recipe,entry.manifest.plan);
|
||||
featureQueries.push_back({field.recipe(),field.plan(),
|
||||
field.buildFeatureTileList(queryMinimum,queryMaximum)});
|
||||
}
|
||||
}
|
||||
FVoxelMeshData mesh;
|
||||
if(IslandTilePlan)
|
||||
{
|
||||
FStandaloneShapeWorld::FScopedNativeIslandTileFeatureContext tileFeatureContext(
|
||||
&IslandTilePlan->manifest.plan,&IslandTileFeatureList);
|
||||
mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
|
||||
}
|
||||
else
|
||||
mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
|
||||
out.diagnostics.featureListMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-FeatureListStart).count();
|
||||
const ScopedWorldAssemblyFeatureQueriesV1 featureScope(featureQueries);
|
||||
FVoxelMeshData mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
|
||||
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-MeshingStart).count();
|
||||
const auto FarShellStart=std::chrono::steady_clock::now();
|
||||
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:*impl_->shape;
|
||||
if(farShape.HasFarWorld())
|
||||
{
|
||||
AppendFarStratumShells(world,farShape,nativeKey,step,mesh);
|
||||
AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh);
|
||||
}
|
||||
out.diagnostics.farShellMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-FarShellStart).count();
|
||||
if(impl_->worldAssembly&&key.lod>=8)
|
||||
{
|
||||
const auto CapStats=RefineLod8NativeCap(world,*impl_->worldAssembly,nativeKey,mesh);
|
||||
|
||||
@@ -15,6 +15,45 @@
|
||||
|
||||
namespace VoxelForgeCore
|
||||
{
|
||||
namespace
|
||||
{
|
||||
thread_local const ScopedWorldAssemblyFeatureQueriesV1* ActiveAssemblyQueries = nullptr;
|
||||
}
|
||||
|
||||
ScopedWorldAssemblyFeatureQueriesV1::ScopedWorldAssemblyFeatureQueriesV1(
|
||||
std::span<const WorldAssemblyFeatureQueryV1> queries, bool forceCandidatesForValidation) noexcept
|
||||
: queries_(queries), forceCandidates_(forceCandidatesForValidation), previous_(ActiveAssemblyQueries)
|
||||
{
|
||||
// Resolve cost hints once per domain, never rediscover them in the sample loop.
|
||||
// Resoudre les indications de cout une fois par domaine, hors de la boucle d'echantillons.
|
||||
ActiveAssemblyQueries = nullptr;
|
||||
for (const auto& query : queries_)
|
||||
if (forceCandidates_ || query.features.candidateEvaluationUseful)
|
||||
{ ActiveAssemblyQueries = this; break; }
|
||||
}
|
||||
|
||||
ScopedWorldAssemblyFeatureQueriesV1::~ScopedWorldAssemblyFeatureQueriesV1()
|
||||
{ ActiveAssemblyQueries = previous_; }
|
||||
|
||||
const WorldAssemblyFeatureTileListV1* ScopedWorldAssemblyFeatureQueriesV1::find(
|
||||
const void* recipe, const void* plan, std::int64_t x, std::int64_t y,
|
||||
std::int64_t z, bool columnOnly) noexcept
|
||||
{
|
||||
const std::array<std::int64_t, 3> point{x,y,z};
|
||||
if (ActiveAssemblyQueries)
|
||||
for (const auto& query : ActiveAssemblyQueries->queries_)
|
||||
{
|
||||
if (query.recipe != recipe || query.plan != plan
|
||||
|| (!ActiveAssemblyQueries->forceCandidates_ && !query.features.candidateEvaluationUseful)) continue;
|
||||
bool inside = true;
|
||||
for (std::size_t axis=0; axis<(columnOnly ? 2u : 3u); ++axis)
|
||||
inside = inside && point[axis]>=query.features.minimumSubvoxels[axis]
|
||||
&& point[axis]<=query.features.maximumSubvoxels[axis];
|
||||
if (inside) return &query.features;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> BuildWorldAssemblyFeatureTileListV1(
|
||||
std::span<const WorldAssemblyFeatureBoundsV1> features,
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
@@ -52,8 +91,54 @@ std::vector<std::uint32_t> BuildWorldAssemblyFeatureTileListV1(
|
||||
return result;
|
||||
}
|
||||
|
||||
WorldAssemblyFeatureColumnRangeV1 BuildWorldAssemblyFeatureColumnRangeV1(
|
||||
std::span<const WorldAssemblyFeatureBoundsV1> bounds, std::int64_t x, std::int64_t y)
|
||||
{
|
||||
WorldAssemblyFeatureColumnRangeV1 result;
|
||||
result.known=true;
|
||||
result.empty=true;
|
||||
for(const auto& bound:bounds)
|
||||
{
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(bound.minimumSubvoxels[axis]>bound.maximumSubvoxels[axis]) return {};
|
||||
if(x<bound.minimumSubvoxels[0]||x>bound.maximumSubvoxels[0]
|
||||
||y<bound.minimumSubvoxels[1]||y>bound.maximumSubvoxels[1])continue;
|
||||
if(result.empty)
|
||||
{
|
||||
result.minimumZSubvoxels=bound.minimumSubvoxels[2];
|
||||
result.maximumZSubvoxels=bound.maximumSubvoxels[2];
|
||||
result.empty=false;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.minimumZSubvoxels=std::min(result.minimumZSubvoxels,bound.minimumSubvoxels[2]);
|
||||
result.maximumZSubvoxels=std::max(result.maximumZSubvoxels,bound.maximumSubvoxels[2]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace world_assembly_detail
|
||||
{
|
||||
// Preserve recipe order without allocating or looking up metadata in the point loop.
|
||||
// Garder l'ordre de recette sans allocation ni recherche de metadonnees par point.
|
||||
struct FeatureRange
|
||||
{
|
||||
const std::vector<WorldAssemblyFeatureV1>& features;
|
||||
const WorldAssemblyFeatureTileListV1* list;
|
||||
struct Iterator
|
||||
{
|
||||
const FeatureRange* range;
|
||||
std::size_t index;
|
||||
const WorldAssemblyFeatureV1& operator*() const
|
||||
{ return range->features[range->list ? range->list->featureIndices[index] : index]; }
|
||||
Iterator& operator++() { ++index; return *this; }
|
||||
bool operator!=(const Iterator& other) const { return index!=other.index; }
|
||||
};
|
||||
Iterator begin() const { return {this,0}; }
|
||||
Iterator end() const { return {this,list ? list->featureIndices.size() : features.size()}; }
|
||||
};
|
||||
|
||||
constexpr std::int64_t Q16 = 1ll << 16;
|
||||
constexpr std::int64_t MinimumCliffShelfThicknessSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll;
|
||||
@@ -1294,7 +1379,8 @@ namespace world_assembly_detail
|
||||
:CapHeightV2(recipe,plan,neighbor,x,y);
|
||||
std::int64_t cap=primaryCap+((neighborCap-primaryCap)
|
||||
*(static_cast<std::int64_t>(Q16)-selection.primaryWeightQ16))/Q16;
|
||||
for(const auto& feature:plan.features)
|
||||
const auto* list=ScopedWorldAssemblyFeatureQueriesV1::find(&recipe,&plan,x,y,0,true);
|
||||
for(const auto& feature:FeatureRange{plan.features,list})
|
||||
{
|
||||
if(feature.type==1)
|
||||
{
|
||||
@@ -1730,6 +1816,42 @@ bool Heightfield2D::isValid() const noexcept
|
||||
});
|
||||
}
|
||||
|
||||
WorldAssemblyFeatureTileListV1 Heightfield2D::buildFeatureTileList(
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum) const
|
||||
{
|
||||
WorldAssemblyFeatureTileListV1 result;
|
||||
// One shelf in the staged owner plan has no useful scan to shorten. Keep its full evaluator.
|
||||
// Une seule corniche dans le plan auteur ne justifie pas la selection par echantillon.
|
||||
result.candidateEvaluationUseful=plan_->features.size()>1;
|
||||
result.minimumSubvoxels=minimum;
|
||||
result.maximumSubvoxels=maximum;
|
||||
std::vector<WorldAssemblyFeatureBoundsV1> bounds;
|
||||
bounds.reserve(plan_->features.size());
|
||||
for(const auto& feature:plan_->features)
|
||||
{
|
||||
WorldAssemblyFeatureBoundsV1 bound{feature.stableId,
|
||||
feature.minimumSubvoxels,feature.maximumSubvoxels};
|
||||
// Column writers affect every Z; finite volumes have an exact no-op outside their box.
|
||||
// Les modificateurs de colonne agissent sur tout Z; les volumes sont neutres hors boite.
|
||||
if(feature.type==1||feature.type==3)
|
||||
{
|
||||
bound.minimumSubvoxels[2]=std::numeric_limits<std::int64_t>::min();
|
||||
bound.maximumSubvoxels[2]=std::numeric_limits<std::int64_t>::max();
|
||||
}
|
||||
else if(feature.type!=2&&feature.type!=4)
|
||||
{
|
||||
// Unknown future operations must opt into a proof before they can be culled.
|
||||
// Une operation inconnue reste active tant que sa preuve manque.
|
||||
bound.minimumSubvoxels.fill(std::numeric_limits<std::int64_t>::min());
|
||||
bound.maximumSubvoxels.fill(std::numeric_limits<std::int64_t>::max());
|
||||
}
|
||||
bounds.push_back(bound);
|
||||
}
|
||||
result.featureIndices=BuildWorldAssemblyFeatureTileListV1(bounds,minimum,maximum);
|
||||
return result;
|
||||
}
|
||||
|
||||
HeightfieldPointSample Heightfield2D::point(std::int64_t x, std::int64_t y,
|
||||
std::int64_t z) const noexcept
|
||||
{
|
||||
@@ -1744,7 +1866,8 @@ HeightfieldPointSample Heightfield2D::point(std::int64_t x, std::int64_t y,
|
||||
out.capHeightSubvoxels-z);
|
||||
if(recipe_->evaluatorVersion>=2)
|
||||
{
|
||||
for(const auto& feature:plan_->features)
|
||||
const auto* list=ScopedWorldAssemblyFeatureQueriesV1::find(recipe_,plan_,x,y,z);
|
||||
for(const auto& feature:FeatureRange{plan_->features,list})
|
||||
{
|
||||
if(feature.type==2)
|
||||
{
|
||||
|
||||
@@ -1138,3 +1138,59 @@ link-safety passed. No 0xC0000142 retry or new UECC folder occurred. I did not o
|
||||
editor or build his project, edit any .uasset, create a link, commit, push or stash.
|
||||
|
||||
— Luna
|
||||
|
||||
|
||||
## World assembler S2 / M3 Round 6 — 2026-10-18
|
||||
|
||||
The shared engine now owns a family-neutral, worker-local feature-query scope. I removed the
|
||||
island-only context from ShapeWorld; SurfaceWorld finite features, island groups, meshing, far-shell
|
||||
field calls, decoration cells and landmark columns use the same domain/identity/halo contract.
|
||||
S1's root-aware positive-density bounds and ordered connector composition are preserved.
|
||||
|
||||
The measurements matter here: the staged SurfaceWorld has one finite shelf, and the staged island
|
||||
plan has four islands. Forced candidate bookkeeping gave no useful small-plan gain and regressed
|
||||
the island tile timings by 7–13%. I added a shared cost hint, resolved once per domain, so these
|
||||
small plans keep their full evaluator. Richer plans still use candidates. With the actual staged
|
||||
owner template expanded to 256 islands, forced-list p95 was 110.570 -> 7.3619 ms (about 15x), with
|
||||
identical hashes and a detected dropped-feature control. The composed surface stress (64 shelves,
|
||||
one XY height writer and one cap writer) was 0.7084 -> 0.4078 ms, again exact with its control caught.
|
||||
The S1 57x stress number used a different Round 3 template, so I do not compare those as a regression.
|
||||
|
||||
The placement cost came from marching almost the entire strate and using a full plan at every
|
||||
step. Family-declared solid-support bounds now feed a shared column range query, alongside the
|
||||
same feature scopes. We skip only certified air and keep the old march phase, bisection,
|
||||
interpolation and transforms. Owner-palette p95 fell 7.0706 -> 0.2385 ms, with 58.84 density calls
|
||||
per cell versus S1's 7,763.38. Its sampled authored palette still places zero objects; I keep that
|
||||
limitation explicit rather than claiming those timings include spawning trees.
|
||||
|
||||
A separate populated fixture produced 572 placements across 14 cells. All 572 full/candidate
|
||||
transforms match exactly, all canonical anchor checks pass, and the largest density residual is
|
||||
0.035156 Core voxel units. The 64-cell active batch, including merge and real HISM application,
|
||||
was 3271.147 -> 29.105 ms. All 572 instances were applied and then cleared. This is synchronous
|
||||
CPU work under null RHI; it excludes rendering, GPU upload, async tree completion and owner tree
|
||||
Blueprint logic. One run failed the new count assertion because normal region application clears
|
||||
live landmark footprints. I isolated the validation fixture from those footprints; normal content
|
||||
keeps them enabled. The corrected run passes.
|
||||
|
||||
BuildHost SurfaceWorld passed its full actor/streaming/collision/edit checks and W1/W8/W16 with
|
||||
S1's default 25 cm hashes. The final island actor also keeps S1's entire W1/W8/W16 matrix. The final
|
||||
focused surface replay checks LOD0/1/7/8 full-versus-default tile hashes after the cost hint. M2 V1/V2
|
||||
fixture repeats, borders, MC identity and all 125 doorway phases pass. A separate Core-off run
|
||||
proves unchanged legacy density and 12.5 cm scale. All builds/link audits passed. No timeout,
|
||||
0xC0000142 or new UECC folder occurred. The failed HISM fixture run exited normally with test status 1.
|
||||
|
||||
My next structural suggestion is an immutable per-strate family-provider table, carrying declared
|
||||
bounds, cost hints, value certificates and optional support/surface queries. It would avoid repeating
|
||||
concrete family enumeration and rebuilding metadata in each consumer. I have not rebuilt that
|
||||
boundary this round; the report lists the allocation, many-strate, hash and stale-provider evidence
|
||||
needed first. Preserve the existing bounded biome lattice and cave interval machinery meanwhile.
|
||||
|
||||
New source files are `Round6/SharedFeatureListAudit.cpp` and `Round6/BuildSharedFeatureListAudit.ps1`
|
||||
under WorldAssemblerM3. The report lists every modified source, likely compile spots, all failed and
|
||||
replayed runs, timing limitations and the plain-words owner note. CODEMAP section 3 and ARCHITECTURE
|
||||
section 8.23 are updated. No owner project/editor was opened, closed or built. No .uasset was edited,
|
||||
and no link, commit, push or stash was created.
|
||||
|
||||
[Full S2 report](../../WorldAssemblerM3/Round6/REPORT.md).
|
||||
|
||||
— Sol
|
||||
@@ -0,0 +1,61 @@
|
||||
[CmdletBinding()]
|
||||
param([string]$IslandAssemblyPath, [string]$SurfaceAssemblyPath)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$PluginRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..'))
|
||||
$HostOutput = Join-Path $PluginRoot 'Saved\BuildHost\HostProject\Plugins\VoxelForge\Saved\CoreWorldValidation\User\Saved\CoreWorldValidation\Output'
|
||||
if (!$IslandAssemblyPath) { $IslandAssemblyPath = Join-Path $HostOutput 's2_assembly_FloatingIslands.json' }
|
||||
if (!$SurfaceAssemblyPath) { $SurfaceAssemblyPath = Join-Path $HostOutput 's2_assembly_SurfaceWorld.json' }
|
||||
$Results = Join-Path $PSScriptRoot 'results'
|
||||
$BuildRoot = Join-Path $PluginRoot 'Standalone\Build\VoxelForgeCore'
|
||||
$Exe = Join-Path $BuildRoot 'SharedFeatureListAudit.exe'
|
||||
$Rsp = Join-Path $BuildRoot 'shared-feature-list-audit.rsp'
|
||||
$VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat'
|
||||
$Source = Join-Path $PSScriptRoot 'SharedFeatureListAudit.cpp'
|
||||
$Library = Join-Path $BuildRoot 'VoxelForgeCore.lib'
|
||||
foreach ($Required in @($IslandAssemblyPath,$SurfaceAssemblyPath,$VcVars,$Library,$Source)) {
|
||||
if (!(Test-Path -LiteralPath $Required -PathType Leaf)) { throw "Missing audit input: $Required" }
|
||||
}
|
||||
[IO.Directory]::CreateDirectory($Results) | Out-Null
|
||||
$Lines = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc',
|
||||
('/I"{0}"' -f (Join-Path $PluginRoot 'Standalone\Core\include')),
|
||||
('/Fe"{0}"' -f $Exe),('"{0}"' -f $Source),('"{0}"' -f $Library))
|
||||
[IO.File]::WriteAllLines($Rsp,$Lines,[Text.UTF8Encoding]::new($false))
|
||||
Push-Location $BuildRoot
|
||||
try {
|
||||
$Command = 'call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$Rsp
|
||||
& $env:ComSpec /d /s /c $Command
|
||||
if ($LASTEXITCODE -eq -1073741502) { & $env:ComSpec /d /s /c $Command }
|
||||
if ($LASTEXITCODE -ne 0) { throw "Audit compile failed: $LASTEXITCODE" }
|
||||
} finally { Pop-Location }
|
||||
for ($Attempt=1;$Attempt -le 2;++$Attempt) {
|
||||
$Stdout = Join-Path $Results "shared_audit_attempt_$Attempt.stdout.log"
|
||||
$Stderr = Join-Path $Results "shared_audit_attempt_$Attempt.stderr.log"
|
||||
$Process = Start-Process -FilePath $Exe -WindowStyle Hidden -PassThru -WorkingDirectory $BuildRoot `
|
||||
-ArgumentList @(('"{0}"' -f $IslandAssemblyPath),('"{0}"' -f $SurfaceAssemblyPath),('"{0}"' -f $Results)) `
|
||||
-RedirectStandardOutput $Stdout -RedirectStandardError $Stderr
|
||||
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
|
||||
$Process.ProcessorAffinity = [IntPtr]0xFFF
|
||||
$Timer = [Diagnostics.Stopwatch]::StartNew()
|
||||
$Peak = 0L
|
||||
while (!$Process.WaitForExit(250)) {
|
||||
$Process.Refresh()
|
||||
$Peak = [Math]::Max($Peak,$Process.PrivateMemorySize64)
|
||||
if ($Timer.Elapsed.TotalSeconds -gt 900 -or $Peak -gt 8GB) {
|
||||
Stop-Process -Id $Process.Id -Force
|
||||
throw 'Shared audit exceeded 15 minutes or 8 GiB.'
|
||||
}
|
||||
}
|
||||
$Process.WaitForExit()
|
||||
$Process.Refresh()
|
||||
$ExitCode = $Process.ExitCode
|
||||
if ($null -eq $ExitCode -and (Select-String -LiteralPath $Stdout -SimpleMatch 'SHARED_FEATURE_AUDIT_PASS' -Quiet)) { $ExitCode=0 }
|
||||
"attempt=$Attempt exit=$ExitCode elapsed_s=$($Timer.Elapsed.TotalSeconds) peak_private_bytes=$Peak" |
|
||||
Add-Content (Join-Path $Results 'shared_audit_ledger.txt')
|
||||
Get-Content $Stdout
|
||||
Get-Content $Stderr
|
||||
if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) { continue }
|
||||
if ($ExitCode -ne 0 -or !(Select-String -LiteralPath $Stdout -SimpleMatch 'SHARED_FEATURE_AUDIT_PASS' -Quiet)) {
|
||||
throw "Shared feature audit failed: $ExitCode"
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
# World assembler S2 / M3 Round 6
|
||||
|
||||
Date: 2026-10-18. Author: Sol. Base: `d6e6942`, branch `assembler/world`. No commit, push or stash.
|
||||
|
||||
## Shared engine change
|
||||
|
||||
The island-only worker context has been removed from `ShapeWorldInternal.h`.
|
||||
`WorldAssemblyFeatureTileListV1` and `ScopedWorldAssemblyFeatureQueriesV1` now carry a shared,
|
||||
worker-local candidate domain, keyed by immutable recipe/plan identity. Nested scopes restore their
|
||||
predecessor; samples outside the prepared domain use the full evaluator. Tile preparation includes
|
||||
the mesher's normal halo and prepares both existing native families. Derived far-shell field calls
|
||||
stay in that scope too. The old island payload names remain aliases so S1 research tools still build.
|
||||
|
||||
Families retain their mathematics: SurfaceWorld declares XY influence for column writers and finite
|
||||
no-op boxes for volumetric exceptions; islands retain S1's root-aware positive-density certificate
|
||||
and ordered island/connector groups. The engine never assumes that density magnitude is Euclidean
|
||||
distance. Unknown operations stay active. A shared `candidateEvaluationUseful` cost hint is resolved
|
||||
once per domain. The measured one-feature SurfaceWorld and four-island plans prefer the full scan;
|
||||
larger plans retain culling. Audits force candidates on small plans to prove their bounds too. No density arithmetic, recipe serialization/version,
|
||||
voxel-size resolution, queue, Epoch, edit lock or streaming policy was changed.
|
||||
|
||||
The default contract for future caves, shafts, pits, passages and rivers is documented in
|
||||
ARCHITECTURE §8.23. Declare conservative bounds and any value-side proof needed to preserve exact
|
||||
positive values. Preserve operation order; unknown proofs evaluate normally. An air-base family can
|
||||
also declare conservative solid support, allowing the shared column union to shorten a search.
|
||||
Solid-base caves cannot use that air proof. Every new proof needs equal full/candidate hashes and a
|
||||
dropped-needed-feature control.
|
||||
|
||||
Surface biome-site selection already searches a fixed local lattice and is cached by XY. It is not
|
||||
a global feature scan. We preserve that machinery and target its finite feature loops. Existing
|
||||
cave extraction keeps its existing line/spatial bins; this round makes no new cave performance claim.
|
||||
FarShells remains derived repair geometry with existing connector/stratum candidate filtering, rather
|
||||
than becoming another canonical generator. Its field queries inherit the shared scope.
|
||||
|
||||
## Placement cause and correction
|
||||
|
||||
Native island placement previously scanned the entire owner strate, calling a full-plan evaluator
|
||||
at each step. The owner workload used about 7,763 density queries per cell in S1. Its sampling cell
|
||||
was mostly air, so optimizing only the top-anchor helper did not remove the dominant work.
|
||||
|
||||
Snapshots now declare conservative support bounds once. Decoration cells and landmark columns
|
||||
prepare the same shared feature queries as meshing. A shared support-union query skips proven air,
|
||||
including entirely empty columns. The march retains its original lattice phase, crossing budget,
|
||||
four bisections, interpolation, normals and placement hashes. No alternate approximate top surface
|
||||
was introduced. Companion queries get their own nested domain when needed.
|
||||
|
||||
Validation preserves the owner's palette and reports it separately from a deliberately populated
|
||||
fixture. That fixture uses dense deterministic floor placements, both water classifications, the
|
||||
resolved biome tags, and a staged decoration mesh (engine cube fallback if none is available).
|
||||
It checks exact full/candidate transforms and canonical island density residuals, then merges and
|
||||
applies real HISM regions and clears them immediately. CPU totals include cell preparation, merge
|
||||
and region application; the identical application/merge cost is shared by both comparison totals.
|
||||
The isolated HISM fixture bypasses live landmark footprints, which otherwise suppress its instances.
|
||||
Normal region application keeps those footprints enabled. Null RHI does not measure GPU upload,
|
||||
rendering, a rendered frame, or asynchronous cluster-tree completion. Existing native content
|
||||
snapshots still describe the planned base field; this round does not add edit-aware reanchoring.
|
||||
|
||||
## Measurements and proof
|
||||
|
||||
The field audit replays the plans exported from BuildHost's staged owner route. These are the
|
||||
same transient owner-height fixtures described in Round 4: the saved settings point to `DA_Strate4`,
|
||||
while the logged SurfaceWorld layout is reconstructed from `DA_Strate1` without saving an asset.
|
||||
No Season is assigned in these copies. The island plan has 4 islands and 3 connectors; the surface
|
||||
plan has one finite type-4 shelf. The stress cases extend those plans to 256 islands/240 connectors
|
||||
and 64 surface shelves plus one XY height writer and one XY cap writer. They do not change any production recipe.
|
||||
|
||||
### Forced candidate proof and cost
|
||||
|
||||
Each island row samples 64 tiles / 64,000 points, including the normal halo. Each surface row
|
||||
samples 64 tiles / 46,656 points and hashes density, ground, cap and biome ID (186,624 values).
|
||||
Surface timings start both paths with cold column caches by changing only the local validation
|
||||
cache epoch. Final timing replay ran without an overlapping build or Unreal process.
|
||||
|
||||
| Family / plan | Full-scan p95 ms | Forced-candidate p95 ms | List build p95 ms | Full and candidate hash | Negative control |
|
||||
|---|---:|---:|---:|---|---|
|
||||
| Islands, staged owner | 0.9803 | 1.0194 | 0.0012 | `cb94db3ca317d001` | detected |
|
||||
| Islands, 256 stress | 110.570 | 7.3619 | 0.0803 | `d83bac7778f3d62a` | detected |
|
||||
| Surface, staged owner | 0.4748 | 0.4559 | 0.0003 | `4207efd4ac869108` | detected |
|
||||
| Surface, 64 shelves + height/cap writers | 0.7084 | 0.4078 | 0.0011 | `f1c6ea123284cc38` | detected |
|
||||
|
||||
Every row has zero mismatches. Each control removes a needed feature; the island control removes
|
||||
its group certificate too. The shared-scope audit also checks identity/domain fallback, nested empty
|
||||
scope restoration, scope teardown and the full-scan cost hint.
|
||||
|
||||
The owner island point audit offers no useful culling gain. Stress p95 improves about **15×**;
|
||||
3,362 stress lists cost 216.615 ms in total (108 owner lists cost 0.0766 ms). The stress template comes
|
||||
from the actual staged owner plan, whereas S1's standalone audit used Round 3's manifest; its 57×
|
||||
figure is not an apples-to-apples regression comparison.
|
||||
|
||||
Surface's single feature has no material point-query gain. Its column computation alone is about
|
||||
0.368 ms of the 0.475 ms p95, and the existing bounded biome/cap path stays intact. The stress case
|
||||
reduces p95 by about 42% while column cost stays similar (0.3699/0.3552 ms). There is no reason to
|
||||
force a global biome/cap feature list into this owner plan.
|
||||
|
||||
Warmed BuildHost tile timings initially exposed a 7–13% island regression from forcing group
|
||||
bookkeeping on four islands. That led to the shared cost hint. The final default path keeps its small-plan timing:
|
||||
|
||||
| Staged plan | LOD | Full tile p95 ms | Default shared-engine p95 ms | Full / shared shell p95 ms |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Islands | 0 | 94.984 | 95.094 | 0.0002 / 0.0003 |
|
||||
| Islands | 1 | 87.280 | 87.577 | 0.0005 / 0.0003 |
|
||||
| Islands | 7 | 20.442 | 20.409 | 16.918 / 16.882 |
|
||||
| Islands | 8 | 73.038 | 72.923 | 69.930 / 69.844 |
|
||||
| Surface | 0 | 11.238 | 11.178 | 0.0001 / 0.0001 |
|
||||
| Surface | 1 | 7.346 | 7.354 | 0.0001 / 0.0001 |
|
||||
| Surface | 7 | 32.739 | 32.381 | 26.378 / 26.021 |
|
||||
| Surface | 8 | 168.545 | 168.063 | 135.960 / 136.310 |
|
||||
|
||||
Each row measures eight owner clipmap keys after warming both routes, including list preparation,
|
||||
meshing and density hashes. All four output hashes match on every paired key. This is a small,
|
||||
hash-capture workload, not a rendered-frame percentile. The far-shell stage excludes LOD8 cap
|
||||
refinement. There is no measured reason to add another shell-specific feature index for these
|
||||
small owner plans; larger canonical fields inherit their family culling through the shared scope.
|
||||
|
||||
### Island placement and active populated cost
|
||||
|
||||
| Workload | Full scan | Shared engine | Result |
|
||||
|---|---:|---:|---|
|
||||
| Owner palette, cell p95 | 7.0706 ms | 0.2385 ms | about 29.6× faster |
|
||||
| Owner palette, 64-cell preparation | 412.960 ms | 3.840 ms | same zero authored placements |
|
||||
| Populated fixture, populated-cell p95 | 53.9875 ms | 2.1501 ms | 14 populated cells / 572 placements |
|
||||
| Populated fixture, all 64 cells | 3270.478 ms | 28.436 ms | exact transforms match |
|
||||
| Merge / real HISM apply | 0.0623 / 0.6068 ms | same products/cost | 572 applied instances |
|
||||
| Populated active batch CPU total | 3271.147 ms | 29.105 ms | about 112× faster |
|
||||
|
||||
Candidate density calls are 58.84 per authored cell, versus S1's 7,763.38 for the same owner plan.
|
||||
Legacy remains 193.25 calls/cell and 0.2855 ms p95. Stationary manager update p95 is 0.0005 ms;
|
||||
that idle number is separate from the active batch totals above.
|
||||
|
||||
All 572 populated transforms match their full-scan counterparts exactly. All 572 canonical island
|
||||
anchor checks pass; maximum density residual is 0.035156 Core voxel density units. The existing
|
||||
terrain/content tests pass on both island and connector: island height difference is 0.373 cm,
|
||||
connector difference 0.096 cm. Capsule contacts, Blueprint carve/fill, water and atmosphere pass.
|
||||
No approximate anchor shortcut is used.
|
||||
|
||||
The six-entry authored palette still produces zero placements in these 64 cells. The populated
|
||||
fixture is explicit synthetic coverage; it does not claim the cost of the owner's tree Blueprint
|
||||
logic or rendered foliage. Active totals sum worker preparation and synchronous merge/application
|
||||
calls over the whole batch, not one frame. Footprint suppression is disabled only for the isolated
|
||||
fixture, which is immediately cleared. Async HISM tree completion and rendering are not included.
|
||||
The earlier SurfaceWorld populated diagnostic produced 8,192 synthetic transforms with exact
|
||||
full/candidate agreement, but had no mesh application in that build and warm-cache ordering;
|
||||
its speed figures are not used as a SurfaceWorld improvement claim.
|
||||
|
||||
### Default identity
|
||||
|
||||
No recipe version was bumped: all changes select exact evaluators or skip certified air.
|
||||
The full SurfaceWorld actor run reproduced S1's W1/W8/W16 matrix:
|
||||
`0cd2a1a4152ee4aa / 17775c835711e5a3 / a27224958fc27d7b / 3821fea90b134ce4`
|
||||
(density / mesh / payload / topology). Island assembly/recipe/plan remain
|
||||
`9c372e7bfb5b8310 / 5e0370d9f95f1c15 / dbbda4932378047a`.
|
||||
The final-source island W1/W8/W16 matrix is also exactly S1:
|
||||
`4dcfb37585d7d986 / 725a18374c5623de / fb345d8cb319d18b / 76f2846be8efb5f4`.
|
||||
|
||||
The full SurfaceWorld actor pass precedes the final cost hint; the final-source focused replay
|
||||
checks its LOD0/1/7/8 full-versus-default hashes after that selection-only change. Core-off's separate
|
||||
final run proves unchanged legacy density at three probes and 12.5 cm. M2 V1/V2 five-fixture repeats,
|
||||
same-LOD borders, V2 MC identity and 125 doorway phases all pass. Existing doorway contour
|
||||
mismatch remains 22.844 cm in that fixture audit; this round does not claim a new seam solution.
|
||||
|
||||
### Run ledger and limits
|
||||
|
||||
| Run | Result / limit |
|
||||
|---|---|
|
||||
| Standalone Core library builds (four iterations) | all first-attempt passes; final link-safety audit checks 220,200 symbol lines |
|
||||
| BuildHost UBT/UHT builds (six iterations) | all first-attempt passes; latest 62.03 s; 18 Core shims, link-safety 95,738 symbol lines; five staged asset hashes verified |
|
||||
| Preliminary island actor | pass, about 3m57s, 3.19 GiB peak; canonical hashes and 572 anchor checks |
|
||||
| Full SurfaceWorld actor | pass, about 12m15s, 3.58 GiB peak; streaming, collisions, motion, edits, geometry/anchors, W1/W8/W16 |
|
||||
| Island actor with new HISM fixture | exit 1, about 4m14s; landmark footprint suppression hit the fixture count assertion; field/worker/anchor gates passed |
|
||||
| Final island actor, cost hint and isolated HISM fixture | pass, about 3m52s, 3.03 GiB peak; W1/W8/W16, 572 exact anchors and applied instances |
|
||||
| Final focused SurfaceWorld tile replay | pass, about 21s, 2.70 GiB peak; 32 paired LOD0/1/7/8 keys, all hashes equal |
|
||||
| Final M2 V1/V2 fixtures | pass, about 17s, 2.34 GiB peak; repeats, borders, MC identity, 125 doorway phases |
|
||||
| Final Core-off | pass, about 14s, 2.14 GiB peak; legacy density and scale unchanged |
|
||||
| Final composed standalone audit | pass, 7.87s, about 64 MiB peak; all four full/candidate rows exact and controls detected |
|
||||
|
||||
Build iterations cover initial integration, added diagnostics, active fixture application, warmed
|
||||
measurements, duplicate-validation removal, and the cost-hint/fixture fix. Their logs are the six
|
||||
`results/host_build*.log` files and four `results/core_build*.log` files. The standalone audit was
|
||||
replayed as those diagnostics were refined; the final composed stress also exercises XY height/cap
|
||||
writers at unrelated Z values. Every replay passed its field/control assertions.
|
||||
|
||||
One actor run failed its new populated-HISM count
|
||||
assertion after passing field/worker/anchor gates: normal `ApplyRegion` also clears live landmark
|
||||
footprints. The fixture now explicitly isolates those footprints; normal application defaults to
|
||||
keeping them enabled. The failed log is `results/islands_hism_fixture_failed.log` (exit 1, no crash).
|
||||
|
||||
Preliminary surface tile timings used a cold full path followed by a warm candidate path. Those
|
||||
numbers are discarded; the final focused replay warms both paths. The first standalone audit
|
||||
also overlapped compilation; it passed functional controls, but its timings are discarded. Idle
|
||||
replays and the final-source replay are all recorded in `results/shared_audit_ledger.txt`.
|
||||
An audit iteration removed duplicate plan validation from scoped island samples. All standalone
|
||||
field audits passed; `vswhere.exe` was not found by the installed VC setup script, but CL compiled
|
||||
and the audit ran successfully. A documentation-edit attempt failed on Python's default Windows
|
||||
encoding; it changed no files and was corrected with UTF-8/apply_patch.
|
||||
|
||||
The first preliminary island launch used the existing launcher; I applied 12-CPU affinity during
|
||||
that run, then added `-corelimit=12` and 12-CPU affinity to the launcher for subsequent runs. Builds
|
||||
used at most eight UBT actions, or eight UBT plus four standalone compiler actions. W16 remains a
|
||||
logical determinism partition under the 12-CPU execution cap. Each Unreal launch checks 4 GiB free
|
||||
RAM and refuses another Unreal process. Every process is guarded at 8 GiB and 900 seconds.
|
||||
No viewer or GPU capture was launched. No run exceeded its memory/time guard, no timeout occurred,
|
||||
and no run returned `0xC0000142` (so its one-retry rule was never needed).
|
||||
|
||||
Crash audit: `Saved/BuildHost/HostProject/Saved/Crashes/` still contains only
|
||||
`UECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000` (2026-09-17). The isolated User crash root
|
||||
still contains only `UECC-Windows-7B23387E4235BCFEDE42BC9D637E41CB_0000` (2026-10-08).
|
||||
No new UECC folder exists in the host, staged plugin or isolated User crash roots. The final
|
||||
commandlets finished with zero errors; known missing staged `/Game/VoxelForge/Blocks/NewDataAsset`
|
||||
and certificate-store warnings remain. The failed fixture run logged its explicit test error and
|
||||
exited normally.
|
||||
|
||||
Evidence: `results/SUMMARY.json`, `results/shared_audit_attempt_1.stdout.log`,
|
||||
`results/island_tile_lists.json`, `results/shared_audit_ledger.txt`, `results/surface_full_actor.log`,
|
||||
`results/islands_full_actor_final.log`, `results/surface_warmed_final.log`,
|
||||
`results/m2_fixtures_final.log` and `results/core_off_final.log`. Owner plans exported under
|
||||
BuildHost's isolated User Saved directory are the standalone replay inputs.
|
||||
|
||||
No owner project/editor was opened, closed or built, no `.uasset` was edited, and no junction,
|
||||
symlink, commit, push or stash was created. All authored writes and build outputs stayed inside
|
||||
this plugin's workspace/BuildHost.
|
||||
|
||||
|
||||
## My view of the boundary
|
||||
|
||||
The next structural improvement should be a prepared immutable family-provider table per strate.
|
||||
Today the shared scope is generic, but the world and content snapshot adapters still enumerate the
|
||||
two concrete field families, and preparation rebuilds metadata vectors for each query. A provider
|
||||
could publish stable feature IDs, bounds, an optional value certificate and optional surface/support
|
||||
queries once, while the engine handles consumer domains, ordering, cache lifetime and instrumentation.
|
||||
That would also give future cave/rivers placement the same entry point without adding another
|
||||
content snapshot class. I have not rebuilt the architecture for that proposal in this round.
|
||||
|
||||
Evidence required before doing it: a many-strate preparation profile showing the cost of rebuilding
|
||||
bounds/group summaries; separate point-evaluation and list-build allocation counts; equal full versus
|
||||
prepared hashes across owner plans, halos and edited epochs; a deliberately stale-provider control;
|
||||
and no regression in time to collision-ready terrain or peak memory. Preserve the current bounded
|
||||
lattice path for procedural biomes and the current cave interval engine until a measured replacement
|
||||
wins. A universal list of shapes would not capture those processes or their proofs.
|
||||
|
||||
## Files and compile spots
|
||||
|
||||
New source files:
|
||||
|
||||
- `Standalone/Research/WorldAssemblerM3/Round6/SharedFeatureListAudit.cpp` — standalone owner/stress audit.
|
||||
- `Standalone/Research/WorldAssemblerM3/Round6/BuildSharedFeatureListAudit.ps1` — guarded audit compile/run script.
|
||||
|
||||
Modified runtime/Core sources:
|
||||
|
||||
- `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`
|
||||
- `Standalone/Core/src/WorldAssemblerInternal.h`
|
||||
- `Standalone/Core/src/FloatingIslandsInternal.h`
|
||||
- `Standalone/Core/src/ShapeWorldInternal.h`
|
||||
- `Standalone/Core/src/VoxelForgeCore.cpp`
|
||||
- `Source/VoxelForge/Private/VoxelContentManager.cpp`
|
||||
- `Source/VoxelForge/Public/VoxelContentManager.h`
|
||||
- `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp`
|
||||
|
||||
Also updated `Tools/TestVoxelForgeCoreWorldHost.ps1`, CODEMAP §3, ARCHITECTURE §8.23 and the dialogue.
|
||||
Likely compile spots: the public Core API/export boundary and canonical Core shim; shared scope/TLS
|
||||
linkage; `VoxelContentManager.h` parsing and its runtime translation unit; editor commandlet timing
|
||||
and fixture code. No new runtime translation unit or manifest shim is required.
|
||||
|
||||
## Owner note
|
||||
|
||||
Keep Core enabled at the existing 25 cm setting. There is no new generator setting to configure.
|
||||
The shared engine now narrows terrain searches for both native families and skips empty space when
|
||||
placing island content. Terrain shapes and default hashes are preserved. The populated validation
|
||||
fixture measures placement without changing your decoration assets. Ready to build your project
|
||||
when you choose; I only build and launch the isolated plugin BuildHost.
|
||||
@@ -0,0 +1,601 @@
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <numeric>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
constexpr std::int64_t Q = WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr std::int64_t TileQ = 32 * Q;
|
||||
constexpr std::int64_t TileStepQ = 4 * Q;
|
||||
constexpr std::int64_t MesherHaloQ = Q;
|
||||
|
||||
struct Tile
|
||||
{
|
||||
std::array<std::int64_t, 3> minimum{};
|
||||
std::array<std::int64_t, 3> maximum{};
|
||||
FloatingIslandTileFeatureListV1 featureList;
|
||||
double buildMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
struct Measurement
|
||||
{
|
||||
std::uint64_t tilesBuilt = 0;
|
||||
std::uint64_t sampledTiles = 0;
|
||||
std::uint64_t samples = 0;
|
||||
std::uint64_t candidateEvaluations = 0;
|
||||
std::uint64_t fullEvaluations = 0;
|
||||
std::uint64_t exactMismatches = 0;
|
||||
std::uint64_t droppedFeatureDetected = 0;
|
||||
double listBuildTotalMilliseconds = 0.0;
|
||||
double listBuildP95Milliseconds = 0.0;
|
||||
double beforeP95Milliseconds = 0.0;
|
||||
double afterP95Milliseconds = 0.0;
|
||||
double beforeMedianMilliseconds = 0.0;
|
||||
double afterMedianMilliseconds = 0.0;
|
||||
double meanCandidateFeatures = 0.0;
|
||||
double p95CandidateFeatures = 0.0;
|
||||
std::string beforeHash;
|
||||
std::string afterHash;
|
||||
bool negativeControlPassed = false;
|
||||
};
|
||||
|
||||
std::string Hex64(std::uint64_t value)
|
||||
{
|
||||
std::ostringstream output;
|
||||
output << std::hex << std::setfill('0') << std::setw(16) << value;
|
||||
return output.str();
|
||||
}
|
||||
|
||||
std::uint64_t HashValues(const std::vector<std::int64_t>& values)
|
||||
{
|
||||
std::uint64_t hash = 14695981039346656037ull;
|
||||
for (const auto value : values)
|
||||
for (int byte = 0; byte < 8; ++byte)
|
||||
{
|
||||
hash ^= static_cast<std::uint8_t>(static_cast<std::uint64_t>(value) >> (byte * 8));
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
double Percentile95(std::vector<double> values)
|
||||
{
|
||||
if (values.empty()) return 0.0;
|
||||
std::sort(values.begin(), values.end());
|
||||
const std::size_t index = (values.size() * 95 + 99) / 100 - 1;
|
||||
return values[std::min(index, values.size() - 1)];
|
||||
}
|
||||
|
||||
double Median(std::vector<double> values)
|
||||
{
|
||||
if (values.empty()) return 0.0;
|
||||
std::sort(values.begin(), values.end());
|
||||
const std::size_t middle = values.size() / 2;
|
||||
if (values.size() & 1u) return values[middle];
|
||||
return 0.5 * (values[middle - 1] + values[middle]);
|
||||
}
|
||||
|
||||
std::int64_t FloorDiv(std::int64_t value, std::int64_t divisor)
|
||||
{
|
||||
std::int64_t quotient = value / divisor;
|
||||
const std::int64_t remainder = value % divisor;
|
||||
if (remainder != 0 && ((remainder < 0) != (divisor < 0))) --quotient;
|
||||
return quotient;
|
||||
}
|
||||
|
||||
void ShiftBounds(std::array<std::int64_t, 3>& minimum,
|
||||
std::array<std::int64_t, 3>& maximum, const std::array<std::int64_t, 3>& delta)
|
||||
{
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{ minimum[axis] += delta[axis]; maximum[axis] += delta[axis]; }
|
||||
}
|
||||
|
||||
std::uint64_t SyntheticId(std::uint32_t index, std::uint64_t salt)
|
||||
{
|
||||
std::uint64_t value = (static_cast<std::uint64_t>(index) << 32) ^ salt;
|
||||
value += 0x9e3779b97f4a7c15ull;
|
||||
value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ull;
|
||||
value = (value ^ (value >> 27)) * 0x94d049bb133111ebull;
|
||||
value ^= value >> 31;
|
||||
return value == 0 ? 1 : value;
|
||||
}
|
||||
|
||||
FloatingIslandPlanV1 MakeStressPlan(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandPlanV1& source, std::size_t islandCount)
|
||||
{
|
||||
if (source.islands.empty()) throw std::runtime_error("source plan has no island template");
|
||||
FloatingIslandPlanV1 result;
|
||||
result.islands.reserve(islandCount);
|
||||
const auto& prototype = source.islands.front();
|
||||
constexpr std::size_t Columns = 16;
|
||||
const std::int64_t spacing = 80 * Q;
|
||||
for (std::size_t index = 0; index < islandCount; ++index)
|
||||
{
|
||||
FloatingIslandAnchorV1 island = prototype;
|
||||
const std::int64_t dx = static_cast<std::int64_t>(index % Columns) * spacing;
|
||||
const std::int64_t dy = static_cast<std::int64_t>(index / Columns) * spacing;
|
||||
const std::int64_t dz = static_cast<std::int64_t>(index % 3) * 4 * Q;
|
||||
const std::array<std::int64_t, 3> delta{dx, dy, dz};
|
||||
island.centerSubvoxels = {prototype.centerSubvoxels[0] + dx,
|
||||
prototype.centerSubvoxels[1] + dy, prototype.centerSubvoxels[2] + dz};
|
||||
island.topZSubvoxels += dz;
|
||||
island.bottomZSubvoxels += dz;
|
||||
ShiftBounds(island.influenceMinimumSubvoxels, island.influenceMaximumSubvoxels, delta);
|
||||
island.ordinal = static_cast<std::uint32_t>(index);
|
||||
island.stableId = SyntheticId(static_cast<std::uint32_t>(index), 0x49534c414e44ull);
|
||||
result.islands.push_back(island);
|
||||
}
|
||||
|
||||
result.clusterEdges.reserve(islandCount - islandCount / Columns);
|
||||
for (std::size_t index = 0; index < islandCount; ++index)
|
||||
{
|
||||
if (index % Columns + 1 >= Columns || index + 1 >= islandCount) continue;
|
||||
const auto& from = result.islands[index];
|
||||
const auto& to = result.islands[index + 1];
|
||||
FloatingIslandConnectorV1 edge;
|
||||
edge.stableId = SyntheticId(static_cast<std::uint32_t>(index), 0x425249444745ull);
|
||||
edge.fromIslandId = from.stableId;
|
||||
edge.toIslandId = to.stableId;
|
||||
edge.startSubvoxels = {from.centerSubvoxels[0] + from.radiusSubvoxels,
|
||||
from.centerSubvoxels[1], from.topZSubvoxels};
|
||||
edge.endSubvoxels = {to.centerSubvoxels[0] - to.radiusSubvoxels,
|
||||
to.centerSubvoxels[1], to.topZSubvoxels};
|
||||
edge.halfWidthSubvoxels = recipe.bridgeHalfWidthSubvoxels;
|
||||
edge.thicknessSubvoxels = recipe.bridgeThicknessSubvoxels;
|
||||
const std::int64_t pad = recipe.blendRadiusSubvoxels + recipe.roughnessSubvoxels;
|
||||
const std::int64_t archDepth = recipe.geometryVersion >= 2
|
||||
? 2 * edge.thicknessSubvoxels : 0;
|
||||
const std::int64_t xyPad = edge.halfWidthSubvoxels + pad;
|
||||
edge.influenceMinimumSubvoxels = {
|
||||
std::min(edge.startSubvoxels[0], edge.endSubvoxels[0]) - xyPad,
|
||||
std::min(edge.startSubvoxels[1], edge.endSubvoxels[1]) - xyPad,
|
||||
std::min(edge.startSubvoxels[2], edge.endSubvoxels[2])
|
||||
- edge.thicknessSubvoxels - pad - archDepth};
|
||||
edge.influenceMaximumSubvoxels = {
|
||||
std::max(edge.startSubvoxels[0], edge.endSubvoxels[0]) + xyPad,
|
||||
std::max(edge.startSubvoxels[1], edge.endSubvoxels[1]) + xyPad,
|
||||
std::max(edge.startSubvoxels[2], edge.endSubvoxels[2]) + pad};
|
||||
result.clusterEdges.push_back(edge);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Tile> BuildMixedTiles(const FloatingIslandFieldV1& field,
|
||||
const std::array<std::int64_t, 3>& tileLimitMinimum,
|
||||
const std::array<std::int64_t, 3>& tileLimitMaximum,
|
||||
Measurement& measurement)
|
||||
{
|
||||
std::vector<Tile> tiles;
|
||||
for (std::int64_t z = tileLimitMinimum[2]; z <= tileLimitMaximum[2]; z += TileQ)
|
||||
for (std::int64_t y = tileLimitMinimum[1]; y <= tileLimitMaximum[1]; y += TileQ)
|
||||
for (std::int64_t x = tileLimitMinimum[0]; x <= tileLimitMaximum[0]; x += TileQ)
|
||||
{
|
||||
Tile tile;
|
||||
tile.minimum = {x, y, z};
|
||||
tile.maximum = {x + TileQ, y + TileQ, z + TileQ};
|
||||
const std::array<std::int64_t, 3> sampleMinimum{
|
||||
x - MesherHaloQ, y - MesherHaloQ, z - MesherHaloQ};
|
||||
const std::array<std::int64_t, 3> sampleMaximum{
|
||||
x + TileQ + MesherHaloQ, y + TileQ + MesherHaloQ, z + TileQ + MesherHaloQ};
|
||||
const auto begin = Clock::now();
|
||||
tile.featureList = field.buildFeatureTileList(sampleMinimum, sampleMaximum);
|
||||
tile.buildMilliseconds = std::chrono::duration<double, std::milli>(Clock::now() - begin).count();
|
||||
measurement.listBuildTotalMilliseconds += tile.buildMilliseconds;
|
||||
++measurement.tilesBuilt;
|
||||
if (!tile.featureList.featureIndices.empty()) tiles.push_back(std::move(tile));
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
Measurement MeasureField(const FloatingIslandFieldV1& field,
|
||||
std::array<std::int64_t, 3> limitMinimum,
|
||||
std::array<std::int64_t, 3> limitMaximum)
|
||||
{
|
||||
Measurement measurement;
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
limitMinimum[axis] = FloorDiv(limitMinimum[axis], TileQ) * TileQ;
|
||||
limitMaximum[axis] = FloorDiv(limitMaximum[axis], TileQ) * TileQ;
|
||||
}
|
||||
auto tiles = BuildMixedTiles(field, limitMinimum, limitMaximum, measurement);
|
||||
if (tiles.empty()) throw std::runtime_error("tile bounds produced no candidate tiles");
|
||||
std::vector<double> buildTimes, candidateCounts;
|
||||
buildTimes.reserve(tiles.size()); candidateCounts.reserve(tiles.size());
|
||||
for (const Tile& tile : tiles)
|
||||
{ buildTimes.push_back(tile.buildMilliseconds); candidateCounts.push_back(static_cast<double>(tile.featureList.featureIndices.size())); }
|
||||
measurement.listBuildP95Milliseconds = Percentile95(std::move(buildTimes));
|
||||
measurement.meanCandidateFeatures = std::accumulate(candidateCounts.begin(), candidateCounts.end(), 0.0)
|
||||
/ static_cast<double>(candidateCounts.size());
|
||||
measurement.p95CandidateFeatures = Percentile95(std::move(candidateCounts));
|
||||
|
||||
constexpr std::size_t MaximumSampleTiles = 64;
|
||||
if (tiles.size() > MaximumSampleTiles)
|
||||
{
|
||||
std::vector<Tile> selected;
|
||||
selected.reserve(MaximumSampleTiles);
|
||||
for (std::size_t slot = 0; slot < MaximumSampleTiles; ++slot)
|
||||
{
|
||||
const std::size_t index = slot * (tiles.size() - 1) / (MaximumSampleTiles - 1);
|
||||
selected.push_back(std::move(tiles[index]));
|
||||
}
|
||||
tiles = std::move(selected);
|
||||
}
|
||||
|
||||
std::vector<double> beforeTimes, afterTimes;
|
||||
std::vector<std::int64_t> beforeValues, afterValues;
|
||||
beforeTimes.reserve(tiles.size()); afterTimes.reserve(tiles.size());
|
||||
for (const Tile& tile : tiles)
|
||||
{
|
||||
// Cover the mesher's one-sample LOD0 halo as well as the tile at four-voxel spacing.
|
||||
// Couvrir la marge d'un echantillon du maillage LOD0 et la tuile a pas de quatre voxels.
|
||||
const auto buildAxis = [](std::int64_t minimum, std::int64_t maximum)
|
||||
{
|
||||
std::vector<std::int64_t> values;
|
||||
const std::int64_t start = minimum - MesherHaloQ;
|
||||
const std::int64_t end = maximum + MesherHaloQ;
|
||||
for (std::int64_t value = start; value <= end; value += TileStepQ)
|
||||
values.push_back(value);
|
||||
if (values.empty() || values.back() != end) values.push_back(end);
|
||||
return values;
|
||||
};
|
||||
const auto xs = buildAxis(tile.minimum[0], tile.maximum[0]);
|
||||
const auto ys = buildAxis(tile.minimum[1], tile.maximum[1]);
|
||||
const auto zs = buildAxis(tile.minimum[2], tile.maximum[2]);
|
||||
std::vector<std::array<std::int64_t, 3>> points;
|
||||
points.reserve(xs.size() * ys.size() * zs.size());
|
||||
for (const std::int64_t z : zs)
|
||||
for (const std::int64_t y : ys)
|
||||
for (const std::int64_t x : xs)
|
||||
points.push_back({x, y, z});
|
||||
measurement.samples += points.size();
|
||||
measurement.fullEvaluations += points.size();
|
||||
measurement.candidateEvaluations += points.size() * tile.featureList.featureIndices.size();
|
||||
|
||||
const auto beforeStart = Clock::now();
|
||||
for (const auto& point : points)
|
||||
beforeValues.push_back(field.densitySubvoxels(point[0], point[1], point[2]));
|
||||
beforeTimes.push_back(std::chrono::duration<double, std::milli>(Clock::now() - beforeStart).count());
|
||||
|
||||
const auto afterStart = Clock::now();
|
||||
const WorldAssemblyFeatureQueryV1 query{field.recipe(),field.plan(),tile.featureList};
|
||||
{
|
||||
const ScopedWorldAssemblyFeatureQueriesV1 scope(std::span(&query,1),true);
|
||||
for (const auto& point : points)
|
||||
afterValues.push_back(field.densitySubvoxels(point[0],point[1],point[2]));
|
||||
}
|
||||
afterTimes.push_back(std::chrono::duration<double, std::milli>(Clock::now() - afterStart).count());
|
||||
++measurement.sampledTiles;
|
||||
}
|
||||
measurement.beforeP95Milliseconds = Percentile95(beforeTimes);
|
||||
measurement.afterP95Milliseconds = Percentile95(afterTimes);
|
||||
measurement.beforeMedianMilliseconds = Median(beforeTimes);
|
||||
measurement.afterMedianMilliseconds = Median(afterTimes);
|
||||
measurement.beforeHash = Hex64(HashValues(beforeValues));
|
||||
measurement.afterHash = Hex64(HashValues(afterValues));
|
||||
const std::size_t common = std::min(beforeValues.size(), afterValues.size());
|
||||
for (std::size_t index = 0; index < common; ++index)
|
||||
if (beforeValues[index] != afterValues[index]) ++measurement.exactMismatches;
|
||||
|
||||
if (field.plan() && !field.plan()->islands.empty())
|
||||
{
|
||||
constexpr std::uint32_t droppedFeature = 0;
|
||||
const auto& island = field.plan()->islands.front();
|
||||
const std::array<std::int64_t, 3> point{island.centerSubvoxels[0], island.centerSubvoxels[1],
|
||||
island.bottomZSubvoxels + (island.topZSubvoxels - island.bottomZSubvoxels) / 2};
|
||||
std::array<std::int64_t, 3> minimum{};
|
||||
std::array<std::int64_t, 3> maximum{};
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
minimum[axis] = FloorDiv(point[axis], TileQ) * TileQ;
|
||||
maximum[axis] = minimum[axis] + TileQ;
|
||||
}
|
||||
auto sampleMinimum = minimum;
|
||||
auto sampleMaximum = maximum;
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
sampleMinimum[axis] -= MesherHaloQ;
|
||||
sampleMaximum[axis] += MesherHaloQ;
|
||||
}
|
||||
FloatingIslandTileFeatureListV1 dropped = field.buildFeatureTileList(
|
||||
sampleMinimum, sampleMaximum);
|
||||
auto candidate = std::lower_bound(dropped.featureIndices.begin(), dropped.featureIndices.end(),
|
||||
droppedFeature);
|
||||
if (candidate != dropped.featureIndices.end() && *candidate == droppedFeature)
|
||||
{
|
||||
dropped.featureIndices.erase(candidate);
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
dropped.groupBounds[droppedFeature].minimumSubvoxels[axis] += 100000000000ll;
|
||||
dropped.groupBounds[droppedFeature].maximumSubvoxels[axis] += 100000000000ll;
|
||||
}
|
||||
const auto order = std::find(dropped.groupSearchOrder.begin(),
|
||||
dropped.groupSearchOrder.end(), droppedFeature);
|
||||
if (order != dropped.groupSearchOrder.end())
|
||||
{
|
||||
const std::size_t orderIndex = static_cast<std::size_t>(order - dropped.groupSearchOrder.begin());
|
||||
dropped.groupSearchOrder.erase(order);
|
||||
dropped.groupDistanceLowerBoundsSubvoxels.erase(
|
||||
dropped.groupDistanceLowerBoundsSubvoxels.begin() + orderIndex);
|
||||
dropped.groupSearchComplete = false;
|
||||
const auto full = field.densitySubvoxels(point[0], point[1], point[2]);
|
||||
const WorldAssemblyFeatureQueryV1 badQuery{field.recipe(),field.plan(),dropped};
|
||||
const ScopedWorldAssemblyFeatureQueriesV1 badScope(std::span(&badQuery,1),true);
|
||||
const auto bad = field.densitySubvoxels(point[0],point[1],point[2]);
|
||||
if (full != bad) ++measurement.droppedFeatureDetected;
|
||||
}
|
||||
}
|
||||
}
|
||||
measurement.negativeControlPassed = measurement.droppedFeatureDetected > 0;
|
||||
return measurement;
|
||||
}
|
||||
|
||||
void Print(const char* label, const Measurement& measurement,
|
||||
std::size_t islandCount, std::size_t connectorCount)
|
||||
{
|
||||
std::cout << label << " islands=" << islandCount << " connectors=" << connectorCount
|
||||
<< " forced_candidates=1 list_tiles_built=" << measurement.tilesBuilt
|
||||
<< " sampled_tiles=" << measurement.sampledTiles
|
||||
<< " sample_count=" << measurement.samples
|
||||
<< " mean_candidates=" << measurement.meanCandidateFeatures
|
||||
<< " p95_candidates=" << measurement.p95CandidateFeatures
|
||||
<< " list_build_total_ms=" << measurement.listBuildTotalMilliseconds
|
||||
<< " list_build_p95_ms=" << measurement.listBuildP95Milliseconds
|
||||
<< " full_tile_p95_ms=" << measurement.beforeP95Milliseconds
|
||||
<< " candidate_tile_p95_ms=" << measurement.afterP95Milliseconds
|
||||
<< " full_tile_median_ms=" << measurement.beforeMedianMilliseconds
|
||||
<< " candidate_tile_median_ms=" << measurement.afterMedianMilliseconds
|
||||
<< " exact_mismatches=" << measurement.exactMismatches
|
||||
<< " full_hash=" << measurement.beforeHash
|
||||
<< " candidate_hash=" << measurement.afterHash
|
||||
<< " negative_control_detected=" << measurement.negativeControlPassed << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
int IslandAuditMain(int argc, char** argv)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (argc != 3)
|
||||
{ std::cerr << "usage: FloatingIslandTileListAudit <manifest.json> <output.json>\n"; return 2; }
|
||||
std::ifstream input(argv[1], std::ios::binary);
|
||||
if (!input) throw std::runtime_error("could not open the recorded round-3 island manifest");
|
||||
const std::string json{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
std::string error;
|
||||
const auto manifest = FloatingIslandManifestV1::FromJson(json, error);
|
||||
if (!manifest || !manifest->validate(error)) throw std::runtime_error("manifest invalid: " + error);
|
||||
|
||||
FloatingIslandFieldV1 field(manifest->recipe, manifest->plan);
|
||||
if (!field.isValid()) throw std::runtime_error("round-3 production field did not validate");
|
||||
const auto bounds = field.influenceBounds();
|
||||
const auto& center=manifest->plan.islands.front().centerSubvoxels;
|
||||
WorldAssemblyFeatureQueryV1 guardQuery{field.recipe(),field.plan(),field.buildFeatureTileList(center,center)};
|
||||
if(!guardQuery.features.candidateEvaluationUseful)
|
||||
{
|
||||
const ScopedWorldAssemblyFeatureQueriesV1 preferredFull(std::span(&guardQuery,1));
|
||||
if(ScopedWorldAssemblyFeatureQueriesV1::find(field.recipe(),field.plan(),center[0],center[1],center[2]))
|
||||
throw std::runtime_error("shared scope ignored the full-scan cost hint");
|
||||
}
|
||||
const auto outsideX=center[0]+1000*Q;
|
||||
const auto outsideFull=field.densitySubvoxels(outsideX,center[1],center[2]);
|
||||
{
|
||||
const ScopedWorldAssemblyFeatureQueriesV1 scope(std::span(&guardQuery,1),true);
|
||||
if(field.densitySubvoxels(outsideX,center[1],center[2])!=outsideFull)
|
||||
throw std::runtime_error("outside-domain fallback changed density");
|
||||
{
|
||||
const ScopedWorldAssemblyFeatureQueriesV1 emptyScope(std::span<const WorldAssemblyFeatureQueryV1>{});
|
||||
if(ScopedWorldAssemblyFeatureQueriesV1::find(field.recipe(),field.plan(),center[0],center[1],center[2]))
|
||||
throw std::runtime_error("nested empty scope inherited candidates");
|
||||
}
|
||||
if(!ScopedWorldAssemblyFeatureQueriesV1::find(field.recipe(),field.plan(),center[0],center[1],center[2]))
|
||||
throw std::runtime_error("nested scope did not restore candidates");
|
||||
}
|
||||
if(ScopedWorldAssemblyFeatureQueriesV1::find(field.recipe(),field.plan(),center[0],center[1],center[2]))
|
||||
throw std::runtime_error("scope leaked candidates");
|
||||
const Measurement today = MeasureField(field, bounds.minimumSubvoxels, bounds.maximumSubvoxels);
|
||||
|
||||
const FloatingIslandPlanV1 stressPlan = MakeStressPlan(manifest->recipe, manifest->plan, 256);
|
||||
FloatingIslandFieldV1 stressField(manifest->recipe, stressPlan);
|
||||
if (!stressField.isValid()) throw std::runtime_error("synthetic stress field did not validate");
|
||||
const auto stressBounds = stressField.influenceBounds();
|
||||
const Measurement stress = MeasureField(stressField,
|
||||
stressBounds.minimumSubvoxels, stressBounds.maximumSubvoxels);
|
||||
|
||||
Print("today", today, manifest->plan.islands.size(), manifest->plan.clusterEdges.size());
|
||||
Print("stress", stress, stressPlan.islands.size(), stressPlan.clusterEdges.size());
|
||||
std::ofstream output(argv[2], std::ios::trunc);
|
||||
if (!output) throw std::runtime_error("could not create audit result JSON");
|
||||
output << std::setprecision(12) << "{\n"
|
||||
<< " \"baseline\":{\"islands\":" << manifest->plan.islands.size()
|
||||
<< ",\"connectors\":" << manifest->plan.clusterEdges.size()
|
||||
<< ",\"tiles_built\":" << today.tilesBuilt << ",\"sampled_tiles\":" << today.sampledTiles
|
||||
<< ",\"samples\":" << today.samples << ",\"mean_candidates\":" << today.meanCandidateFeatures
|
||||
<< ",\"p95_candidates\":" << today.p95CandidateFeatures
|
||||
<< ",\"list_build_total_ms\":" << today.listBuildTotalMilliseconds
|
||||
<< ",\"list_build_p95_ms\":" << today.listBuildP95Milliseconds
|
||||
<< ",\"before_p95_ms\":" << today.beforeP95Milliseconds
|
||||
<< ",\"after_p95_ms\":" << today.afterP95Milliseconds
|
||||
<< ",\"before_median_ms\":" << today.beforeMedianMilliseconds
|
||||
<< ",\"after_median_ms\":" << today.afterMedianMilliseconds
|
||||
<< ",\"exact_mismatches\":" << today.exactMismatches
|
||||
<< ",\"before_hash\":\"" << today.beforeHash << "\",\"after_hash\":\"" << today.afterHash
|
||||
<< "\",\"negative_control\":" << (today.negativeControlPassed ? "true" : "false") << "},\n"
|
||||
<< " \"stress\":{\"islands\":" << stressPlan.islands.size()
|
||||
<< ",\"connectors\":" << stressPlan.clusterEdges.size()
|
||||
<< ",\"tiles_built\":" << stress.tilesBuilt << ",\"sampled_tiles\":" << stress.sampledTiles
|
||||
<< ",\"samples\":" << stress.samples << ",\"mean_candidates\":" << stress.meanCandidateFeatures
|
||||
<< ",\"p95_candidates\":" << stress.p95CandidateFeatures
|
||||
<< ",\"list_build_total_ms\":" << stress.listBuildTotalMilliseconds
|
||||
<< ",\"list_build_p95_ms\":" << stress.listBuildP95Milliseconds
|
||||
<< ",\"before_p95_ms\":" << stress.beforeP95Milliseconds
|
||||
<< ",\"after_p95_ms\":" << stress.afterP95Milliseconds
|
||||
<< ",\"before_median_ms\":" << stress.beforeMedianMilliseconds
|
||||
<< ",\"after_median_ms\":" << stress.afterMedianMilliseconds
|
||||
<< ",\"exact_mismatches\":" << stress.exactMismatches
|
||||
<< ",\"before_hash\":\"" << stress.beforeHash << "\",\"after_hash\":\"" << stress.afterHash
|
||||
<< "\",\"negative_control\":" << (stress.negativeControlPassed ? "true" : "false") << "}\n}\n";
|
||||
if (!today.negativeControlPassed || !stress.negativeControlPassed)
|
||||
throw std::runtime_error("dropped-feature negative control was not detected");
|
||||
if (today.exactMismatches || stress.exactMismatches)
|
||||
throw std::runtime_error("tile feature evaluation changed canonical field values");
|
||||
std::cout << "TILE_LIST_AUDIT_PASS\n";
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
std::cerr << "FLOATING_ISLAND_TILE_LIST_AUDIT_FAIL: " << exception.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Surface finite features use the same scope as islands. Biome sites stay in their fixed local window.
|
||||
// Les features finies de surface partagent la portee; les sites biomes restent dans leur fenetre locale.
|
||||
void MeasureSurface(WorldAssemblyRecipeV1 recipe, WorldAssemblyStratumPlanV1 plan,
|
||||
const char* label, bool stress)
|
||||
{
|
||||
if(stress)
|
||||
{
|
||||
const auto originals=plan.features;
|
||||
for(int copy=1;copy<64;++copy)
|
||||
for(auto feature:originals)
|
||||
{
|
||||
feature.stableId=SyntheticId(static_cast<std::uint32_t>(plan.features.size()),0x53555246);
|
||||
for(int axis=0;axis<2;++axis)
|
||||
{
|
||||
const auto delta=(axis==0 ? copy%8 : copy/8)*512*Q;
|
||||
feature.minimumSubvoxels[axis]+=delta;
|
||||
feature.maximumSubvoxels[axis]+=delta;
|
||||
}
|
||||
plan.features.push_back(feature);
|
||||
}
|
||||
}
|
||||
if(stress)
|
||||
{
|
||||
// One composed stress case also exercises XY column writers at unrelated Z values.
|
||||
// Une fixture composee couvre aussi les modificateurs XY a des altitudes independantes.
|
||||
const auto& shelf=plan.features.front();
|
||||
WorldAssemblyFeatureV1 cliff;
|
||||
cliff.stableId=SyntheticId(10000,0x434c494646);
|
||||
cliff.type=1;
|
||||
cliff.minimumSubvoxels={(shelf.minimumSubvoxels[0]+shelf.maximumSubvoxels[0])/2-8*Q,
|
||||
(shelf.minimumSubvoxels[1]+shelf.maximumSubvoxels[1])/2-8*Q,0};
|
||||
cliff.maximumSubvoxels={cliff.minimumSubvoxels[0]+16*Q,cliff.minimumSubvoxels[1]+16*Q,0};
|
||||
cliff.parametersSubvoxels={8*Q,8*Q,0,0};
|
||||
auto cap=cliff;
|
||||
cap.stableId=SyntheticId(10001,0x434150);
|
||||
cap.type=3;
|
||||
cap.parametersSubvoxels={4*Q,32*Q,8*Q,0};
|
||||
plan.features.push_back(cliff);
|
||||
plan.features.push_back(cap);
|
||||
}
|
||||
if(stress) plan.contentHash ^= 0x5354524553534341ull; // Separate validation cache epochs. / Epoques distinctes.
|
||||
Heightfield2D field(recipe,plan);
|
||||
std::vector<double> fullTimes,candidateTimes,buildTimes,counts,fullColumnTimes,candidateColumnTimes;
|
||||
std::vector<std::int64_t> fullValues,candidateValues;
|
||||
std::uint64_t mismatches=0;
|
||||
bool negative=false;
|
||||
if(plan.features.empty())throw std::runtime_error("surface has no finite features to audit");
|
||||
// 64 bounded tile probes, spanning each feature and its faces, including normal halos.
|
||||
for(int tile=0;tile<64;++tile)
|
||||
{
|
||||
const auto& feature=plan.features[static_cast<std::size_t>(tile)%plan.features.size()];
|
||||
const std::int64_t x=(feature.minimumSubvoxels[0]+feature.maximumSubvoxels[0])/2;
|
||||
const std::int64_t y=(feature.minimumSubvoxels[1]+feature.maximumSubvoxels[1])/2;
|
||||
const auto column=field.point(x,y,0);
|
||||
const std::int64_t z=(tile%3==0) ? column.groundHeightSubvoxels :
|
||||
(tile%3==1 ? column.capHeightSubvoxels :
|
||||
(feature.minimumSubvoxels[2]+feature.maximumSubvoxels[2])/2);
|
||||
const std::array<std::int64_t,3> lo{x-17*Q,y-17*Q,z-17*Q};
|
||||
const std::array<std::int64_t,3> hi{x+17*Q,y+17*Q,z+17*Q};
|
||||
const auto buildStart=Clock::now();
|
||||
WorldAssemblyFeatureQueryV1 query{field.recipe(),field.plan(),field.buildFeatureTileList(lo,hi)};
|
||||
buildTimes.push_back(std::chrono::duration<double,std::milli>(Clock::now()-buildStart).count());
|
||||
counts.push_back(static_cast<double>(query.features.featureIndices.size()));
|
||||
auto sample=[&](std::vector<std::int64_t>& values)
|
||||
{
|
||||
for(int iz=0;iz<9;++iz)for(int iy=0;iy<9;++iy)for(int ix=0;ix<9;++ix)
|
||||
{
|
||||
const auto p=field.point(lo[0]+ix*4*Q,lo[1]+iy*4*Q,lo[2]+iz*4*Q);
|
||||
values.push_back(p.densitySubvoxels);
|
||||
values.push_back(p.groundHeightSubvoxels);
|
||||
values.push_back(p.capHeightSubvoxels);
|
||||
values.push_back(p.biomeId);
|
||||
}
|
||||
};
|
||||
// Change only the local cache epoch: both timed paths start with cold columns.
|
||||
// Epoch de cache local seulement : les deux parcours partent de colonnes froides.
|
||||
++plan.contentHash;
|
||||
Heightfield2D::resetThreadColumnCacheStats();
|
||||
const auto start=Clock::now();sample(fullValues);
|
||||
fullTimes.push_back(std::chrono::duration<double,std::milli>(Clock::now()-start).count());
|
||||
fullColumnTimes.push_back(Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds);
|
||||
++plan.contentHash;
|
||||
Heightfield2D::resetThreadColumnCacheStats();
|
||||
const auto candidateStart=Clock::now();
|
||||
{const ScopedWorldAssemblyFeatureQueriesV1 scope(std::span(&query,1),true);sample(candidateValues);}
|
||||
candidateTimes.push_back(std::chrono::duration<double,std::milli>(Clock::now()-candidateStart).count());
|
||||
candidateColumnTimes.push_back(Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds);
|
||||
if(!negative)
|
||||
{
|
||||
if (!query.features.featureIndices.empty())
|
||||
query.features.featureIndices.erase(query.features.featureIndices.begin());
|
||||
std::vector<std::int64_t> bad;
|
||||
++plan.contentHash;
|
||||
{const ScopedWorldAssemblyFeatureQueriesV1 scope(std::span(&query,1),true);sample(bad);}
|
||||
const auto offset=fullValues.size()-bad.size();
|
||||
for(std::size_t i=0;i<bad.size();++i)if(bad[i]!=fullValues[offset+i])negative=true;
|
||||
++plan.contentHash; // Discard deliberately poisoned negative-control columns. / Purger le controle negatif.
|
||||
}
|
||||
}
|
||||
for(std::size_t i=0;i<fullValues.size();++i)if(fullValues[i]!=candidateValues[i])++mismatches;
|
||||
std::cout<<"surface label="<<label<<" forced_candidates=1 features="<<plan.features.size()
|
||||
<<" values="<<fullValues.size()<<" full_hash="<<Hex64(HashValues(fullValues))
|
||||
<<" candidate_hash="<<Hex64(HashValues(candidateValues))
|
||||
<<" mismatches="<<mismatches<<" negative_control="<<negative
|
||||
<<" full_p95_ms="<<Percentile95(fullTimes)<<" candidate_p95_ms="<<Percentile95(candidateTimes)
|
||||
<<" build_p95_ms="<<Percentile95(buildTimes)<<" candidates_p95="<<Percentile95(counts)
|
||||
<<" full_column_p95_ms="<<Percentile95(fullColumnTimes)
|
||||
<<" candidate_column_p95_ms="<<Percentile95(candidateColumnTimes)<<'\n';
|
||||
if(mismatches||!negative)throw std::runtime_error("surface candidate proof failed");
|
||||
}
|
||||
|
||||
int main(int argc,char** argv)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(argc!=4)throw std::runtime_error("usage: audit owner_island_assembly owner_surface_assembly results_dir");
|
||||
const auto read=[](const char* path)
|
||||
{std::ifstream input(path);return std::string(std::istreambuf_iterator<char>(input),{});};
|
||||
std::string error;
|
||||
auto islands=WorldAssemblyManifest::FromJson(read(argv[1]),error);
|
||||
if(!islands||islands->floatingIslands.empty())throw std::runtime_error("island assembly: "+error);
|
||||
const auto islandPath=std::filesystem::path(argv[3])/"owner_island_manifest.json";
|
||||
{std::ofstream output(islandPath);output<<islands->floatingIslands.front().manifest.ToJson();}
|
||||
std::string exe=argv[0],islandFile=islandPath.string();
|
||||
std::string resultFile=(std::filesystem::path(argv[3])/"island_tile_lists.json").string();
|
||||
char* islandArgs[]{exe.data(),islandFile.data(),resultFile.data()};
|
||||
if(IslandAuditMain(3,islandArgs)!=0)return 1;
|
||||
auto surface=WorldAssemblyManifest::FromJson(read(argv[2]),error);
|
||||
if(!surface||surface->plan.strates.empty())throw std::runtime_error("surface assembly: "+error);
|
||||
MeasureSurface(surface->recipe,surface->plan.strates.front(),"owner",false);
|
||||
MeasureSurface(surface->recipe,surface->plan.strates.front(),"stress64",true);
|
||||
std::cout<<"SHARED_FEATURE_AUDIT_PASS\n";
|
||||
return 0;
|
||||
}
|
||||
catch(const std::exception& e){std::cerr<<"SHARED_FEATURE_AUDIT_FAIL: "<<e.what()<<'\n';return 1;}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
{
|
||||
"field_audit": [
|
||||
{
|
||||
"islands": 4,
|
||||
"connectors": 3,
|
||||
"forced_candidates": 1,
|
||||
"list_tiles_built": 108,
|
||||
"sampled_tiles": 64,
|
||||
"sample_count": 64000,
|
||||
"mean_candidates": 2.63158,
|
||||
"p95_candidates": 6,
|
||||
"list_build_total_ms": 0.0766,
|
||||
"list_build_p95_ms": 0.0012,
|
||||
"full_tile_p95_ms": 0.9803,
|
||||
"candidate_tile_p95_ms": 1.0194,
|
||||
"full_tile_median_ms": 0.85265,
|
||||
"candidate_tile_median_ms": 0.88875,
|
||||
"exact_mismatches": 0,
|
||||
"full_hash": "cb94db3ca317d001",
|
||||
"candidate_hash": "cb94db3ca317d001",
|
||||
"negative_control_detected": 1
|
||||
},
|
||||
{
|
||||
"islands": 256,
|
||||
"connectors": 240,
|
||||
"forced_candidates": 1,
|
||||
"list_tiles_built": 3362,
|
||||
"sampled_tiles": 64,
|
||||
"sample_count": 64000,
|
||||
"mean_candidates": 3.31231,
|
||||
"p95_candidates": 4,
|
||||
"list_build_total_ms": 216.615,
|
||||
"list_build_p95_ms": 0.0803,
|
||||
"full_tile_p95_ms": 110.57,
|
||||
"candidate_tile_p95_ms": 7.3619,
|
||||
"full_tile_median_ms": 109.41,
|
||||
"candidate_tile_median_ms": 4.12255,
|
||||
"exact_mismatches": 0,
|
||||
"full_hash": "d83bac7778f3d62a",
|
||||
"candidate_hash": "d83bac7778f3d62a",
|
||||
"negative_control_detected": 1
|
||||
},
|
||||
{
|
||||
"label": "owner",
|
||||
"forced_candidates": 1,
|
||||
"features": 1,
|
||||
"values": 186624,
|
||||
"full_hash": "4207efd4ac869108",
|
||||
"candidate_hash": "4207efd4ac869108",
|
||||
"mismatches": 0,
|
||||
"negative_control": 1,
|
||||
"full_p95_ms": 0.4748,
|
||||
"candidate_p95_ms": 0.4559,
|
||||
"build_p95_ms": 0.0003,
|
||||
"candidates_p95": 1,
|
||||
"full_column_p95_ms": 0.3679,
|
||||
"candidate_column_p95_ms": 0.3693
|
||||
},
|
||||
{
|
||||
"label": "stress64",
|
||||
"forced_candidates": 1,
|
||||
"features": 66,
|
||||
"values": 186624,
|
||||
"full_hash": "f1c6ea123284cc38",
|
||||
"candidate_hash": "f1c6ea123284cc38",
|
||||
"mismatches": 0,
|
||||
"negative_control": 1,
|
||||
"full_p95_ms": 0.7084,
|
||||
"candidate_p95_ms": 0.4078,
|
||||
"build_p95_ms": 0.0011,
|
||||
"candidates_p95": 1,
|
||||
"full_column_p95_ms": 0.3699,
|
||||
"candidate_column_p95_ms": 0.3552
|
||||
}
|
||||
],
|
||||
"default_tile_audit": [
|
||||
{
|
||||
"label": "islands",
|
||||
"lod": 0,
|
||||
"tiles": 8,
|
||||
"hashes_identical": 1,
|
||||
"full_p95_ms": 94.9835,
|
||||
"candidate_p95_ms": 95.0935,
|
||||
"list_p95_ms": 0.0021,
|
||||
"full_shell_p95_ms": 0.0002,
|
||||
"candidate_shell_p95_ms": 0.0003
|
||||
},
|
||||
{
|
||||
"label": "islands",
|
||||
"lod": 1,
|
||||
"tiles": 8,
|
||||
"hashes_identical": 1,
|
||||
"full_p95_ms": 87.2796,
|
||||
"candidate_p95_ms": 87.5774,
|
||||
"list_p95_ms": 0.0035,
|
||||
"full_shell_p95_ms": 0.0005,
|
||||
"candidate_shell_p95_ms": 0.0003
|
||||
},
|
||||
{
|
||||
"label": "islands",
|
||||
"lod": 7,
|
||||
"tiles": 8,
|
||||
"hashes_identical": 1,
|
||||
"full_p95_ms": 20.4421,
|
||||
"candidate_p95_ms": 20.4088,
|
||||
"list_p95_ms": 0.0017,
|
||||
"full_shell_p95_ms": 16.9176,
|
||||
"candidate_shell_p95_ms": 16.8824
|
||||
},
|
||||
{
|
||||
"label": "islands",
|
||||
"lod": 8,
|
||||
"tiles": 8,
|
||||
"hashes_identical": 1,
|
||||
"full_p95_ms": 73.0376,
|
||||
"candidate_p95_ms": 72.9233,
|
||||
"list_p95_ms": 0.0016,
|
||||
"full_shell_p95_ms": 69.9303,
|
||||
"candidate_shell_p95_ms": 69.8442
|
||||
},
|
||||
{
|
||||
"label": "surface",
|
||||
"lod": 0,
|
||||
"tiles": 8,
|
||||
"hashes_identical": 1,
|
||||
"full_p95_ms": 11.2377,
|
||||
"candidate_p95_ms": 11.1781,
|
||||
"list_p95_ms": 0.0006,
|
||||
"full_shell_p95_ms": 0.0001,
|
||||
"candidate_shell_p95_ms": 0.0001
|
||||
},
|
||||
{
|
||||
"label": "surface",
|
||||
"lod": 1,
|
||||
"tiles": 8,
|
||||
"hashes_identical": 1,
|
||||
"full_p95_ms": 7.3458,
|
||||
"candidate_p95_ms": 7.3544,
|
||||
"list_p95_ms": 0.0007,
|
||||
"full_shell_p95_ms": 0.0001,
|
||||
"candidate_shell_p95_ms": 0.0001
|
||||
},
|
||||
{
|
||||
"label": "surface",
|
||||
"lod": 7,
|
||||
"tiles": 8,
|
||||
"hashes_identical": 1,
|
||||
"full_p95_ms": 32.7389,
|
||||
"candidate_p95_ms": 32.3814,
|
||||
"list_p95_ms": 0.0006,
|
||||
"full_shell_p95_ms": 26.3776,
|
||||
"candidate_shell_p95_ms": 26.021
|
||||
},
|
||||
{
|
||||
"label": "surface",
|
||||
"lod": 8,
|
||||
"tiles": 8,
|
||||
"hashes_identical": 1,
|
||||
"full_p95_ms": 168.5449,
|
||||
"candidate_p95_ms": 168.0627,
|
||||
"list_p95_ms": 0.0008,
|
||||
"full_shell_p95_ms": 135.9596,
|
||||
"candidate_shell_p95_ms": 136.31
|
||||
}
|
||||
],
|
||||
"island_placement": [
|
||||
{
|
||||
"label": "FloatingIslands",
|
||||
"authored_full_p95_ms": 7.0706,
|
||||
"authored_candidate_p95_ms": 0.2385,
|
||||
"authored_active_batch_full_ms": 412.95957,
|
||||
"authored_active_batch_candidate_ms": 3.84022,
|
||||
"populated_cells": 14,
|
||||
"populated_placements": 572,
|
||||
"populated_full_p95_ms": 53.9875,
|
||||
"populated_candidate_p95_ms": 2.1501,
|
||||
"exact_transform_comparisons": 572,
|
||||
"anchor_checks": 572,
|
||||
"max_density_residual": 0.035156
|
||||
}
|
||||
],
|
||||
"island_active": [
|
||||
{
|
||||
"label": "FloatingIslands",
|
||||
"cells": 64,
|
||||
"placements": 572,
|
||||
"full_batch_ms": 3270.4782,
|
||||
"candidate_batch_ms": 28.43621,
|
||||
"merge_ms": 0.06231,
|
||||
"apply_ms": 0.6068,
|
||||
"applied_instances": 572,
|
||||
"full_total_cpu_ms": 3271.14731,
|
||||
"candidate_total_cpu_ms": 29.10532,
|
||||
"rhi": "null"
|
||||
}
|
||||
],
|
||||
"hash_matrices": [
|
||||
{
|
||||
"worker_hash_matrix": "1/8/16",
|
||||
"stable": 1,
|
||||
"density": "0x4dcfb37585d7d986",
|
||||
"mesh": "0x725a18374c5623de",
|
||||
"payload": "0xfb345d8cb319d18b",
|
||||
"topology": "0x76f2846be8efb5f4"
|
||||
},
|
||||
{
|
||||
"worker_hash_matrix": "1/8/16",
|
||||
"stable": 1,
|
||||
"density": "0x0cd2a1a4152ee4aa",
|
||||
"mesh": "0x17775c835711e5a3",
|
||||
"payload": "0xa27224958fc27d7b",
|
||||
"topology": "0x3821fea90b134ce4"
|
||||
}
|
||||
],
|
||||
"limits": {
|
||||
"unreal_core_limit": 12,
|
||||
"max_ubt_actions": 8,
|
||||
"max_standalone_compile_actions": 4,
|
||||
"process_limit_gib": 8,
|
||||
"launch_memory_gate_gib": 4,
|
||||
"step_limit_seconds": 900
|
||||
},
|
||||
"known_failed_run": "islands_hism_fixture_failed.log: exit 1; fixture collided with live landmark suppression; isolated fixture passes."
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"baseline":{"islands":4,"connectors":3,"tiles_built":108,"sampled_tiles":64,"samples":64000,"mean_candidates":2.63157894737,"p95_candidates":6,"list_build_total_ms":0.0766,"list_build_p95_ms":0.0012,"before_p95_ms":0.9803,"after_p95_ms":1.0194,"before_median_ms":0.85265,"after_median_ms":0.88875,"exact_mismatches":0,"before_hash":"cb94db3ca317d001","after_hash":"cb94db3ca317d001","negative_control":true},
|
||||
"stress":{"islands":256,"connectors":240,"tiles_built":3362,"sampled_tiles":64,"samples":64000,"mean_candidates":3.31231409875,"p95_candidates":4,"list_build_total_ms":216.6153,"list_build_p95_ms":0.0803,"before_p95_ms":110.5695,"after_p95_ms":7.3619,"before_median_ms":109.40955,"after_median_ms":4.12255,"exact_mismatches":0,"before_hash":"d83bac7778f3d62a","after_hash":"d83bac7778f3d62a","negative_control":true}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":1,"content_hash":"87222553193002d8","recipe":{"schema_version":1,"units_per_voxel":256,"owner_voxel_size_microns":125000,"core_voxel_size_microns":250000,"owner_to_core_scale_numerator":1,"owner_to_core_scale_denominator":2,"seed":0,"origin_x_q":0,"origin_y_q":0,"center_top_z_q":-126976,"layout_min_z_q":-258048,"layout_max_z_q":4096,"primary_island_count":3,"island_density_q16":32768,"spacing_q":14336,"minimum_radius_q":3072,"maximum_radius_q":6144,"thickness_ratio_q16":39322,"vertical_jitter_q16":39322,"top_flatten_q16":39322,"taper_end_min_q16":29491,"taper_end_max_q16":45875,"geometry_version":2,"crust_depth_bonus_q16":9830,"root_depth_ratio_q16":49152,"blend_radius_q":768,"contour_warp_q":1613,"roughness_q":384,"boundary_seal_q":512,"bridge_half_width_q":768,"bridge_thickness_q":1024,"route_constraints":{"version":1,"capsule_radius_q":348,"capsule_height_q":1802,"maximum_grade_q16":17560,"sweep_spacing_q":128,"support_probe_radius_q":230,"support_search_depth_q":512,"minimum_clearance_q":128,"minimum_support_probes":3},"content_hash":"5e0370d9f95f1c15"},"plan":{"content_hash":"dbbda4932378047a","islands":[{"stable_id":"e6e461e18b3a9c87","ordinal":0,"center_q":[-10035,4422,-129150],"radius_q":3605,"top_half_q":721,"top_z_q":-128429,"bottom_z_q":-133837,"taper_end_q16":41663,"bounds_min_q":[-17941,-3484,-136525],"bounds_max_q":[-2129,12328,-125741]},{"stable_id":"2bc1820baba047f6","ordinal":1,"center_q":[0,-5178,-127714],"radius_q":3693,"top_half_q":738,"top_z_q":-126976,"bottom_z_q":-132516,"taper_end_q16":33506,"bounds_min_q":[-7994,-13172,-135204],"bounds_max_q":[7994,2816,-124288]},{"stable_id":"adc259cbe2a395e9","ordinal":2,"center_q":[10035,4681,-127887],"radius_q":5181,"top_half_q":1036,"top_z_q":-126851,"bottom_z_q":-134623,"taper_end_q16":39164,"bounds_min_q":[553,-4801,-137311],"bounds_max_q":[19517,14163,-124163]},{"stable_id":"7855fb022cc561c9","ordinal":3,"center_q":[-6451,-19514,-124414],"radius_q":5840,"top_half_q":1168,"top_z_q":-123246,"bottom_z_q":-132006,"taper_end_q16":33440,"bounds_min_q":[-16592,-29655,-134694],"bounds_max_q":[3690,-9373,-120558]}],"cluster_edges":[{"stable_id":"eaa5dd310e846520","from_island_id":"e6e461e18b3a9c87","to_island_id":"2bc1820baba047f6","start_q":[-8680,3126,-128554],"end_q":[-1388,-3851,-126819],"half_width_q":768,"thickness_q":1024,"bounds_min_q":[-10600,-5771,-132778],"bounds_max_q":[532,5046,-125667],"sharp_tag_ready":true},{"stable_id":"034023599b466c51","from_island_id":"2bc1820baba047f6","to_island_id":"adc259cbe2a395e9","start_q":[1370,-3832,-126951],"end_q":[8113,2793,-126831],"half_width_q":768,"thickness_q":1024,"bounds_min_q":[-550,-5752,-131175],"bounds_max_q":[10033,4713,-125679],"sharp_tag_ready":true},{"stable_id":"4962fa885545fb26","from_island_id":"2bc1820baba047f6","to_island_id":"7855fb022cc561c9","start_q":[-788,-6929,-126846],"end_q":[-5205,-16745,-123222],"half_width_q":768,"thickness_q":1024,"bounds_min_q":[-7125,-18665,-131070],"bounds_max_q":[1132,-5009,-122070],"sharp_tag_ready":true}],"route":[{"feature_id":"2e805a1f1f778cbb","position_q":[-10035,4422,-127367],"radius_q":1024},{"feature_id":"5be3b2b1e01722ea","position_q":[-9318,3736,-127414],"radius_q":1024},{"feature_id":"ab4eda612c1f03b5","position_q":[-8601,3051,-127336],"radius_q":1024},{"feature_id":"ba2fd2b050d0dd8c","position_q":[-7885,2365,-127288],"radius_q":1024},{"feature_id":"fc5829ee129fdc4f","position_q":[-7168,1679,-127113],"radius_q":1024},{"feature_id":"1393bb00329a9d5e","position_q":[-6451,993,-126951],"radius_q":1024},{"feature_id":"1dabc9715c93e2c9","position_q":[-5734,308,-126760],"radius_q":1024},{"feature_id":"0ea2008a85e73050","position_q":[-5018,-378,-126563],"radius_q":1024},{"feature_id":"754f55a5f1465cf3","position_q":[-4301,-1064,-126408],"radius_q":1024},{"feature_id":"1b6752776b4a7d22","position_q":[-3584,-1749,-126236],"radius_q":1024},{"feature_id":"0ab3b704db76084d","position_q":[-2867,-2435,-126017],"radius_q":1024},{"feature_id":"bd6980911e0879c4","position_q":[-2150,-3121,-125838],"radius_q":1024},{"feature_id":"ad164d7d0c049d87","position_q":[-1434,-3807,-125738],"radius_q":1024},{"feature_id":"9661463acbada2d6","position_q":[-717,-4492,-125928],"radius_q":1024},{"feature_id":"280a96547fadb0a1","position_q":[0,-5178,-125908],"radius_q":1024},{"feature_id":"531455b42fc13108","position_q":[717,-4474,-125939],"radius_q":1024},{"feature_id":"55d04c20849d7fcb","position_q":[1434,-3770,-125798],"radius_q":1024},{"feature_id":"ac4ebe80860dbdfa","position_q":[2150,-3065,-125893],"radius_q":1024},{"feature_id":"ff942855bf8e3fc5","position_q":[2867,-2361,-125886],"radius_q":1024},{"feature_id":"e6b8bba6c6acaf9c","position_q":[3584,-1657,-125861],"radius_q":1024},{"feature_id":"ae47379bbd4f7fdf","position_q":[4301,-953,-125839],"radius_q":1024},{"feature_id":"98779442384a30ee","position_q":[5018,-249,-125844],"radius_q":1024},{"feature_id":"f4c7757b2ee9fe19","position_q":[5734,456,-125814],"radius_q":1024},{"feature_id":"3ea33fae0fLine truncated
|
||||
@@ -0,0 +1,9 @@
|
||||
attempt=1 exit=0 elapsed_s=8.7570529 peak_private_bytes=68067328
|
||||
Final timing replay: no overlapping BuildHost build or Unreal process.
|
||||
attempt=1 exit=0 elapsed_s=8.0385819 peak_private_bytes=66658304
|
||||
Final source timing replay: redundant scoped validation removed; idle build/Unreal.
|
||||
attempt=1 exit=0 elapsed_s=7.9966612 peak_private_bytes=66658304
|
||||
Final cost-hint source: force candidates for proof; no overlapping build/Unreal.
|
||||
attempt=1 exit=0 elapsed_s=8.0365247 peak_private_bytes=65884160
|
||||
Final composed stress: 64 shelves plus XY cliff/cap writers; no overlapping build/Unreal.
|
||||
attempt=1 exit=0 elapsed_s=7.8703235 peak_private_bytes=66654208
|
||||
@@ -12,6 +12,7 @@ param(
|
||||
[switch]$MismatchedManifestFallback,
|
||||
[switch]$CoreOffTest,
|
||||
[switch]$CorePlanOnly,
|
||||
[switch]$SharedFeatureAuditOnly,
|
||||
[int]$CoreTestSeed = [int]::MinValue
|
||||
)
|
||||
|
||||
@@ -34,7 +35,8 @@ $DerivedDataCache = Join-Path $PluginRoot 'Saved\BuildHost\VFCoreWorldDDC'
|
||||
$ZenData = Join-Path $OutputRoot 'ZenData'
|
||||
$EditorCmd = 'E:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
|
||||
$MemoryLimitBytes = 8GB
|
||||
$CrashRoots = @((Join-Path $HostRoot 'Saved\Crashes'), (Join-Path $StagePlugin 'Saved\Crashes'))
|
||||
$CrashRoots = @((Join-Path $HostRoot 'Saved\Crashes'), (Join-Path $StagePlugin 'Saved\Crashes'),
|
||||
(Join-Path $UserDirectory 'Saved\Crashes'))
|
||||
$CoreSizeSuffix = ''
|
||||
if ([Math]::Abs($CoreVoxelSizeCm - 25.0) -gt 0.000001) {
|
||||
$CoreSizeText = $CoreVoxelSizeCm.ToString('0.####', [Globalization.CultureInfo]::InvariantCulture)
|
||||
@@ -127,6 +129,10 @@ if ($HasManifestOverride -and -not (Test-Path -LiteralPath $WorldAssemblyManifes
|
||||
}
|
||||
|
||||
if ($MismatchedManifestFallback) { $RunName = 'mismatched_fallback' }
|
||||
if ($SharedFeatureAuditOnly) {
|
||||
if ($SelectedModeCount -ne 0) { throw '-SharedFeatureAuditOnly uses the SurfaceWorld owner route.' }
|
||||
$RunName = 'shared_feature_audit'
|
||||
}
|
||||
|
||||
$foreignEditors = @(Get-Process -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.ProcessName -like 'UnrealEditor*' })
|
||||
@@ -221,7 +227,7 @@ $CoreOffTestFlag = if ($CoreOffTest) { 1 } else { 0 }
|
||||
$CorePlanOnlyFlag = if ($CorePlanOnly) { 1 } else { 0 }
|
||||
$BaseArguments = @(
|
||||
('-project="{0}"' -f $HostProject),
|
||||
'-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound',
|
||||
'-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound', '-corelimit=12',
|
||||
'-stdout', '-FullStdOutLogOutput', '-forcelogflush', '-NoZenAutoLaunch',
|
||||
'-notraceserver', '-nocrashreports', '-ddc=NoZenLocalFallback',
|
||||
('-userdir="{0}"' -f $UserDirectory),
|
||||
@@ -241,6 +247,7 @@ $BaseArguments = @(
|
||||
("-corehybridfixturesonly=$HybridFixturesOnlyFlag"),
|
||||
('-out="{0}"' -f $OutputDirectory)
|
||||
)
|
||||
if ($SharedFeatureAuditOnly) { $BaseArguments += '-corefeatureauditonly' }
|
||||
if ($HasManifestOverride) {
|
||||
$BaseArguments += ('-coreworldassembly="{0}"' -f $StagedWorldAssembly)
|
||||
}
|
||||
@@ -285,6 +292,9 @@ try {
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath `
|
||||
-WindowStyle Hidden -PassThru
|
||||
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
|
||||
# W16 is a logical determinism partition; at most 12 logical CPUs execute host work.
|
||||
# W16 reste un partitionnement logique; limiter l'execution hote a 12 processeurs logiques.
|
||||
$Process.ProcessorAffinity = [IntPtr]0xFFF
|
||||
$StartedUtc = [datetime]::UtcNow
|
||||
$PeakPrivateBytes = [int64]0
|
||||
while (-not $Process.HasExited) {
|
||||
@@ -351,6 +361,16 @@ if (-not (Test-Path -LiteralPath $FinalLog -PathType Leaf)) {
|
||||
$FinalLog = Join-Path $LogDirectory "CoreWorldValidation-$RunName.retry.log"
|
||||
}
|
||||
$Text = Get-Content -LiteralPath $FinalLog -Raw
|
||||
if ($SharedFeatureAuditOnly) {
|
||||
if ($Text -notmatch '\[VoxelForgeSharedFeatureCost\] pass complete=1 warmed_routes=1') {
|
||||
throw "Shared feature audit did not pass: $FinalLog"
|
||||
}
|
||||
if (@(Get-NewCrashFolders).Count -gt 0) { throw 'Shared feature audit produced a new crash folder.' }
|
||||
Write-Host 'VOXELFORGE_SHARED_FEATURE_AUDIT_PASS warmed_routes=1'
|
||||
Write-Host "Core actor log: $FinalLog"
|
||||
Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes"
|
||||
return
|
||||
}
|
||||
if ($CoreFallbackTest) {
|
||||
foreach ($Marker in @(
|
||||
'[VoxelForgeCoreFallbackTest] fallback=PASS core_disabled=1 legacy_generator=1',
|
||||
|
||||
Reference in new issue
Block a user