From 6ddb834b1c6e23570a734f55fc5a767834f0cdfc Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sun, 18 Oct 2026 19:50:45 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- ARCHITECTURE.md | 38 ++ CODEMAP.md | 24 +- .../Private/VoxelContentManager.cpp | 220 ++++++- .../VoxelForge/Public/VoxelContentManager.h | 18 +- .../Private/VoxelForgeExploreCommandlet.cpp | 77 +++ .../include/VoxelForgeCore/VoxelForgeCore.h | 70 +- Standalone/Core/src/FloatingIslandsInternal.h | 26 +- Standalone/Core/src/ShapeWorldInternal.h | 51 +- Standalone/Core/src/VoxelForgeCore.cpp | 81 +-- Standalone/Core/src/WorldAssemblerInternal.h | 127 +++- .../gpu_field_port/LUNA_DIALOGUE.md | 56 ++ .../Round6/BuildSharedFeatureListAudit.ps1 | 61 ++ .../WorldAssemblerM3/Round6/REPORT.md | 269 ++++++++ .../Round6/SharedFeatureListAudit.cpp | 601 ++++++++++++++++++ .../Round6/results/SUMMARY.json | 224 +++++++ .../Round6/results/island_tile_lists.json | 4 + .../Round6/results/owner_island_manifest.json | 1 + .../Round6/results/shared_audit_ledger.txt | 9 + Tools/TestVoxelForgeCoreWorldHost.ps1 | 24 +- 19 files changed, 1849 insertions(+), 132 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Round6/BuildSharedFeatureListAudit.ps1 create mode 100644 Standalone/Research/WorldAssemblerM3/Round6/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Round6/SharedFeatureListAudit.cpp create mode 100644 Standalone/Research/WorldAssemblerM3/Round6/results/SUMMARY.json create mode 100644 Standalone/Research/WorldAssemblerM3/Round6/results/island_tile_lists.json create mode 100644 Standalone/Research/WorldAssemblerM3/Round6/results/owner_island_manifest.json create mode 100644 Standalone/Research/WorldAssemblerM3/Round6/results/shared_audit_ledger.txt diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4fabcf9..797d6ea 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/CODEMAP.md b/CODEMAP.md index 0a64e87..910a505 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -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 diff --git a/Source/VoxelForge/Private/VoxelContentManager.cpp b/Source/VoxelForge/Private/VoxelContentManager.cpp index 06fa37f..626517d 100644 --- a/Source/VoxelForge/Private/VoxelContentManager.cpp +++ b/Source/VoxelForge/Private/VoxelContentManager.cpp @@ -133,6 +133,7 @@ struct FCoreFloatingIslandFieldSnapshot VoxelForgeCore::FloatingIslandRecipeV1 Recipe; VoxelForgeCore::FloatingIslandPlanV1 Plan; TUniquePtr Field; + std::vector 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 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 Min{static_cast(std::floor(MinX*Q)), + static_cast(std::floor(MinY*Q)),static_cast(std::floor(MinZ*Q))}; + const std::array Max{static_cast(std::ceil(MaxX*Q)), + static_cast(std::ceil(MaxY*Q)),static_cast(std::ceil(MaxZ*Q))}; + std::vector 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 Bounds, + float X, float Y, float Step, float& Top, float& Bottom) +{ + constexpr double Q = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const auto Range = VoxelForgeCore::BuildWorldAssemblyFeatureColumnRangeV1(Bounds, + static_cast(std::llround(X*Q)),static_cast(std::llround(Y*Q))); + if (!Range.known) return true; + if (Range.empty) return false; + const float SupportTop = static_cast(Range.maximumZSubvoxels/Q); + const float SupportBottom = static_cast(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(FMath::Max(8, Settings->DecorationColumnDepthVoxels)); - TArray CellMilliseconds; + TArray 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 Spawns; FPlacementBuildCounters Counters; + TArray FullScanSpawns; + const double FullStart = FPlatformTime::Seconds(); + BuildCellSpawns(Generator, OwnerXf, Cell, CurrentCtx, Entries, EntryBiome, + static_cast(Seed), Grid.Spacing, static_cast(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(Seed), Grid.Spacing, static_cast(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 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(nullptr,TEXT("/Engine/BasicShapes/Cube.Cube")); + TMap FixtureBuilds; + TArray FixtureEntries; + TArray FixtureBiomes; + const int32 BiomeCount = CurrentCtx.BiomeCtx.IsValid() ? CurrentCtx.BiomeCtx.Biomes.Num() : 0; + for (int32 Biome=0;Biome PopulatedTimes, FullPopulatedTimes; + for (int32 Sample=0;Sample FullSpawns, Spawns; + const double FullStart=FPlatformTime::Seconds(); + BuildCellSpawns(Generator,OwnerXf,Cell,CurrentCtx,FixtureEntries,FixtureBiomes, + static_cast(Seed),4,static_cast(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(Seed),4,static_cast(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;IndexGetDensity(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(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& EntryBiome, uint32 InSeed, int32 Spacing, float Step, int32 MaxCrossings, float ColumnDepth, TArray& 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{}; + const VoxelForgeCore::ScopedWorldAssemblyFeatureQueriesV1 FeatureScope(FeatureQueries); + TArray 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& 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 diff --git a/Source/VoxelForge/Public/VoxelContentManager.h b/Source/VoxelForge/Public/VoxelContentManager.h index cf7c98e..cf868f8 100644 --- a/Source/VoxelForge/Public/VoxelContentManager.h +++ b/Source/VoxelForge/Public/VoxelContentManager.h @@ -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& EntryBiome, uint32 InSeed, int32 Spacing, float Step, int32 MaxCrossings, float ColumnDepth, TArray& 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(); diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 15ea3c8..f06511b 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -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& Keys,const TCHAR* Label,FString& OutError) +{ + for (const int32 LOD : {0,1,7,8}) + { + TArray 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 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 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) diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 798af49..3a27951 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -56,6 +56,18 @@ namespace VoxelForgeCore const std::array& tileMinimumSubvoxels, const std::array& 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 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 minimumSubvoxels{}; std::array 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 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 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 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 groupSearchOrder; std::vector 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 minimumSubvoxels{}; + std::array 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 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 queries_; + bool forceCandidates_ = false; + const ScopedWorldAssemblyFeatureQueriesV1* previous_ = nullptr; }; struct VFCORE_API FloatingIslandManifestV1 @@ -510,6 +559,7 @@ namespace VoxelForgeCore FloatingIslandTileFeatureListV1 buildFeatureTileList( const std::array& tileMinimumSubvoxels, const std::array& tileMaximumSubvoxels) const; + std::vector 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& tileMinimumSubvoxels, + const std::array& 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 diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h index 857669b..481e32b 100644 --- a/Standalone/Core/src/FloatingIslandsInternal.h +++ b/Standalone/Core/src/FloatingIslandsInternal.h @@ -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& tileMinimumSubvoxels, - const std::array& tileMaximumSubvoxels) const +std::vector FloatingIslandFieldV1::solidSupportBounds() const { - FloatingIslandTileFeatureListV1 result; - if (!isValid()) return result; std::vector 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& tileMinimumSubvoxels, + const std::array& 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(); diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index f037230..d9e84c1 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -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; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index efab0fb..a58f912 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -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 queryMinimum={ + (static_cast(origin.X)-step)*queryScale, + (static_cast(origin.Y)-step)*queryScale, + (static_cast(origin.Z)-step)*queryScale}; + const std::array queryMaximum={ + (static_cast(origin.X)+extent+step)*queryScale, + (static_cast(origin.Y)+extent+step)*queryScale, + (static_cast(origin.Z)+extent+step)*queryScale}; + const auto FeatureListStart=std::chrono::steady_clock::now(); + std::vector 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(CHUNK_SIZE) - *ownerToCoreNumerator/ownerToCoreDenominator; - if(coreVoxelsPerOwnerChunk<=0)continue; - const std::int64_t stratumBottom=static_cast(entry.bottomChunkZ) - *coreVoxelsPerOwnerChunk; - const std::int64_t stratumTop=(static_cast(entry.topChunkZ)+1) - *coreVoxelsPerOwnerChunk; - if(static_cast(origin.Z)>stratumTop - ||static_cast(origin.Z)+extentmanifest.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(step)*scale; - const std::array tileMinimum={ - static_cast(origin.X)*scale-sampleHalo, - static_cast(origin.Y)*scale-sampleHalo, - static_cast(origin.Z)*scale-sampleHalo}; - const std::array tileMaximum={ - (static_cast(origin.X)+extent)*scale+sampleHalo, - (static_cast(origin.Y)+extent)*scale+sampleHalo, - (static_cast(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( + 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( 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( + std::chrono::steady_clock::now()-FarShellStart).count(); if(impl_->worldAssembly&&key.lod>=8) { const auto CapStats=RefineLod8NativeCap(world,*impl_->worldAssembly,nativeKey,mesh); diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h index 96b22f3..368ff24 100644 --- a/Standalone/Core/src/WorldAssemblerInternal.h +++ b/Standalone/Core/src/WorldAssemblerInternal.h @@ -15,6 +15,45 @@ namespace VoxelForgeCore { +namespace +{ + thread_local const ScopedWorldAssemblyFeatureQueriesV1* ActiveAssemblyQueries = nullptr; +} + +ScopedWorldAssemblyFeatureQueriesV1::ScopedWorldAssemblyFeatureQueriesV1( + std::span 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 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 BuildWorldAssemblyFeatureTileListV1( std::span features, const std::array& tileMinimumSubvoxels, @@ -52,8 +91,54 @@ std::vector BuildWorldAssemblyFeatureTileListV1( return result; } +WorldAssemblyFeatureColumnRangeV1 BuildWorldAssemblyFeatureColumnRangeV1( + std::span 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(xbound.maximumSubvoxels[0] + ||ybound.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& 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(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& minimum, + const std::array& 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 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::min(); + bound.maximumSubvoxels[2]=std::numeric_limits::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::min()); + bound.maximumSubvoxels.fill(std::numeric_limits::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) { diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md index 68daff2..5ff4369 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md @@ -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 diff --git a/Standalone/Research/WorldAssemblerM3/Round6/BuildSharedFeatureListAudit.ps1 b/Standalone/Research/WorldAssemblerM3/Round6/BuildSharedFeatureListAudit.ps1 new file mode 100644 index 0000000..8272868 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round6/BuildSharedFeatureListAudit.ps1 @@ -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 +} diff --git a/Standalone/Research/WorldAssemblerM3/Round6/REPORT.md b/Standalone/Research/WorldAssemblerM3/Round6/REPORT.md new file mode 100644 index 0000000..5250690 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round6/REPORT.md @@ -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. diff --git a/Standalone/Research/WorldAssemblerM3/Round6/SharedFeatureListAudit.cpp b/Standalone/Research/WorldAssemblerM3/Round6/SharedFeatureListAudit.cpp new file mode 100644 index 0000000..bc2cb5a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round6/SharedFeatureListAudit.cpp @@ -0,0 +1,601 @@ +#include "VoxelForgeCore/VoxelForgeCore.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 minimum{}; + std::array 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& values) + { + std::uint64_t hash = 14695981039346656037ull; + for (const auto value : values) + for (int byte = 0; byte < 8; ++byte) + { + hash ^= static_cast(static_cast(value) >> (byte * 8)); + hash *= 1099511628211ull; + } + return hash; + } + + double Percentile95(std::vector 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 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& minimum, + std::array& maximum, const std::array& 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(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(index % Columns) * spacing; + const std::int64_t dy = static_cast(index / Columns) * spacing; + const std::int64_t dz = static_cast(index % 3) * 4 * Q; + const std::array 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(index); + island.stableId = SyntheticId(static_cast(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(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 BuildMixedTiles(const FloatingIslandFieldV1& field, + const std::array& tileLimitMinimum, + const std::array& tileLimitMaximum, + Measurement& measurement) + { + std::vector 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 sampleMinimum{ + x - MesherHaloQ, y - MesherHaloQ, z - MesherHaloQ}; + const std::array 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(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 limitMinimum, + std::array 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 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(tile.featureList.featureIndices.size())); } + measurement.listBuildP95Milliseconds = Percentile95(std::move(buildTimes)); + measurement.meanCandidateFeatures = std::accumulate(candidateCounts.begin(), candidateCounts.end(), 0.0) + / static_cast(candidateCounts.size()); + measurement.p95CandidateFeatures = Percentile95(std::move(candidateCounts)); + + constexpr std::size_t MaximumSampleTiles = 64; + if (tiles.size() > MaximumSampleTiles) + { + std::vector 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 beforeTimes, afterTimes; + std::vector 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 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> 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(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(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 point{island.centerSubvoxels[0], island.centerSubvoxels[1], + island.bottomZSubvoxels + (island.topZSubvoxels - island.bottomZSubvoxels) / 2}; + std::array minimum{}; + std::array 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(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 \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(input), std::istreambuf_iterator()}; + 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{}); + 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(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 fullTimes,candidateTimes,buildTimes,counts,fullColumnTimes,candidateColumnTimes; + std::vector 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(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 lo{x-17*Q,y-17*Q,z-17*Q}; + const std::array 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(Clock::now()-buildStart).count()); + counts.push_back(static_cast(query.features.featureIndices.size())); + auto sample=[&](std::vector& 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(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(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 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(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<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: "<