wip(assembler): round L21 -- resolved strate layout snapshot + validator (no world change)
Core ResolvedLayoutSnapshotV1 (canonical JSON, stable identity): per strate stable ID, generator, recipe, interior bounds in metres, ceiling/bedrock bands as Core really applies them, transition mode/width, dispatch provenance. Validator: compat mode reports today's deviations; strict mode enforces ceiling -> content -> bedrock -> next ceiling (owner rule), two separate bands, nominal contiguity, interpenetration only within the declared EVoxelStrateTransition width; 7 negative controls rejected. Findings: SW built at -120..-632 m although the params say -60 m; a 60 m unowned gap filled by analytic FI fallback; no active bedrock bands; transitions ignored by Core. All L19 hashes unchanged (28 + 14 crossing runs). The Unreal adapter exports resolved_layout (uncompiled here). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
1 parent
5d5447dc32
commit
8753f6dbe4
12 files changed
+1347
-3
No files matched your search
@@ -1869,6 +1869,36 @@ query, stratum position, scale, or asynchronous route changes. The strict LOD0
|
||||
audit and the matched before/after captures are in
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md).
|
||||
|
||||
### 8.39 Resolved layout snapshots and nominal strate bands — World Assembler L21
|
||||
|
||||
`ResolvedLayoutSnapshotV1` is a separate Core-owned record built from the resolved manager slots and
|
||||
the native SurfaceWorld/FloatingIslands plans. It has a canonical JSON representation, schema version,
|
||||
and stable FNV-1a identity. Its physical intervals use half-open micrometre coordinates; recipe anchors
|
||||
retain their serialized fixed-point units. The Unreal adapter exports it beside `world_assembly`, so the
|
||||
layout digest never salts a recipe, manifest, density, or mesh hash. The standalone exporter follows
|
||||
the same builder after plan resolution. It runs once per plan request and is not read from density or
|
||||
tile-worker loops.
|
||||
|
||||
Each record separates the nominal ceiling, content, and bedrock bands. A strict layout requires that
|
||||
sequence for every strate and requires bedrock(N) to touch the next strate's ceiling(N+1). The two
|
||||
bands remain separately owned. Gradient and Interleaved interfaces may declare interpenetration only
|
||||
inside their explicit transition zone; Hard has zero effective width. The manager's `TransitionBlendChunks`
|
||||
is expressed in owner-chunk metres. Interleaved's existing two-chunk domain warp extends its possible
|
||||
fingers beyond the blend width and is recorded separately. Compatibility validation reports current
|
||||
gaps, overlaps, ambiguities, inactive/missing bands, and unhonoured transitions while accepting them;
|
||||
strict validation rejects them. Core dispatches per-strate recipe fields without applying that
|
||||
parameter blend, even though the legacy manager helper implements it. Both band types default diggable,
|
||||
with protection represented explicitly.
|
||||
|
||||
Compatibility facts from L21: native SurfaceWorld plan/query bounds use 32 Core voxels (8 m) per
|
||||
dispatch chunk, while FloatingIslands converts the 32-voxel owner chunk through its 12.5/25 cm scale
|
||||
(16 Core voxels, 4 m). In the prototype where the top slot is FloatingIslands, this leaves a nominal
|
||||
60 m interval between the FI bottom (−60 m) and SurfaceWorld top (−120 m); the Core selectors fall
|
||||
through to the compiled analytic FloatingIslands fallback there. Core does not apply the manager's
|
||||
parameter blend at either recipe boundary. The FI v3 upper seal remains a ceiling inside its current
|
||||
content range, and its missing lower bedrock is recorded without changing evaluation. See
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux21/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux21/REPORT.md).
|
||||
|
||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||
|
||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||
|
||||
+2
-1
@@ -163,13 +163,14 @@ remains `0`, and lateral regions remain gated off.
|
||||
| `../../Standalone/Core/src/CoreNamespacePreamble.h` | First include of every Core TU. Forces FP contraction off per compiler (Clang/MSVC/GCC pragmas; unknown compiler = `#error`) and defines `vfcore::fp_contract::selfTest`, which `World::compile` runs before building: a fused multiply-add (e.g. Clang `-ffp-contract=fast`) refuses world generation. The world depends on contraction (Linux2/Linux3 reports), so never remove either. |
|
||||
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV1`: versioned micrometre intervals, owned ceiling/content/bedrock bands, transition and dispatch provenance, recipe anchors, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built from resolved plans; no evaluator call sites consume it. §8.39. |
|
||||
| `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
|
||||
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
|
||||
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. |
|
||||
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
|
||||
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; schema-v1 owner-asset export check and BuildHost Core actor tests. `-coreworldassembly=PATH` supplies a reviewed manifest transiently to `-coreworldtest=1`; it does not save a data asset. `FStandaloneCoreAdapterScheduler` runs Core work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. |
|
||||
| `Source/VoxelForge/Public/VoxelSettings.h` / `Private/VoxelSettings.cpp` | `UVoxelSettings` owns optional canonical world-assembly JSON and its Core digest. The editor file picker plus `ImportWorldAssemblyManifestJson` imports the reviewed manifest into the cooked settings asset and validates the recipe seed against the effective settings seed. |
|
||||
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Validates the optional manifest against each resolved `SurfaceWorld` slot (unique strate index and exact top/bottom chunks), then places canonical `world_assembly` JSON and its hash in the Core snapshot. Invalid plans fail Core initialization. |
|
||||
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Validates the optional manifest against each resolved `SurfaceWorld` slot (unique strate index and exact top/bottom chunks), then exports canonical `resolved_layout` separately from `world_assembly` and its hash. The layout digest does not alter recipe hashes. Invalid plans fail Core initialization. |
|
||||
| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. |
|
||||
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. |
|
||||
| `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. |
|
||||
|
||||
@@ -944,6 +944,199 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
MatchedStrateIndices.Add(Plan.strateIndex);
|
||||
}
|
||||
|
||||
// Keep the inspectable physical-layout export separate from the manifest identity: it is
|
||||
// assembled from the same resolved slots/plans, but adding it must not salt evaluation hashes.
|
||||
VoxelForgeCore::ResolvedLayoutSnapshotV1 Layout;
|
||||
Layout.worldIdentity = std::string("owner-world/seed/")
|
||||
+ std::to_string(StrateManager->GetWorldSeed());
|
||||
Layout.ownerVoxelSizeMicrometres = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
||||
Layout.coreVoxelSizeMicrometres = ExpectedCoreVoxelMicrons;
|
||||
const auto generatorName = [](ECaveGeneratorType Type) -> const char*
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork: return "TunnelNetwork";
|
||||
case ECaveGeneratorType::FlatPlain: return "FlatPlain";
|
||||
case ECaveGeneratorType::CrystalChamber: return "CrystalChamber";
|
||||
case ECaveGeneratorType::Maze: return "Maze";
|
||||
case ECaveGeneratorType::SurfaceWorld: return "SurfaceWorld";
|
||||
case ECaveGeneratorType::VerticalShafts: return "VerticalShafts";
|
||||
case ECaveGeneratorType::FloatingIslands: return "FloatingIslands";
|
||||
case ECaveGeneratorType::Underwater: return "Underwater";
|
||||
}
|
||||
return "Unknown";
|
||||
};
|
||||
const auto transitionName = [](EVoxelStrateTransition Type) -> const char*
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case EVoxelStrateTransition::Gradient: return "Gradient";
|
||||
case EVoxelStrateTransition::Hard: return "Hard";
|
||||
case EVoxelStrateTransition::Interleaved: return "Interleaved";
|
||||
}
|
||||
return "Unknown";
|
||||
};
|
||||
const std::int64_t OwnerVoxelMicrons = Layout.ownerVoxelSizeMicrometres;
|
||||
const auto FixedToMicrons = [ExpectedCoreVoxelMicrons](std::int64_t Fixed)
|
||||
{
|
||||
return static_cast<std::int64_t>(std::llround(static_cast<double>(Fixed)
|
||||
* ExpectedCoreVoxelMicrons / VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
||||
};
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(
|
||||
FIntVector(0, 0, Slot.TopChunkZ));
|
||||
VoxelForgeCore::ResolvedLayoutStratumV1 Item;
|
||||
Item.strateIndex = Slot.StrateIndex;
|
||||
Item.generatorKind = generatorName(Type);
|
||||
Item.stableId = Layout.worldIdentity + "/strate/" + std::to_string(Slot.StrateIndex)
|
||||
+ "/generator/" + generatorName(Type);
|
||||
Item.recipeId = Type == ECaveGeneratorType::SurfaceWorld ? "SurfaceWorld.Heightfield2D"
|
||||
: Type == ECaveGeneratorType::FloatingIslands ? "FloatingIslands.ClusterField"
|
||||
: std::string("legacy.") + generatorName(Type);
|
||||
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
|
||||
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
|
||||
Item.interiorBottomMicrometres = static_cast<std::int64_t>(Slot.BottomChunkZ)
|
||||
* CHUNK_SIZE * ExpectedCoreVoxelMicrons;
|
||||
Item.interiorTopMicrometres = static_cast<std::int64_t>(Slot.TopChunkZ + 1)
|
||||
* CHUNK_SIZE * ExpectedCoreVoxelMicrons;
|
||||
Item.managerChunkWidthMicrometres = CHUNK_SIZE * OwnerVoxelMicrons;
|
||||
Item.dispatchChunkWidthMicrometres = Type == ECaveGeneratorType::FloatingIslands
|
||||
? CHUNK_SIZE * OwnerVoxelMicrons : CHUNK_SIZE * ExpectedCoreVoxelMicrons;
|
||||
Item.transitionMode = transitionName(Slot.Definition->TransitionType);
|
||||
Item.transitionBlendChunks = Slot.Definition->TransitionBlendChunks;
|
||||
Item.transitionWidthMicrometres = static_cast<std::int64_t>(Item.transitionBlendChunks)
|
||||
* Item.managerChunkWidthMicrometres;
|
||||
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
|
||||
? 2 * Item.managerChunkWidthMicrometres : 0;
|
||||
Item.transitionInterpenetrationMicrometres = 0;
|
||||
Item.transitionHonouredInCore = false;
|
||||
Item.transitionDispatchPath = "upper strate owns lower interface; manager GetGenerationParams can blend legacy vectors, but Core's compiled field selects per-strate/native recipe fields without transition blending";
|
||||
Item.managerLookup = "UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate";
|
||||
if (Type == ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
Item.boundDispatchPath = "BuildSurfaceStratumPlan uses slot chunkZ * 32 Core voxels";
|
||||
Item.queryDispatchPath = "NativeSurfaceDensityAt uses floor(CoreZ / 32), then GetGeneratorTypeForChunk/GetStrateIndex";
|
||||
const auto Plan = std::find_if(Manifest->plan.strates.begin(), Manifest->plan.strates.end(),
|
||||
[&Slot](const auto& Entry) { return Entry.strateIndex == Slot.StrateIndex; });
|
||||
if (Plan != Manifest->plan.strates.end())
|
||||
{
|
||||
for (const auto& Profile : Manifest->recipe.biomes)
|
||||
if (Profile.hasCapProfile)
|
||||
{
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV1 Anchor;
|
||||
Anchor.kind = "cap_profile";
|
||||
Anchor.stableId = "biome/" + std::to_string(Profile.stableId);
|
||||
Anchor.units = "1/256 Core voxel";
|
||||
Anchor.values = { Profile.capHeightSubvoxels, Profile.capUndulationSubvoxels,
|
||||
Profile.capWavelengthSubvoxels, Profile.capDetailAmplitudeSubvoxels,
|
||||
Profile.capRidgeAmplitudeSubvoxels };
|
||||
Item.anchors.push_back(std::move(Anchor));
|
||||
}
|
||||
for (std::size_t RouteIndex = 0; RouteIndex < Plan->route.size(); ++RouteIndex)
|
||||
{
|
||||
const auto& Point = Plan->route[RouteIndex];
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV1 Anchor;
|
||||
Anchor.kind = "route_point"; Anchor.stableId = "route/" + std::to_string(RouteIndex);
|
||||
Anchor.units = "1/256 Core voxel";
|
||||
Anchor.values = { Point.positionSubvoxels[0], Point.positionSubvoxels[1],
|
||||
Point.positionSubvoxels[2], Point.radiusSubvoxels };
|
||||
Item.anchors.push_back(std::move(Anchor));
|
||||
}
|
||||
}
|
||||
Item.ceiling = { "ceiling", Item.interiorTopMicrometres, Item.interiorTopMicrometres,
|
||||
false, true, false, "no separate volumetric boundary band; cap profile remains content",
|
||||
"none", "Heightfield2D cap + AppendStratumBoundaryCeilings fallback mesh" };
|
||||
Item.bedrock = { "bedrock", Item.interiorBottomMicrometres,
|
||||
Item.interiorBottomMicrometres, false, true, false,
|
||||
"no lower bedrock band", "none", "none" };
|
||||
}
|
||||
else if (Type == ECaveGeneratorType::FloatingIslands)
|
||||
{
|
||||
Item.boundDispatchPath = "BuildFloatingIslandStratumPlan uses slot chunkZ * 32 * owner/Core voxel ratio";
|
||||
Item.queryDispatchPath = "NativeFloatingIslandDensityAt uses floor(CoreZ / (32 * owner/Core ratio)), then manager lookup";
|
||||
const auto Plan = std::find_if(Manifest->floatingIslands.begin(), Manifest->floatingIslands.end(),
|
||||
[&Slot](const auto& Entry) { return Entry.strateIndex == Slot.StrateIndex; });
|
||||
std::int64_t SealMicrometres = 0;
|
||||
if (Plan != Manifest->floatingIslands.end())
|
||||
{
|
||||
Item.interiorBottomMicrometres = FixedToMicrons(Plan->manifest.recipe.layoutMinimumZSubvoxels);
|
||||
Item.interiorTopMicrometres = FixedToMicrons(Plan->manifest.recipe.layoutMaximumZSubvoxels);
|
||||
SealMicrometres = FixedToMicrons(Plan->manifest.recipe.boundarySealSubvoxels);
|
||||
Item.recipeVersion = Plan->manifest.recipe.geometryVersion;
|
||||
for (std::size_t IslandIndex = 0; IslandIndex < Plan->manifest.plan.islands.size(); ++IslandIndex)
|
||||
{
|
||||
const auto& Island = Plan->manifest.plan.islands[IslandIndex];
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV1 Anchor;
|
||||
Anchor.kind = "island_anchor"; Anchor.stableId = "island/" + std::to_string(IslandIndex);
|
||||
Anchor.units = "1/256 Core voxel";
|
||||
Anchor.values = { Island.centerSubvoxels[0], Island.centerSubvoxels[1],
|
||||
Island.centerSubvoxels[2], Island.radiusSubvoxels, Island.topHalfSubvoxels };
|
||||
Item.anchors.push_back(std::move(Anchor));
|
||||
}
|
||||
}
|
||||
Item.ceiling = { "ceiling", Item.interiorTopMicrometres - SealMicrometres,
|
||||
Item.interiorTopMicrometres, SealMicrometres > 0, true, false,
|
||||
"v3 shared upper boundary seal; one-sided and inset from upper edge",
|
||||
"upper/interior side", "VF_ApplyUpperBoundarySeal" };
|
||||
Item.bedrock = { "bedrock", Item.interiorBottomMicrometres,
|
||||
Item.interiorBottomMicrometres, false, true, false,
|
||||
"no lower seal in FloatingIsland geometryVersion 3", "none", "none" };
|
||||
}
|
||||
else
|
||||
{
|
||||
Item.boundDispatchPath = "resolved manager slot scaled by Core chunk width";
|
||||
Item.queryDispatchPath = "legacy Core route; manager chunk selected by floor(CoreZ / 32)";
|
||||
const std::int64_t SealMicrometres = static_cast<std::int64_t>(std::llround(
|
||||
std::max(0.0f, Slot.Definition->GenerationParams.BoundarySealThickness)
|
||||
* OwnerVoxelMicrons));
|
||||
Item.ceiling = { "ceiling", Item.interiorTopMicrometres,
|
||||
Item.interiorTopMicrometres + SealMicrometres, false, true, false,
|
||||
"manager configures a two-sided legacy seal, but Core's compiled analytic field does not apply this density band",
|
||||
"configured upper side", "legacy VF_ApplyBoundarySeal; absent from current Core density path" };
|
||||
Item.bedrock = { "bedrock", Item.interiorBottomMicrometres - SealMicrometres,
|
||||
Item.interiorBottomMicrometres, false, true, false,
|
||||
"manager configures a two-sided legacy seal, but Core's compiled analytic field does not apply this density band; breakable by default",
|
||||
"configured lower side", "legacy VF_ApplyBoundarySeal; absent from current Core density path" };
|
||||
}
|
||||
Item.content = { "content", Item.interiorBottomMicrometres,
|
||||
Item.interiorTopMicrometres, true, true, false, "resolved generator interior",
|
||||
"none", "none" };
|
||||
for (std::size_t PassageIndex = 0; PassageIndex < StrateManager->GetPassages().Num(); ++PassageIndex)
|
||||
{
|
||||
const FVoxelPassage& Passage = StrateManager->GetPassages()[static_cast<int32>(PassageIndex)];
|
||||
if (Passage.UpperStrateIndex != Slot.StrateIndex && Passage.LowerStrateIndex != Slot.StrateIndex) continue;
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV1 Anchor;
|
||||
Anchor.kind = "passage"; Anchor.stableId = "passage/" + std::to_string(PassageIndex);
|
||||
Anchor.units = "milli owner voxels";
|
||||
Anchor.values = { static_cast<std::int64_t>(std::llround(Passage.UpperPoint.X * 1000.0)),
|
||||
static_cast<std::int64_t>(std::llround(Passage.UpperPoint.Y * 1000.0)),
|
||||
static_cast<std::int64_t>(std::llround(Passage.UpperPoint.Z * 1000.0)),
|
||||
static_cast<std::int64_t>(std::llround(Passage.LowerPoint.X * 1000.0)),
|
||||
static_cast<std::int64_t>(std::llround(Passage.LowerPoint.Y * 1000.0)),
|
||||
static_cast<std::int64_t>(std::llround(Passage.LowerPoint.Z * 1000.0)) };
|
||||
Item.anchors.push_back(std::move(Anchor));
|
||||
}
|
||||
Layout.strates.push_back(std::move(Item));
|
||||
}
|
||||
std::string LayoutError;
|
||||
if (!Layout.finalize(LayoutError))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core resolved-layout export failed: %s"), UTF8_TO_TCHAR(LayoutError.c_str()));
|
||||
return false;
|
||||
}
|
||||
const std::string LayoutJson = Layout.ToJson();
|
||||
const FString LayoutText = UTF8_TO_TCHAR(LayoutJson.c_str());
|
||||
TSharedPtr<FJsonObject> LayoutObject;
|
||||
const TSharedRef<TJsonReader<>> LayoutReader = TJsonReaderFactory<>::Create(LayoutText);
|
||||
if (!FJsonSerializer::Deserialize(LayoutReader, LayoutObject) || !LayoutObject.IsValid())
|
||||
{
|
||||
OutError = TEXT("Core resolved-layout canonical JSON could not be embedded in the resolved snapshot.");
|
||||
return false;
|
||||
}
|
||||
Root->SetObjectField(TEXT("resolved_layout"), LayoutObject.ToSharedRef());
|
||||
|
||||
const std::string CanonicalJson = Manifest->ToJson();
|
||||
if (CanonicalJson.empty())
|
||||
{
|
||||
|
||||
@@ -563,6 +563,96 @@ namespace VoxelForgeCore
|
||||
std::string ToJson() const;
|
||||
};
|
||||
|
||||
// Read-only, versioned account of the physical layout resolved by the director and the
|
||||
// Core adapter. Coordinates are half-open micrometre intervals; recipe anchors retain their
|
||||
// source fixed-point units so this record never rounds the recipe itself.
|
||||
inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV1 = 1;
|
||||
enum class ResolvedLayoutValidationModeV1 : std::uint8_t { compatibility, strict };
|
||||
|
||||
struct ResolvedLayoutAnchorV1
|
||||
{
|
||||
std::string kind;
|
||||
std::string stableId;
|
||||
std::string units;
|
||||
std::vector<std::int64_t> values;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutBandV1
|
||||
{
|
||||
std::string kind; // ceiling, content, bedrock
|
||||
std::int64_t bottomMicrometres = 0;
|
||||
std::int64_t topMicrometres = 0;
|
||||
bool presentInCurrentField = false;
|
||||
bool diggable = true;
|
||||
bool protectedBand = false;
|
||||
std::string behaviour;
|
||||
std::string densitySide;
|
||||
std::string capPath;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutStratumV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t order = 0;
|
||||
std::int32_t strateIndex = -1;
|
||||
std::string generatorKind;
|
||||
std::string recipeId;
|
||||
std::uint32_t recipeVersion = 0;
|
||||
std::int64_t interiorBottomMicrometres = 0;
|
||||
std::int64_t interiorTopMicrometres = 0;
|
||||
std::uint32_t voxelSizeMicrometres = 0;
|
||||
std::array<std::int64_t, 3> latticeOriginMicrometres{};
|
||||
ResolvedLayoutBandV1 ceiling;
|
||||
ResolvedLayoutBandV1 content;
|
||||
ResolvedLayoutBandV1 bedrock;
|
||||
std::string transitionMode; // mode at this strate's lower interface
|
||||
std::int32_t transitionBlendChunks = 0;
|
||||
std::int64_t transitionWidthMicrometres = 0;
|
||||
std::int64_t transitionWarpAmplitudeMicrometres = 0;
|
||||
std::int64_t transitionInterpenetrationMicrometres = 0;
|
||||
bool transitionHonouredInCore = false;
|
||||
std::string transitionDispatchPath;
|
||||
std::string boundDispatchPath;
|
||||
std::string queryDispatchPath;
|
||||
std::string managerLookup;
|
||||
std::int64_t managerChunkWidthMicrometres = 0;
|
||||
std::int64_t dispatchChunkWidthMicrometres = 0;
|
||||
std::vector<ResolvedLayoutAnchorV1> anchors;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutIssueV1
|
||||
{
|
||||
std::string code;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
struct VFCORE_API ResolvedLayoutSnapshotV1
|
||||
{
|
||||
std::uint32_t schemaVersion = ResolvedLayoutSchemaVersionV1;
|
||||
std::string worldIdentity;
|
||||
std::uint32_t ownerVoxelSizeMicrometres = 0;
|
||||
std::uint32_t coreVoxelSizeMicrometres = 0;
|
||||
std::vector<ResolvedLayoutStratumV1> strates;
|
||||
std::string stableIdentity;
|
||||
|
||||
bool finalize(std::string& error);
|
||||
bool validate(ResolvedLayoutValidationModeV1 mode,
|
||||
std::vector<ResolvedLayoutIssueV1>& issues,
|
||||
std::string& error) const;
|
||||
std::string ToJson() const;
|
||||
static std::optional<ResolvedLayoutSnapshotV1> FromJson(
|
||||
std::string_view json, std::string& error);
|
||||
};
|
||||
|
||||
struct ResolvedLayoutValidatorAuditV1
|
||||
{
|
||||
std::uint32_t controlsRun = 0;
|
||||
std::uint32_t controlsRejectedAsExpected = 0;
|
||||
bool passed = false;
|
||||
std::vector<ResolvedLayoutIssueV1> failures;
|
||||
};
|
||||
VFCORE_API ResolvedLayoutValidatorAuditV1 AuditResolvedLayoutValidatorV1();
|
||||
|
||||
struct FloatingIslandBoundsAuditResult
|
||||
{
|
||||
std::uint64_t boxesTested = 0;
|
||||
@@ -1154,6 +1244,8 @@ namespace VoxelForgeCore
|
||||
ShapeBuildStats buildStats() const;
|
||||
|
||||
float densityAt(Vec3d voxelPosition) const;
|
||||
// Audit-only selector provenance for the same point evaluator; never used by meshing.
|
||||
std::string densityDispatchPathAt(Vec3d voxelPosition) const;
|
||||
// Analytic source distance without the occupancy-preserving near-zero clamp or packed proxy.
|
||||
float unclampedDensityAt(Vec3d voxelPosition) const;
|
||||
DetailFieldSample detailSampleAt(Vec3d voxelPosition) const;
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
#pragma once
|
||||
|
||||
#include "Json.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
namespace VoxelForgeCore
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string LayoutEscape(std::string_view value)
|
||||
{
|
||||
std::string out="\"";
|
||||
for(unsigned char c:value)
|
||||
{
|
||||
switch(c)
|
||||
{
|
||||
case '"':out+="\\\"";break;
|
||||
case '\\':out+="\\\\";break;
|
||||
case '\b':out+="\\b";break;
|
||||
case '\f':out+="\\f";break;
|
||||
case '\n':out+="\\n";break;
|
||||
case '\r':out+="\\r";break;
|
||||
case '\t':out+="\\t";break;
|
||||
default:if(c<0x20){char b[7];std::snprintf(b,sizeof(b),"\\u%04x",c);out+=b;}
|
||||
else out+=static_cast<char>(c);
|
||||
}
|
||||
}
|
||||
out+='"';return out;
|
||||
}
|
||||
std::uint64_t LayoutHash(std::string_view value)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
for(unsigned char c:value){hash^=c;hash*=1099511628211ull;}
|
||||
return hash;
|
||||
}
|
||||
std::string LayoutHex(std::uint64_t value)
|
||||
{std::ostringstream out;out<<std::hex<<std::setfill('0')<<std::setw(16)<<value;return out.str();}
|
||||
std::int64_t LayoutTransitionLimit(const ResolvedLayoutStratumV1& stratum)
|
||||
{
|
||||
if(stratum.transitionMode=="Hard")return 0;
|
||||
return stratum.transitionWidthMicrometres
|
||||
+(stratum.transitionMode=="Interleaved"?stratum.transitionWarpAmplitudeMicrometres:0);
|
||||
}
|
||||
std::string LayoutBandJson(const ResolvedLayoutBandV1& band)
|
||||
{
|
||||
std::ostringstream out;out<<"{\"kind\":"<<LayoutEscape(band.kind)
|
||||
<<",\"bottom_um\":"<<band.bottomMicrometres<<",\"top_um\":"<<band.topMicrometres
|
||||
<<",\"present\":"<<(band.presentInCurrentField?"true":"false")
|
||||
<<",\"diggable\":"<<(band.diggable?"true":"false")
|
||||
<<",\"protected\":"<<(band.protectedBand?"true":"false")
|
||||
<<",\"behaviour\":"<<LayoutEscape(band.behaviour)
|
||||
<<",\"density_side\":"<<LayoutEscape(band.densitySide)
|
||||
<<",\"cap_path\":"<<LayoutEscape(band.capPath)<<'}';return out.str();
|
||||
}
|
||||
ResolvedLayoutBandV1 ParseLayoutBand(const VFJson::Value& value)
|
||||
{
|
||||
ResolvedLayoutBandV1 b;b.kind=value.Get("kind").String();
|
||||
b.bottomMicrometres=value.Get("bottom_um").Integer();b.topMicrometres=value.Get("top_um").Integer();
|
||||
b.presentInCurrentField=value.Get("present").Boolean();b.diggable=value.Get("diggable").Boolean(true);
|
||||
b.protectedBand=value.Get("protected").Boolean();b.behaviour=value.Get("behaviour").String();
|
||||
b.densitySide=value.Get("density_side").String();b.capPath=value.Get("cap_path").String();return b;
|
||||
}
|
||||
std::string LayoutBodyJson(const ResolvedLayoutSnapshotV1& s)
|
||||
{
|
||||
std::ostringstream out;out<<"{\"schema_version\":"<<s.schemaVersion
|
||||
<<",\"world_identity\":"<<LayoutEscape(s.worldIdentity)
|
||||
<<",\"owner_voxel_size_um\":"<<s.ownerVoxelSizeMicrometres
|
||||
<<",\"core_voxel_size_um\":"<<s.coreVoxelSizeMicrometres<<",\"strates\":[";
|
||||
for(std::size_t i=0;i<s.strates.size();++i)
|
||||
{
|
||||
if(i)out<<',';const auto& x=s.strates[i];
|
||||
out<<"{\"stable_id\":"<<LayoutEscape(x.stableId)<<",\"order\":"<<x.order
|
||||
<<",\"strate_index\":"<<x.strateIndex<<",\"generator_kind\":"<<LayoutEscape(x.generatorKind)
|
||||
<<",\"recipe_id\":"<<LayoutEscape(x.recipeId)<<",\"recipe_version\":"<<x.recipeVersion
|
||||
<<",\"interior_bottom_um\":"<<x.interiorBottomMicrometres
|
||||
<<",\"interior_top_um\":"<<x.interiorTopMicrometres
|
||||
<<",\"voxel_size_um\":"<<x.voxelSizeMicrometres
|
||||
<<",\"lattice_origin_um\":["<<x.latticeOriginMicrometres[0]<<','
|
||||
<<x.latticeOriginMicrometres[1]<<','<<x.latticeOriginMicrometres[2]<<']'
|
||||
<<",\"ceiling\":"<<LayoutBandJson(x.ceiling)
|
||||
<<",\"content\":"<<LayoutBandJson(x.content)
|
||||
<<",\"bedrock\":"<<LayoutBandJson(x.bedrock)
|
||||
<<",\"transition_mode\":"<<LayoutEscape(x.transitionMode)
|
||||
<<",\"transition_blend_chunks\":"<<x.transitionBlendChunks
|
||||
<<",\"transition_width_um\":"<<x.transitionWidthMicrometres
|
||||
<<",\"transition_warp_amplitude_um\":"<<x.transitionWarpAmplitudeMicrometres
|
||||
<<",\"transition_interpenetration_um\":"<<x.transitionInterpenetrationMicrometres
|
||||
<<",\"transition_honoured_in_core\":"<<(x.transitionHonouredInCore?"true":"false")
|
||||
<<",\"transition_dispatch_path\":"<<LayoutEscape(x.transitionDispatchPath)
|
||||
<<",\"bound_dispatch_path\":"<<LayoutEscape(x.boundDispatchPath)
|
||||
<<",\"query_dispatch_path\":"<<LayoutEscape(x.queryDispatchPath)
|
||||
<<",\"manager_lookup\":"<<LayoutEscape(x.managerLookup)
|
||||
<<",\"manager_chunk_width_um\":"<<x.managerChunkWidthMicrometres
|
||||
<<",\"dispatch_chunk_width_um\":"<<x.dispatchChunkWidthMicrometres
|
||||
<<",\"anchors\":[";
|
||||
for(std::size_t a=0;a<x.anchors.size();++a)
|
||||
{
|
||||
if(a)out<<',';const auto& anchor=x.anchors[a];
|
||||
out<<"{\"kind\":"<<LayoutEscape(anchor.kind)<<",\"stable_id\":"
|
||||
<<LayoutEscape(anchor.stableId)<<",\"units\":"<<LayoutEscape(anchor.units)<<",\"values\":[";
|
||||
for(std::size_t v=0;v<anchor.values.size();++v){if(v)out<<',';out<<anchor.values[v];}
|
||||
out<<"]}";
|
||||
}
|
||||
out<<"]}";
|
||||
}
|
||||
out<<"]}";return out.str();
|
||||
}
|
||||
std::int64_t LayoutInt(const VFJson::Value& obj,std::string_view key)
|
||||
{return obj.Get(key).Integer();}
|
||||
std::string LayoutString(const VFJson::Value& obj,std::string_view key)
|
||||
{return obj.Get(key).String();}
|
||||
bool LayoutBool(const VFJson::Value& obj,std::string_view key,bool fallback=false)
|
||||
{return obj.Get(key).Boolean(fallback);}
|
||||
}
|
||||
|
||||
std::string ResolvedLayoutSnapshotV1::ToJson() const
|
||||
{
|
||||
const std::string body=LayoutBodyJson(*this);
|
||||
return body.substr(0,body.size()-1)+",\"stable_identity\":"
|
||||
+LayoutEscape(stableIdentity.empty()?"resolved-layout-v1-fnv64:"+LayoutHex(LayoutHash(body)):stableIdentity)+"}";
|
||||
}
|
||||
|
||||
bool ResolvedLayoutSnapshotV1::finalize(std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
if(schemaVersion!=ResolvedLayoutSchemaVersionV1||worldIdentity.empty()||strates.empty())
|
||||
{error="resolved layout has an unsupported version, empty world identity, or no strates";return false;}
|
||||
std::stable_sort(strates.begin(),strates.end(),[](const auto& a,const auto& b)
|
||||
{return a.interiorTopMicrometres!=b.interiorTopMicrometres
|
||||
?a.interiorTopMicrometres>b.interiorTopMicrometres:a.order<b.order;});
|
||||
for(std::size_t i=0;i<strates.size();++i)
|
||||
{
|
||||
auto& x=strates[i];x.order=static_cast<std::int32_t>(i);
|
||||
if(x.stableId.empty())x.stableId=worldIdentity+"/strate/"+std::to_string(x.strateIndex);
|
||||
if(x.content.kind.empty())x.content.kind="content";
|
||||
if(x.ceiling.kind.empty())x.ceiling.kind="ceiling";
|
||||
if(x.bedrock.kind.empty())x.bedrock.kind="bedrock";
|
||||
if(x.interiorTopMicrometres<=x.interiorBottomMicrometres||x.voxelSizeMicrometres==0)
|
||||
{error="resolved layout contains an invalid interior interval or voxel size";return false;}
|
||||
}
|
||||
stableIdentity.clear();const std::string body=LayoutBodyJson(*this);
|
||||
stableIdentity="resolved-layout-v1-fnv64:"+LayoutHex(LayoutHash(body));
|
||||
std::vector<ResolvedLayoutIssueV1> issues;
|
||||
return validate(ResolvedLayoutValidationModeV1::compatibility,issues,error);
|
||||
}
|
||||
|
||||
bool ResolvedLayoutSnapshotV1::validate(ResolvedLayoutValidationModeV1 mode,
|
||||
std::vector<ResolvedLayoutIssueV1>& issues,std::string& error) const
|
||||
{
|
||||
issues.clear();error.clear();
|
||||
if(schemaVersion!=ResolvedLayoutSchemaVersionV1||worldIdentity.empty()||strates.empty())
|
||||
{error="resolved layout schema/world/strata header is invalid";return false;}
|
||||
const auto add=[&](std::string code,std::string detail)
|
||||
{issues.push_back({std::move(code),std::move(detail)});};
|
||||
const bool strict=mode==ResolvedLayoutValidationModeV1::strict;
|
||||
for(std::size_t i=0;i<strates.size();++i)
|
||||
{
|
||||
const auto& s=strates[i];
|
||||
const std::int64_t effectiveTransitionWidth=LayoutTransitionLimit(s);
|
||||
if(s.interiorTopMicrometres<=s.interiorBottomMicrometres)
|
||||
add("invalid_interior",s.stableId+" has non-positive content height");
|
||||
if(s.transitionInterpenetrationMicrometres>effectiveTransitionWidth
|
||||
||s.transitionInterpenetrationMicrometres<0)
|
||||
add("transition_overlap_exceeds_width",s.stableId+" transition interpenetration exceeds its declared width");
|
||||
if(s.managerChunkWidthMicrometres>0&&s.dispatchChunkWidthMicrometres>0
|
||||
&&s.managerChunkWidthMicrometres!=s.dispatchChunkWidthMicrometres)
|
||||
add("ambiguous_dispatch_units",s.stableId+" manager chunk width differs from Core dispatch chunk width");
|
||||
if(i+1<strates.size()&&s.transitionMode!="Hard"&&!s.transitionHonouredInCore)
|
||||
add("transition_not_honoured",s.stableId+" declares a blend/interleave that Core does not apply on this interface");
|
||||
if(!s.ceiling.presentInCurrentField)add("missing_ceiling",s.stableId+" has no explicit ceiling band in today's field");
|
||||
if(!s.bedrock.presentInCurrentField)add("missing_bedrock",s.stableId+" has no explicit bedrock floor in today's field");
|
||||
if(strict)
|
||||
{
|
||||
if(s.order!=static_cast<std::int32_t>(i))add("wrong_order",s.stableId+" order is not top-to-bottom");
|
||||
if(!s.ceiling.presentInCurrentField||!s.bedrock.presentInCurrentField)
|
||||
add(!s.ceiling.presentInCurrentField?"missing_ceiling":"missing_bedrock",s.stableId+" lacks a required boundary band");
|
||||
if(!s.content.presentInCurrentField)
|
||||
add("missing_content",s.stableId+" has no content generator band");
|
||||
const auto bandShape=[&](const ResolvedLayoutBandV1& b,std::string_view expected)
|
||||
{
|
||||
if(b.kind!=expected||b.topMicrometres<=b.bottomMicrometres)
|
||||
add("wrong_order",s.stableId+" has an invalid "+std::string(expected)+" band");
|
||||
if(b.protectedBand&&b.diggable)
|
||||
add("invalid_protection",s.stableId+" band is both protected and diggable");
|
||||
};
|
||||
bandShape(s.ceiling,"ceiling");bandShape(s.content,"content");bandShape(s.bedrock,"bedrock");
|
||||
if(s.ceiling.bottomMicrometres!=s.content.topMicrometres
|
||||
||s.content.bottomMicrometres!=s.bedrock.topMicrometres)
|
||||
add("wrong_order",s.stableId+" bands do not follow ceiling -> content -> bedrock");
|
||||
if(i+1<strates.size()
|
||||
&&s.bedrock.bottomMicrometres!=strates[i+1].ceiling.topMicrometres)
|
||||
add(s.bedrock.bottomMicrometres>strates[i+1].ceiling.topMicrometres?"gap":"overlap",
|
||||
s.stableId+" bedrock and next strate ceiling are not nominally contiguous");
|
||||
if(s.transitionInterpenetrationMicrometres>effectiveTransitionWidth)
|
||||
add("transition_overlap_exceeds_width",s.stableId+" interpenetration is wider than the declared transition zone");
|
||||
}
|
||||
}
|
||||
if(!strict)
|
||||
{
|
||||
for(const auto& s:strates)
|
||||
{
|
||||
if(s.content.presentInCurrentField&&s.ceiling.presentInCurrentField)
|
||||
{
|
||||
if(s.ceiling.bottomMicrometres<s.content.topMicrometres)
|
||||
add("band_overlap",s.stableId+" ceiling overlaps content by "+std::to_string(
|
||||
s.content.topMicrometres-s.ceiling.bottomMicrometres)+" um");
|
||||
else if(s.ceiling.bottomMicrometres>s.content.topMicrometres)
|
||||
add("uncovered_interval",s.stableId+" has a "+std::to_string(
|
||||
s.ceiling.bottomMicrometres-s.content.topMicrometres)+" um gap below its ceiling");
|
||||
}
|
||||
if(s.content.presentInCurrentField&&s.bedrock.presentInCurrentField)
|
||||
{
|
||||
if(s.bedrock.topMicrometres>s.content.bottomMicrometres)
|
||||
add("band_overlap",s.stableId+" bedrock overlaps content by "+std::to_string(
|
||||
s.bedrock.topMicrometres-s.content.bottomMicrometres)+" um");
|
||||
else if(s.bedrock.topMicrometres<s.content.bottomMicrometres)
|
||||
add("uncovered_interval",s.stableId+" has a "+std::to_string(
|
||||
s.content.bottomMicrometres-s.bedrock.topMicrometres)+" um gap above its bedrock");
|
||||
}
|
||||
}
|
||||
for(std::size_t i=0;i+1<strates.size();++i)
|
||||
{
|
||||
const auto& a=strates[i];const auto& b=strates[i+1];
|
||||
const std::int64_t effectiveTransitionWidth=LayoutTransitionLimit(a);
|
||||
if(a.interiorBottomMicrometres>b.interiorTopMicrometres)
|
||||
add("uncovered_interval",a.stableId+" to "+b.stableId+" leaves "+std::to_string(
|
||||
a.interiorBottomMicrometres-b.interiorTopMicrometres)+" um uncovered");
|
||||
else if(a.interiorBottomMicrometres<b.interiorTopMicrometres)
|
||||
{
|
||||
const std::int64_t overlap=b.interiorTopMicrometres-a.interiorBottomMicrometres;
|
||||
if(overlap>effectiveTransitionWidth)
|
||||
add("overlap",a.stableId+" and "+b.stableId+" interiors overlap by "+std::to_string(overlap)+" um");
|
||||
else add("transition_interpenetration",a.stableId+" and "+b.stableId+" overlap inside the declared transition zone");
|
||||
}
|
||||
if(a.bedrock.presentInCurrentField&&b.ceiling.presentInCurrentField)
|
||||
{
|
||||
if(a.bedrock.bottomMicrometres>b.ceiling.topMicrometres)
|
||||
add("uncovered_interval",a.stableId+" bedrock and "+b.stableId+" ceiling leave "+std::to_string(
|
||||
a.bedrock.bottomMicrometres-b.ceiling.topMicrometres)+" um uncovered");
|
||||
else if(a.bedrock.bottomMicrometres<b.ceiling.topMicrometres)
|
||||
{
|
||||
const std::int64_t overlap=b.ceiling.topMicrometres-a.bedrock.bottomMicrometres;
|
||||
if(overlap>effectiveTransitionWidth)
|
||||
add("overlap",a.stableId+" bedrock and "+b.stableId+" ceiling overlap by "+std::to_string(overlap)+" um");
|
||||
else add("transition_interpenetration",a.stableId+" bedrock and "+b.stableId+" ceiling interpenetrate by "+std::to_string(overlap)+" um inside the declared transition zone");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<ResolvedLayoutSnapshotV1> ResolvedLayoutSnapshotV1::FromJson(
|
||||
std::string_view json,std::string& error)
|
||||
{
|
||||
VFJson::Value root;if(!VFJson::Parser(std::string(json)).Parse(root,error))return std::nullopt;
|
||||
if(!root.IsObject()){error="resolved layout JSON root must be an object";return std::nullopt;}
|
||||
ResolvedLayoutSnapshotV1 s;s.schemaVersion=static_cast<std::uint32_t>(LayoutInt(root,"schema_version"));
|
||||
s.worldIdentity=LayoutString(root,"world_identity");
|
||||
s.ownerVoxelSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(root,"owner_voxel_size_um"));
|
||||
s.coreVoxelSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(root,"core_voxel_size_um"));
|
||||
for(const auto& v:root.Get("strates").AsArray())
|
||||
{
|
||||
ResolvedLayoutStratumV1 x;x.stableId=LayoutString(v,"stable_id");
|
||||
x.order=static_cast<std::int32_t>(LayoutInt(v,"order"));x.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
||||
x.generatorKind=LayoutString(v,"generator_kind");x.recipeId=LayoutString(v,"recipe_id");
|
||||
x.recipeVersion=static_cast<std::uint32_t>(LayoutInt(v,"recipe_version"));
|
||||
x.interiorBottomMicrometres=LayoutInt(v,"interior_bottom_um");x.interiorTopMicrometres=LayoutInt(v,"interior_top_um");
|
||||
x.voxelSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(v,"voxel_size_um"));
|
||||
const auto& origin=v.Get("lattice_origin_um").AsArray();for(std::size_t i=0;i<3&&i<origin.size();++i)x.latticeOriginMicrometres[i]=origin[i].Integer();
|
||||
x.ceiling=ParseLayoutBand(v.Get("ceiling"));x.content=ParseLayoutBand(v.Get("content"));x.bedrock=ParseLayoutBand(v.Get("bedrock"));
|
||||
x.transitionMode=LayoutString(v,"transition_mode");x.transitionBlendChunks=static_cast<std::int32_t>(LayoutInt(v,"transition_blend_chunks"));
|
||||
x.transitionWidthMicrometres=LayoutInt(v,"transition_width_um");
|
||||
x.transitionWarpAmplitudeMicrometres=LayoutInt(v,"transition_warp_amplitude_um");
|
||||
x.transitionInterpenetrationMicrometres=LayoutInt(v,"transition_interpenetration_um");
|
||||
x.transitionHonouredInCore=LayoutBool(v,"transition_honoured_in_core");x.transitionDispatchPath=LayoutString(v,"transition_dispatch_path");
|
||||
x.boundDispatchPath=LayoutString(v,"bound_dispatch_path");x.queryDispatchPath=LayoutString(v,"query_dispatch_path");
|
||||
x.managerLookup=LayoutString(v,"manager_lookup");x.managerChunkWidthMicrometres=LayoutInt(v,"manager_chunk_width_um");
|
||||
x.dispatchChunkWidthMicrometres=LayoutInt(v,"dispatch_chunk_width_um");
|
||||
for(const auto& a:v.Get("anchors").AsArray())
|
||||
{ResolvedLayoutAnchorV1 p;p.kind=LayoutString(a,"kind");p.stableId=LayoutString(a,"stable_id");p.units=LayoutString(a,"units");
|
||||
for(const auto& value:a.Get("values").AsArray())p.values.push_back(value.Integer());x.anchors.push_back(std::move(p));}
|
||||
s.strates.push_back(std::move(x));
|
||||
}
|
||||
const std::string serializedIdentity=LayoutString(root,"stable_identity");
|
||||
if(!s.finalize(error))return std::nullopt;
|
||||
if(s.stableIdentity!=serializedIdentity){error="resolved layout stable identity does not match its canonical payload";return std::nullopt;}
|
||||
return s;
|
||||
}
|
||||
|
||||
ResolvedLayoutValidatorAuditV1 AuditResolvedLayoutValidatorV1()
|
||||
{
|
||||
ResolvedLayoutValidatorAuditV1 result;
|
||||
auto make=[](std::string id,std::int32_t order,std::int64_t top,std::int64_t bottom)
|
||||
{
|
||||
ResolvedLayoutStratumV1 s;s.stableId=std::move(id);s.order=order;s.strateIndex=order;s.generatorKind="synthetic";
|
||||
s.interiorTopMicrometres=top;s.interiorBottomMicrometres=bottom;s.voxelSizeMicrometres=250000;
|
||||
s.transitionMode="Gradient";s.transitionWidthMicrometres=4000000;
|
||||
s.transitionHonouredInCore=true;
|
||||
s.ceiling={"ceiling",top,top+1000000,true,true,false,"closed ceiling band","upper face","synthetic"};
|
||||
s.content={"content",bottom,top,true,true,false,"generator content","none","none"};
|
||||
s.bedrock={"bedrock",bottom-1000000,bottom,true,true,false,"breakable bedrock","lower face","none"};
|
||||
return s;
|
||||
};
|
||||
const auto expectReject=[&](std::string name,ResolvedLayoutSnapshotV1 candidate)
|
||||
{
|
||||
++result.controlsRun;std::vector<ResolvedLayoutIssueV1> issues;std::string error;
|
||||
if(!candidate.validate(ResolvedLayoutValidationModeV1::strict,issues,error))++result.controlsRejectedAsExpected;
|
||||
else result.failures.push_back({"negative_control_accepted",std::move(name)+" was accepted by strict validator"});
|
||||
};
|
||||
auto base=ResolvedLayoutSnapshotV1{};base.worldIdentity="synthetic";base.strates={make("a",0,10000000,0),make("b",1,-2000000,-12000000)};
|
||||
{std::vector<ResolvedLayoutIssueV1> issues;std::string error;
|
||||
if(!base.validate(ResolvedLayoutValidationModeV1::strict,issues,error))
|
||||
result.failures.push_back({"positive_control_rejected","valid ceiling/content/bedrock layout was rejected"});}
|
||||
auto boundaryGap=base;boundaryGap.strates[1].ceiling.topMicrometres-=1;
|
||||
expectReject("bedrock/next-ceiling gap",boundaryGap);
|
||||
auto missingBedrock=base;missingBedrock.strates[0].bedrock.presentInCurrentField=false;expectReject("missing bedrock",missingBedrock);
|
||||
auto missingCeiling=base;missingCeiling.strates[0].ceiling.presentInCurrentField=false;expectReject("missing ceiling",missingCeiling);
|
||||
auto wrongOrder=base;wrongOrder.strates[0].content.kind="bedrock";expectReject("wrong band order",wrongOrder);
|
||||
auto largeOverlap=base;largeOverlap.strates[0].transitionInterpenetrationMicrometres=4000001;expectReject("interpenetration exceeds transition width",largeOverlap);
|
||||
auto genericOverlap=base;genericOverlap.strates[0].bedrock.bottomMicrometres-=1;expectReject("nominal band overlap",genericOverlap);
|
||||
auto genericGap=base;genericGap.strates[0].bedrock.bottomMicrometres+=1;expectReject("nominal band gap",genericGap);
|
||||
result.passed=result.controlsRun==result.controlsRejectedAsExpected&&result.failures.empty();return result;
|
||||
}
|
||||
}
|
||||
@@ -1095,6 +1095,32 @@ public:
|
||||
return AnalyticDensityAt(X,Y,Z);
|
||||
}
|
||||
|
||||
std::string DensityDispatchPathAt(float X,float Y,float Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->DensityDispatchPathAt(X,Y,Z);
|
||||
float Density=0.0f;
|
||||
if(NativeFloatingIslandDensityAt(X,Y,Z,Density))return "FloatingIslands-native";
|
||||
if(NativeSurfaceDensityAt(X,Y,Z,Density))return "SurfaceWorld-native";
|
||||
if(UsesLegacySurfaceField(X,Y,Z))return "SurfaceWorld-legacy";
|
||||
if(Owner&&Owner->Manager)
|
||||
{
|
||||
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CHUNK_SIZE));
|
||||
const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ));
|
||||
switch(Type)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork:return "compiled-analytic/TunnelNetwork";
|
||||
case ECaveGeneratorType::FlatPlain:return "compiled-analytic/FlatPlain";
|
||||
case ECaveGeneratorType::CrystalChamber:return "compiled-analytic/CrystalChamber";
|
||||
case ECaveGeneratorType::Maze:return "compiled-analytic/Maze";
|
||||
case ECaveGeneratorType::SurfaceWorld:return "compiled-analytic/SurfaceWorld";
|
||||
case ECaveGeneratorType::VerticalShafts:return "compiled-analytic/VerticalShafts";
|
||||
case ECaveGeneratorType::FloatingIslands:return "compiled-analytic/FloatingIslands";
|
||||
case ECaveGeneratorType::Underwater:return "compiled-analytic/Underwater";
|
||||
}
|
||||
}
|
||||
return "compiled-analytic/fallback";
|
||||
}
|
||||
|
||||
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->DensityAtLattice(X,Y,Z);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "StandaloneSnapshotAdapter.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
#include "WorldAssemblerInternal.h"
|
||||
#include "ResolvedLayoutInternal.h"
|
||||
#include "FloatingIslandsInternal.h"
|
||||
|
||||
#include <shared_mutex>
|
||||
@@ -1457,6 +1458,10 @@ namespace VoxelForgeCore
|
||||
|
||||
float World::densityAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
|
||||
std::string World::densityDispatchPathAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
||||
?impl_->shape->DensityDispatchPathAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z))
|
||||
:"uncompiled"; }
|
||||
float World::unclampedDensityAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAtUnclamped(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
|
||||
DetailFieldSample World::detailSampleAt(Vec3d position) const
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# World Assembler L21 — resolved strate layout
|
||||
|
||||
This round adds a read-only layout snapshot and validator. It does not change density or mesh
|
||||
evaluation.
|
||||
|
||||
## Snapshot and current stack
|
||||
|
||||
World identity is `owner-world/seed/0`; Core voxel size is 0.25 m, owner voxel size is 0.125 m, and
|
||||
all lattice origins are `(0,0,0)`. Intervals below are half-open `[bottom, top)` metres. Stable strate
|
||||
IDs are `owner-world/seed/0/strate/<index>/generator/<kind>`. The shared Core record uses canonical
|
||||
JSON schema v1 and an FNV-1a payload identity.
|
||||
|
||||
The required structure, top to bottom, is:
|
||||
|
||||
```text
|
||||
strate N ceiling band
|
||||
content
|
||||
bedrock band
|
||||
strate N+1 ceiling band
|
||||
content
|
||||
bedrock band
|
||||
```
|
||||
|
||||
Ceiling and bedrock remain distinct bands owned by their own strate. Their nominal edges touch; a
|
||||
separate `EVoxelStrateTransition` declaration describes possible interpenetration.
|
||||
|
||||
| Prototype | Order / stable suffix | Generator; recipe version | Interior/content (m) | Current ceiling | Current bedrock | Transition at lower interface |
|
||||
|---|---|---|---|---|---|---|
|
||||
| SurfaceWorld | 0 / `TunnelNetwork` | TunnelNetwork; `legacy.TunnelNetwork` v0 | `[-120, 8)` | No Core density band. Manager configures a 1 m upper `VF_ApplyBoundarySeal` at `[8,9)`, but the compiled analytic Core path does not apply it. | No Core density band. Manager configures a 1 m lower `VF_ApplyBoundarySeal` at `[-121,-120)`, but the compiled analytic Core path does not apply it. | Gradient, 2 chunks = 8 m by the 4 m manager chunk. Core does not apply it; manager/dispatch chunk widths are 4/8 m. |
|
||||
| SurfaceWorld | 1 / `SurfaceWorld` | SurfaceWorld; `SurfaceWorld.Heightfield2D` v1 | `[-632,-120)` | No separate volumetric band; cap profile is in content, with `AppendStratumBoundaryCeilings` mesh fallback. | None. | Gradient, 1 chunk = 4 m by manager units; no lower strate in this plan. Manager/dispatch widths are 4/8 m. |
|
||||
| FloatingIslands | 0 / `FloatingIslands` | FloatingIslands; `FloatingIslands.ClusterField` geometry v3 | `[-60,4)` | Active one-sided upper seal `[3.5,4)` = 0.5 m. It overlaps the content range by 0.5 m. | None in geometry v3. | Gradient, 2 chunks = 8 m. Core does not apply it; manager/dispatch widths are both 4 m. |
|
||||
| FloatingIslands | 1 / `SurfaceWorld` | SurfaceWorld; `SurfaceWorld.Heightfield2D` v1 | `[-632,-120)` | No separate volumetric band; cap profile is in content, with mesh fallback. | None. | Gradient, 1 chunk = 4 m; no lower strate in this plan. Manager/dispatch widths are 4/8 m. |
|
||||
|
||||
Dispatch provenance is recorded per stratum in the JSON snapshot:
|
||||
|
||||
- SurfaceWorld bounds come from `BuildSurfaceStratumPlan`, which converts each chunk with 32 Core
|
||||
voxels = 8 m. `NativeSurfaceDensityAt` selects with `floor(CoreZ/32)`, then calls
|
||||
`GetGeneratorTypeForChunk` / `GetStrateIndex`. The manager's owner chunk is 4 m.
|
||||
- FI bounds come from `BuildFloatingIslandStratumPlan`, which converts each 32-voxel owner chunk
|
||||
through the 12.5/25 cm owner/Core ratio = 16 Core voxels = 4 m. `NativeFloatingIslandDensityAt`
|
||||
uses the same conversion before the manager lookup.
|
||||
- Legacy TunnelNetwork selection uses the Core 32-voxel chunk lookup. The manager helper assigns
|
||||
the upper strate's transition to its lower interface.
|
||||
|
||||
SurfaceWorld strata carry four cap profiles, 33 route points and their attached passages. The FI
|
||||
strate carries four island anchors and its attached passages. The snapshot preserves these recipe
|
||||
anchors in their source fixed-point units.
|
||||
|
||||
The owner configuration’s non-Hard transitions are currently not honored by Core at these interfaces.
|
||||
`UVoxelStrateManager::GetGenerationParams` contains legacy vector blending, but Core’s compiled
|
||||
dispatcher selects the resolved/native per-strate fields without applying that blend. FI v3 calls
|
||||
`VF_ApplyUpperBoundarySeal` for its ceiling; player edit brushes are applied after the native field,
|
||||
so that seal remains diggable. Bands default to `diggable=true`, `protected=false`; protection is an
|
||||
explicit option.
|
||||
|
||||
```text
|
||||
Current SurfaceWorld plan, top → bottom
|
||||
TunnelNetwork ceiling [8,9) (configured; inactive in Core density)
|
||||
TunnelNetwork content [-120,8)
|
||||
TunnelNetwork bedrock [-121,-120) (configured; inactive in Core)
|
||||
SurfaceWorld ceiling: none
|
||||
SurfaceWorld content [-632,-120)
|
||||
SurfaceWorld bedrock: none
|
||||
|
||||
Current FI prototype, top → bottom
|
||||
FI ceiling [3.5,4) active, inset into FI content by 0.5 m
|
||||
FI content [-60,4)
|
||||
FI bedrock: missing
|
||||
Unassigned interval [-120,-60) = 60 m
|
||||
SurfaceWorld ceiling: none
|
||||
SurfaceWorld content [-632,-120)
|
||||
SurfaceWorld bedrock: none
|
||||
```
|
||||
|
||||
The SurfaceWorld and FI content interiors therefore do not overlap: the FI prototype has a 60 m gap.
|
||||
The owner manager slots meet at −60 m when both use 4 m owner chunks, but SurfaceWorld plan/query
|
||||
dispatch converts chunks at 8 m while FI converts at 4 m. In the gap, Core answers through the
|
||||
compiled analytic FloatingIslands fallback, not either planned recipe; probes at −68 m returned
|
||||
solid density −4.125. SurfaceWorld and TunnelNetwork content interiors touch at −120 m in the
|
||||
SurfaceWorld prototype, but Core does not apply the declared Gradient there.
|
||||
|
||||
## Compatibility validation and boundary probes
|
||||
|
||||
Compatibility mode accepted both layouts with seven reported issues apiece. SurfaceWorld reports
|
||||
unhonored transition, 4/8 m dispatch-unit ambiguity on both strates, and missing current-field
|
||||
ceiling and bedrock bands on both. FI reports its unhonored transition, missing FI bedrock, the
|
||||
0.5 m ceiling/content overlap, the 60 m uncovered interval, and missing SurfaceWorld ceiling and
|
||||
bedrock. These are recorded as compatibility deviations, not world changes.
|
||||
|
||||
Strict mode accepted a valid synthetic two-strate fixture. Its seven negative controls rejected:
|
||||
|
||||
| Control | Strict check that rejects it |
|
||||
|---|---|
|
||||
| Nominal band overlap | Bedrock(N) bottom must equal ceiling(N+1) top. |
|
||||
| Nominal band gap | The same exact nominal contiguity check. |
|
||||
| Missing bedrock | `bedrock.present` is required. |
|
||||
| Missing ceiling | `ceiling.present` is required. |
|
||||
| Wrong band order | Each band kind and the ceiling → content → bedrock edges must match. |
|
||||
| Gap between bedrock(N) and ceiling(N+1) | Their nominal endpoints must be equal. |
|
||||
| Interpenetration wider than transition | Explicit overlap must be at most the mode’s limit; this control uses 4,000,001 µm against a 4,000,000 µm Gradient zone. |
|
||||
|
||||
Hard has a 0 m limit. Gradient uses `TransitionBlendChunks × 4 m`; Interleaved adds its recorded
|
||||
two-manager-chunk warp amplitude to the blend width. The nominal bands still touch exactly; the
|
||||
separate interpenetration field records any permitted physical crossing. Current Core
|
||||
interpenetration is 0 m because it does not apply the declared blends.
|
||||
|
||||
Each plan audited 21 points: one Core voxel (0.25 m) below, at, and above every distinct content,
|
||||
active-band, configured-band, and transition-zone endpoint. The remaining points agreed with the
|
||||
snapshot. Every disagreement was an uncovered snapshot point answered by a named compiled analytic
|
||||
fallback:
|
||||
|
||||
| Prototype | Probe Z (m) | Actual Core dispatch | Density / occupancy |
|
||||
|---|---|---|---|
|
||||
| SurfaceWorld | −632.250 | `compiled-analytic/TunnelNetwork` | `+1`, air |
|
||||
| SurfaceWorld | 8.000, 8.250, 8.750, 9.000, 9.250 | `compiled-analytic/TunnelNetwork` | `+9.648438`, `+10.64844`, `+12.64844`, `+13.64844`, `+14.64844`; all air |
|
||||
| FI | −632.250 | `compiled-analytic/TunnelNetwork` | `+1`, air |
|
||||
| FI | −120.000, −119.750 | `compiled-analytic/FloatingIslands` | `−108.4793`, `−107.4969`; solid |
|
||||
| FI | −68.250, −68.000, −67.750, −60.250 | `compiled-analytic/FloatingIslands` | `−4.125`; solid at each point |
|
||||
| FI | 4.000, 4.250 | `compiled-analytic/FloatingIslands` | `+0.671875`, `+2.253906`; air |
|
||||
|
||||
Probe summaries: SurfaceWorld 6/21 disagreements; FI 9/21. Full per-point records are in
|
||||
`Saved/Linux/linux21/boundary-probes-{surface-world,floating-islands}.log`.
|
||||
|
||||
## Identity, output parity and cost
|
||||
|
||||
Snapshots round-tripped through canonical JSON. At W1/W8/W12 their bytes were identical per recipe:
|
||||
|
||||
| Recipe | Stable identity | Canonical JSON SHA-256 |
|
||||
|---|---|---|
|
||||
| SurfaceWorld | `resolved-layout-v1-fnv64:63cf4d2661149fcd` | `267586b3da9bc5c08c654c097559b205222f53c594c4c4332cb4929fd3d75c05` |
|
||||
| FloatingIslands | `resolved-layout-v1-fnv64:1d90167f37ef9323` | `7bb9e9d745de3a2edf4276f507a5e4c082ff86aa73d9c1ad7e77c1b4d8b8bea8` |
|
||||
|
||||
The 28 hash runs (both recipes, Clang `fp-contract=off` and GCC default, W1/W8/W12 and W12×5)
|
||||
matched all 12 L19 rows per run. The 14 dedicated 24 m crossing runs also matched all 12 L19
|
||||
crossing rows. The table gives the final Clang W12×5 rows, in density/mesh/payload/topology order;
|
||||
each equals its L19 reference.
|
||||
|
||||
| Scope | SurfaceWorld | FloatingIslands |
|
||||
|---|---|---|
|
||||
| Full detail | `0ee0579bd15cbbe4 / 32e5d4964d610fee / 1932b96ee4b1d910 / 69743c6bad3447dc` | `43034299d33f2536 / a41e8353ec4bbe9c / cd0e3e15eef827b0 / 0b3c3f13cc1d6f8a` |
|
||||
| Whole view | `c10697a8f48380c7 / 4197fc1072f0ff58 / 7b3e616ec9459851 / c879e35592bf33e5` | `00bc3607a7839854 / 4baa66127c2b0892 / e11ef05e851a1517 / 1278bc4b1fffb869` |
|
||||
| LOD 0 | `769f2a706d585a72 / 33eeeb39fa443816 / 645621a1d968983f / c2457ed905036c9d` | `ccf97d8e95163a35 / 3333fa0ddebdece2 / d1906bffc2b9d6f2 / a261f3d965e15c12` |
|
||||
| LOD 1 | `69cc5a5c0a2c60af / b48335f5410c57d5 / d15c5480162ab8ba / feca1bd025823604` | `25fa2f0fa7fbb6e6 / c549c519707ac1d7 / 66f797cde4ebdecf / 2383982a12a27ee5` |
|
||||
| LOD 2 | `85deed5683a77202 / 53469895987aeea2 / e55f8ae7e1532033 / d5feb12a0e85c09b` | `c0577decd8a02cab / 3ccf08b7be6edc5b / 0abcbbda5a06c110 / 11c77b71b606fe75` |
|
||||
| LOD 3 | `6518caefb3be7e7f / ec513c90b853a614 / f62fa1137ff17f82 / ccf1dcf681965816` | `386e0c0ef2d7c920 / 1ee2c82d34ad9918 / 000a6514ba018406 / 1b4523b19e90f305` |
|
||||
| LOD 4 | `62d4be96021992f4 / 42706847cb9c744a / eab76f9634e4b9bd / 207ffbb6ef184397` | `e8117a3a803be9f8 / 8a6c36cae1fe0e30 / c839f1b1629eff8c / 0ce9d7ea77b4f62b` |
|
||||
| LOD 5 | `5e4a8c38b8cea6fc / 76adc9ae86988e27 / 938975bf7cc6d94d / 42e135856abdbcac` | `ca6d1df453c6b600 / 02c90d834d9a3fb6 / 4364c37d526f4104 / 1b1cf40d25e50ddf` |
|
||||
| LOD 6 | `e036e95b9006945b / a880cc101a4ed957 / bd82f1aefad6dd89 / 5e9f3408345d95b3` | `6764e347b08603a0 / 46c6ed9a1f029b94 / 5b8dc9d5b7cb0d3d / a10a4a69a93eac4d` |
|
||||
| LOD 7 | `8ee358af56a30d8d / 5cbd8e5965a2cc3f / d4a7fe5d9e4bde30 / 4dd52cd090a9af78` | `c7cd1b483c598595 / a545eb567dc4a0df / b0a1fb225437e05a / 301d1b884c509550` |
|
||||
| LOD 8 | `c3458e1cbdb9c471 / fba3698d21dd1be0 / 77dfb814eac3a24c / c9514f1580c42449` | `7b6e9279b4993109 / cc63440d9867a18d / 12f5cec78b38fa1b / eed919db6db6b2a7` |
|
||||
|
||||
Exact-query rows (density/unclamped, detail, collision): SurfaceWorld `e4d4a8eb95b35933 / e4d4a8eb95b35933`,
|
||||
`efcba20c0c70c415`, `e604823a249029bf`; FloatingIslands `87e5c0dbf9ee9ee2 / 87e5c0dbf9ee9ee2`,
|
||||
`efcba20c0c70c415`, `e604823a249029bf`. The representative 24 m crossing W12×5 whole-view row is the
|
||||
SurfaceWorld row above; all crossing full-detail, LOD and exact-query rows matched the L19 crossing
|
||||
reference.
|
||||
|
||||
Five interleaved hashing-off W12 pairs against L19 measured SurfaceWorld medians 1.510 s old / 1.512 s
|
||||
new (+0.14%) and FI 13.487 s old / 13.381 s new (−0.79%). The opt-in snapshot is constructed once
|
||||
at plan time, outside tile workers; ordinary standalone evaluation does not request it. No hash rows
|
||||
were produced during these pairs.
|
||||
|
||||
Clang and GCC builds passed at 12 jobs. Final build peak process-tree/single-process RSS was
|
||||
2,697,676/450,248 KiB (Clang) and 4,570,704/897,024 KiB (GCC). The final evidence runner's maximum
|
||||
single-process RSS was 583,736 KiB. All runs used CPUs 0–11 and stayed below the per-run limit; no
|
||||
governor or system changes were made.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- An initial snapshot invocation omitted `--stream-benchmark --stream-pipeline` and exited 2; adding
|
||||
the required flags passed.
|
||||
- Early Clang builds caught a `strates` member typo, then an audit initializer/type error. Both were
|
||||
fixed; the final Clang builds passed.
|
||||
- The first complete evidence pass exited 1 at boundary probing because the audit read the host
|
||||
manager after the harness had released it. Dispatch provenance was moved to Core's live selector;
|
||||
the final audits passed.
|
||||
- One GCC build wrapper was interrupted (exit 130) while diagnosing an empty progress log, then
|
||||
rerun successfully. No runtime/hash run timed out or exceeded 15 minutes, and no L19 hash mismatch
|
||||
remained.
|
||||
|
||||
## Owner note
|
||||
|
||||
This records the present strata and confirms the selected ceiling → content → bedrock model. FI still
|
||||
has no floor in v3; SurfaceWorld has no volumetric floor or separate ceiling; the 8 m / 4 m dispatch
|
||||
conversions leave a 60 m FI-prototype gap answered by analytic fallback. Core currently ignores the
|
||||
configured transition blend at these interfaces. Bedrock and ceiling protection remain explicit
|
||||
options with diggable defaults. This round changes no world output.
|
||||
|
||||
The Unreal adapter adds only a separate `resolved_layout` JSON field beside `world_assembly`; that
|
||||
small edit could not be compiled on this Linux machine. No async path, density evaluator, hash input,
|
||||
or recipe version changed. No commit, push, or stash was made.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Merged: read-only snapshot + validator, no world change (28 hash runs and 14 crossing runs equal L19;
|
||||
cost within noise). The Unreal adapter gains a 193-line `resolved_layout` export beside
|
||||
`world_assembly`. It is export-only, but UNCOMPILED here; it must be built on the owner PC.
|
||||
- Key facts for the owner: the authored params put the SW top at −60 m (4 m owner chunks), Core
|
||||
builds SW at −120…−632 m (8 m conversion); the 60 m between FI and SW is unowned and answered by
|
||||
the analytic FloatingIslands fallback as solid. No strate has an active bedrock band; SW has no
|
||||
ceiling band (mesh fallback only); configured Gradient transitions are ignored by Core.
|
||||
- To check next: whether the "ledges" ground seen under the islands is this fallback solid in the
|
||||
unowned gap rather than SW terrain (L19 attributed them to SW at about −60…−73 m).
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Constrained L21 snapshot, output-parity, crossing, and off-cost evidence."""
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/linux21"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
if not os.sched_getaffinity(0).issubset(set(range(12))):
|
||||
raise SystemExit("run under taskset -c 0-11")
|
||||
|
||||
BIN = {
|
||||
"clang-off": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux21-clang",
|
||||
"gcc-default": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux21-gcc",
|
||||
}
|
||||
OLD_CLANG = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux19-clang"
|
||||
OLD_GCC = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux19-gcc"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
HASH_ROW = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
LODS = [(1, 1), (8, 1), (12, 1), (12, 2), (12, 3), (12, 4), (12, 5)]
|
||||
RECIPES = {
|
||||
"surface-world": "0,-35,-487.94,0,-22",
|
||||
"floating-islands": "-22,-19,-22.8,0,-4",
|
||||
}
|
||||
results = []
|
||||
|
||||
|
||||
def launch(name, argv, timeout=900):
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", *map(str, argv)]
|
||||
started = time.monotonic()
|
||||
peak_tree = 0
|
||||
peak_process = 0
|
||||
timed_out = False
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||
start_new_session=True)
|
||||
while process.poll() is None:
|
||||
pending, seen, total = [process.pid], set(), 0
|
||||
while pending:
|
||||
pid = pending.pop()
|
||||
if pid in seen:
|
||||
continue
|
||||
seen.add(pid)
|
||||
try:
|
||||
pending.extend(int(x) for x in Path(f"/proc/{pid}/task/{pid}/children").read_text().split())
|
||||
values = {}
|
||||
for line in Path(f"/proc/{pid}/status").read_text().splitlines():
|
||||
if line.startswith(("VmRSS:", "VmHWM:")):
|
||||
key, amount, *_ = line.split()
|
||||
values[key] = int(amount)
|
||||
total += values.get("VmRSS:", 0)
|
||||
peak_process = max(peak_process, values.get("VmRSS:", 0), values.get("VmHWM:", 0))
|
||||
except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError):
|
||||
pass
|
||||
peak_tree = max(peak_tree, total)
|
||||
if time.monotonic() - started > timeout:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
process.wait()
|
||||
timed_out = True
|
||||
break
|
||||
time.sleep(0.1)
|
||||
code = process.wait()
|
||||
elapsed = time.monotonic() - started
|
||||
rows = HASH_ROW.findall(logfile.read_text(encoding="utf-8", errors="replace"))
|
||||
results.append((name, code, elapsed, peak_process, peak_tree, timed_out, len(rows)))
|
||||
print(f"[L21Run] {name} exit={code} seconds={elapsed:.3f} peak_process_rss_kib={peak_process} "
|
||||
f"peak_tree_rss_kib={peak_tree} hash_rows={len(rows)} timed_out={str(timed_out).lower()}", flush=True)
|
||||
return logfile
|
||||
|
||||
|
||||
def base_args(binary, recipe, workers, hash_output=False, crossing=False):
|
||||
args = [binary, DATA, "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", f"--workers={workers}",
|
||||
f"--camera={RECIPES[recipe]}", f"--recipe={recipe}"]
|
||||
if hash_output:
|
||||
args.append("--stream-hash")
|
||||
if crossing:
|
||||
args += ["--camera-step=24,0,0", "--region-crossing"]
|
||||
return args
|
||||
|
||||
|
||||
# Plan-time identities and canonical serialization at W1/W8/W12, both field recipes.
|
||||
snapshots = {}
|
||||
for recipe in RECIPES:
|
||||
for workers, repeat in [(1, 1), (8, 1), (12, 1)]:
|
||||
path = OUT / f"snapshot-{recipe}-w{workers}-r{repeat}.json"
|
||||
args = [BIN["clang-off"], DATA, "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", f"--recipe={recipe}", f"--workers={workers}",
|
||||
f"--resolved-layout-output={path}", "--resolved-layout-only"]
|
||||
log = launch(f"snapshot-{recipe}-w{workers}-r{repeat}", args)
|
||||
if results[-1][1] != 0:
|
||||
raise SystemExit(f"snapshot plan failed: {log}")
|
||||
snapshots[(recipe, workers)] = path.read_bytes()
|
||||
if len({snapshots[(recipe, workers)] for workers in (1, 8, 12)}) != 1:
|
||||
raise SystemExit(f"snapshot serialization changed with worker count: {recipe}")
|
||||
print(f"[L21SnapshotIdentity] recipe={recipe} worker_counts=1,8,12 canonical_bytes=identical status=PASS", flush=True)
|
||||
|
||||
# Full matrix. Every row is compared against the L19 W12x5 reference.
|
||||
for recipe in RECIPES:
|
||||
for compiler, binary in BIN.items():
|
||||
for workers, repeat in LODS:
|
||||
name = f"matrix-{recipe}-{compiler}-w{workers}-r{repeat}"
|
||||
log = launch(name, base_args(binary, recipe, workers, hash_output=True))
|
||||
rows = HASH_ROW.findall(log.read_text(encoding="utf-8", errors="replace"))
|
||||
reference_name = f"{recipe}-{compiler}-w12-r5.log"
|
||||
reference = ROOT / "Saved/Linux/l19-matrix" / reference_name
|
||||
expected = HASH_ROW.findall(reference.read_text(encoding="utf-8", errors="replace"))
|
||||
if results[-1][1] != 0 or len(rows) != 12 or rows != expected:
|
||||
print(f"[L21HashMismatch] {name} expected_rows={len(expected)} actual_rows={len(rows)}", flush=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
# The crossing matrix retains the 24 m two-view query proof on both compilers.
|
||||
for compiler, binary in BIN.items():
|
||||
for workers, repeat in LODS:
|
||||
name = f"crossing-{compiler}-w{workers}-r{repeat}"
|
||||
log = launch(name, base_args(binary, "surface-world", workers, hash_output=True, crossing=True))
|
||||
rows = HASH_ROW.findall(log.read_text(encoding="utf-8", errors="replace"))
|
||||
reference = ROOT / "Saved/Linux/l19-crossing" / f"{compiler}-w12-r5.log"
|
||||
expected = HASH_ROW.findall(reference.read_text(encoding="utf-8", errors="replace"))
|
||||
if results[-1][1] != 0 or len(rows) != 12 or rows != expected:
|
||||
print(f"[L21CrossingHashMismatch] {name} expected_rows={len(expected)} actual_rows={len(rows)}", flush=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
# Five interleaved W12 pairs per recipe, hashing disabled. Alternate order by pair to reduce drift.
|
||||
off_pairs = {recipe: {"old": [], "new": []} for recipe in RECIPES}
|
||||
for recipe in RECIPES:
|
||||
for pair in range(1, 6):
|
||||
order = [("old", OLD_CLANG), ("new", BIN["clang-off"])] if pair % 2 else [("new", BIN["clang-off"]), ("old", OLD_CLANG)]
|
||||
for label, binary in order:
|
||||
name = f"offcost-{recipe}-pair{pair}-{label}"
|
||||
log = launch(name, base_args(binary, recipe, 12, hash_output=False), timeout=900)
|
||||
if results[-1][1] != 0:
|
||||
raise SystemExit(f"hashing-off cost run failed: {log}")
|
||||
off_pairs[recipe][label].append(results[-1][2])
|
||||
print(f"[L21OffCostPair] recipe={recipe} pair={pair} old_seconds={off_pairs[recipe]['old'][-1]:.3f} "
|
||||
f"new_seconds={off_pairs[recipe]['new'][-1]:.3f}", flush=True)
|
||||
for recipe, pair in off_pairs.items():
|
||||
old = statistics.median(pair["old"])
|
||||
new = statistics.median(pair["new"])
|
||||
print(f"[L21OffCostSummary] recipe={recipe} pairs=5 median_old_seconds={old:.3f} "
|
||||
f"median_new_seconds={new:.3f} delta_percent={(new-old)/old*100.0:.2f}", flush=True)
|
||||
|
||||
# Query the real compiled Core field at every distinct recorded stratum/band endpoint.
|
||||
for recipe in RECIPES:
|
||||
name = f"boundary-probes-{recipe}"
|
||||
args = base_args(BIN["clang-off"], recipe, 12, hash_output=False)
|
||||
args += ["--audit-resolved-layout-boundaries"]
|
||||
log = launch(name, args)
|
||||
if results[-1][1] != 0:
|
||||
raise SystemExit(f"Core boundary probes failed: {log}")
|
||||
|
||||
print(f"[L21EvidenceSummary] snapshots=6 matrix=28 crossing=14 off_cost_pairs=10 boundary_audits=2 status=PASS", flush=True)
|
||||
for name, code, elapsed, rss, tree, timed_out, rows in results:
|
||||
print(f"[L21RunDetail] {name} exit={code} seconds={elapsed:.3f} peak_process_rss_kib={rss} "
|
||||
f"peak_tree_rss_kib={tree} hash_rows={rows} timed_out={str(timed_out).lower()}", flush=True)
|
||||
@@ -63,6 +63,9 @@ namespace
|
||||
float CameraYawDegrees=0.0f;
|
||||
float CameraPitchDegrees=0.0f;
|
||||
std::filesystem::path MeshExportPath;
|
||||
std::filesystem::path ResolvedLayoutOutputPath;
|
||||
bool ResolvedLayoutOnly=false;
|
||||
bool AuditResolvedLayoutBoundaries=false;
|
||||
};
|
||||
|
||||
bool ParseInteger(std::string_view text,int& output)
|
||||
@@ -117,6 +120,9 @@ namespace
|
||||
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
|
||||
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
||||
" --seed=N --recipe=surface-world|floating-islands\n"
|
||||
" --resolved-layout-output=FILE (canonical plan-time layout snapshot)\n"
|
||||
" --resolved-layout-only (exit after plan and snapshot validation)\n"
|
||||
" --audit-resolved-layout-boundaries (query Core at each recorded endpoint)\n"
|
||||
" --auto-surface-camera (place the 27-tile cube on this seed's surface)\n"
|
||||
" --mesh-coverage (report non-empty tiles and indexed triangles)\n"
|
||||
" --screen-space-gate (measure display error and historical negative controls)\n"
|
||||
@@ -134,6 +140,8 @@ namespace
|
||||
return false;
|
||||
}
|
||||
if(arg=="--world-assembler-prototype")options.WorldAssemblerPrototype=true;
|
||||
else if(arg=="--resolved-layout-only")options.ResolvedLayoutOnly=true;
|
||||
else if(arg=="--audit-resolved-layout-boundaries")options.AuditResolvedLayoutBoundaries=true;
|
||||
else if(arg=="--stream-benchmark")options.StreamBenchmark=true;
|
||||
else if(arg=="--stream-pipeline")options.StreamPipeline=true;
|
||||
else if(arg=="--stream-hash")options.StreamHash=true;
|
||||
@@ -155,6 +163,8 @@ namespace
|
||||
else if(recipe=="floating-islands")options.FloatingIslandsRecipe=true;
|
||||
else {std::fprintf(stderr,"Invalid --recipe value: %.*s\n",static_cast<int>(recipe.size()),recipe.data());return false;}
|
||||
}
|
||||
else if(arg.starts_with("--resolved-layout-output="))
|
||||
options.ResolvedLayoutOutputPath=std::string(arg.substr(std::string_view("--resolved-layout-output=").size()));
|
||||
else if(arg.starts_with("--seed="))
|
||||
{if(!readInt("--seed=",options.SeedOverride))return false;options.HasSeedOverride=true;}
|
||||
else if(arg.starts_with("--render-distance-chunks="))
|
||||
@@ -314,6 +324,52 @@ int main(int argc,char** argv)
|
||||
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||
*ownerWorld,ownerJsonRoot,worldAssembly,error))
|
||||
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
||||
VoxelForgeCore::ResolvedLayoutSnapshotV1 resolvedLayout;
|
||||
const bool buildResolvedLayout=!options.ResolvedLayoutOutputPath.empty()
|
||||
||options.AuditResolvedLayoutBoundaries||options.ResolvedLayoutOnly;
|
||||
if(buildResolvedLayout)
|
||||
{
|
||||
const std::string_view variant=options.FloatingIslandsRecipe?"floating-islands":"surface-world";
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
||||
*ownerWorld,worldAssembly,variant,resolvedLayout,error))
|
||||
{std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;}
|
||||
const std::string layoutJson=resolvedLayout.ToJson();
|
||||
if(!options.ResolvedLayoutOutputPath.empty())
|
||||
{
|
||||
const auto roundTrip=VoxelForgeCore::ResolvedLayoutSnapshotV1::FromJson(layoutJson,error);
|
||||
if(!roundTrip||roundTrip->ToJson()!=layoutJson||roundTrip->stableIdentity!=resolvedLayout.stableIdentity)
|
||||
{if(error.empty())error="resolved-layout canonical serialization did not round-trip";
|
||||
std::fprintf(stderr,"Resolved-layout round trip failed: %s\n",error.c_str());return 2;}
|
||||
if(!options.ResolvedLayoutOutputPath.parent_path().empty())
|
||||
std::filesystem::create_directories(options.ResolvedLayoutOutputPath.parent_path());
|
||||
std::ofstream output(options.ResolvedLayoutOutputPath,std::ios::binary);
|
||||
if(!output){std::fprintf(stderr,"Could not write resolved-layout snapshot: %s\n",options.ResolvedLayoutOutputPath.c_str());return 2;}
|
||||
output.write(layoutJson.data(),static_cast<std::streamsize>(layoutJson.size()));
|
||||
if(!output){std::fprintf(stderr,"Resolved-layout snapshot write failed: %s\n",options.ResolvedLayoutOutputPath.c_str());return 2;}
|
||||
}
|
||||
const auto controls=VoxelForgeCore::AuditResolvedLayoutValidatorV1();
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutIssueV1> compatibilityIssues;
|
||||
std::string compatibilityError;
|
||||
const bool compatibilityAccepted=resolvedLayout.validate(
|
||||
VoxelForgeCore::ResolvedLayoutValidationModeV1::compatibility,
|
||||
compatibilityIssues,compatibilityError);
|
||||
std::printf("[ResolvedLayoutSnapshot] schema=%u stable_identity=%s strata=%zu bytes=%zu round_trip=PASS\n",
|
||||
resolvedLayout.schemaVersion,resolvedLayout.stableIdentity.c_str(),resolvedLayout.strates.size(),layoutJson.size());
|
||||
std::printf("[ResolvedLayoutCompatibility] accepted=%s issues=%zu error=%s\n",
|
||||
compatibilityAccepted?"true":"false",compatibilityIssues.size(),compatibilityError.c_str());
|
||||
for(const auto& issue:compatibilityIssues)
|
||||
std::printf("[ResolvedLayoutCompatibilityIssue] code=%s detail=%s\n",
|
||||
issue.code.c_str(),issue.detail.c_str());
|
||||
if(!compatibilityAccepted)
|
||||
{std::fprintf(stderr,"Resolved-layout compatibility validation failed: %s\n",compatibilityError.c_str());return 2;}
|
||||
std::printf("[ResolvedLayoutValidatorControls] rejected=%u/%u status=%s\n",
|
||||
controls.controlsRejectedAsExpected,controls.controlsRun,controls.passed?"PASS":"FAIL");
|
||||
if(!controls.passed)
|
||||
{for(const auto& failure:controls.failures)std::printf("[ResolvedLayoutValidatorControlFailure] %s %s\n",failure.code.c_str(),failure.detail.c_str());return 2;}
|
||||
if(!options.ResolvedLayoutOutputPath.empty())
|
||||
std::printf("[ResolvedLayoutSnapshotOutput] path=%s\n",options.ResolvedLayoutOutputPath.c_str());
|
||||
if(options.ResolvedLayoutOnly)return 0;
|
||||
}
|
||||
if(options.AutoSurfaceCamera)
|
||||
{
|
||||
constexpr std::int64_t UnitsPerVoxel=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
@@ -377,5 +433,6 @@ int main(int argc,char** argv)
|
||||
cameraStepVoxels,options.AuditUniformBlockProofs,options.ProfileLodWork,
|
||||
options.RegionCrossing,options.AuditTileShapeReadiness,
|
||||
options.DiagnosticSlowShapeCompileMs,options.AuditBasePageEquivalence,
|
||||
options.AuditRouteParity);
|
||||
options.AuditRouteParity,options.AuditResolvedLayoutBoundaries,
|
||||
buildResolvedLayout?&resolvedLayout:nullptr);
|
||||
}
|
||||
@@ -253,7 +253,9 @@ namespace StandaloneStreamBenchmark
|
||||
bool bAuditUniformBlockProofs=false,bool bReportLodWorkProfile=false,
|
||||
bool bRegionCrossing=false,
|
||||
bool bAuditTileShapeReadiness=false,int DiagnosticSlowShapeCompileMs=0,
|
||||
bool bAuditBasePageEquivalence=false,bool bAuditRouteParity=false)
|
||||
bool bAuditBasePageEquivalence=false,bool bAuditRouteParity=false,
|
||||
bool bAuditResolvedLayoutBoundaries=false,
|
||||
const VoxelForgeCore::ResolvedLayoutSnapshotV1* ResolvedLayout=nullptr)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
FStandaloneOwnerWorld& World=*OwnerWorld;
|
||||
@@ -438,6 +440,89 @@ namespace StandaloneStreamBenchmark
|
||||
Streamer.Stop();
|
||||
std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;
|
||||
}
|
||||
if(bAuditResolvedLayoutBoundaries)
|
||||
{
|
||||
if(ResolvedLayout==nullptr)
|
||||
{Streamer.Stop();std::fprintf(stderr,"[ResolvedLayoutProbe] missing plan-time snapshot\n");return 2;}
|
||||
std::vector<std::int64_t> endpoints;
|
||||
for(const auto& Stratum:ResolvedLayout->strates)
|
||||
{
|
||||
endpoints.push_back(Stratum.interiorBottomMicrometres);
|
||||
endpoints.push_back(Stratum.interiorTopMicrometres);
|
||||
endpoints.push_back(Stratum.ceiling.bottomMicrometres);
|
||||
endpoints.push_back(Stratum.ceiling.topMicrometres);
|
||||
endpoints.push_back(Stratum.bedrock.bottomMicrometres);
|
||||
endpoints.push_back(Stratum.bedrock.topMicrometres);
|
||||
}
|
||||
for(std::size_t StratumIndex=0;StratumIndex+1<ResolvedLayout->strates.size();++StratumIndex)
|
||||
{
|
||||
const auto& Upper=ResolvedLayout->strates[StratumIndex];
|
||||
if(Upper.transitionMode=="Hard"||Upper.transitionWidthMicrometres<=0)continue;
|
||||
endpoints.push_back(Upper.interiorBottomMicrometres-Upper.transitionWidthMicrometres);
|
||||
endpoints.push_back(Upper.interiorBottomMicrometres+Upper.transitionWidthMicrometres);
|
||||
}
|
||||
std::sort(endpoints.begin(),endpoints.end());
|
||||
endpoints.erase(std::unique(endpoints.begin(),endpoints.end()),endpoints.end());
|
||||
std::uint64_t probes=0,disagreements=0;
|
||||
const double CoreVoxelMicrometres=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
||||
const auto expectedAt=[&](std::int64_t zMicrometres)
|
||||
{
|
||||
std::vector<std::string> candidates;
|
||||
for(const auto& Stratum:ResolvedLayout->strates)
|
||||
{
|
||||
const auto contains=[zMicrometres](const VoxelForgeCore::ResolvedLayoutBandV1& Band)
|
||||
{return Band.presentInCurrentField&&zMicrometres>=Band.bottomMicrometres
|
||||
&&zMicrometres<Band.topMicrometres;};
|
||||
if(contains(Stratum.ceiling))candidates.push_back(Stratum.generatorKind+"/ceiling");
|
||||
if(contains(Stratum.content))candidates.push_back(Stratum.generatorKind+"/content");
|
||||
if(contains(Stratum.bedrock))candidates.push_back(Stratum.generatorKind+"/bedrock");
|
||||
}
|
||||
return candidates;
|
||||
};
|
||||
const std::array<std::int64_t,3> offsets{{
|
||||
-static_cast<std::int64_t>(World.CoreVoxelSizeMicrons),0,
|
||||
static_cast<std::int64_t>(World.CoreVoxelSizeMicrons)}};
|
||||
for(const std::int64_t Endpoint:endpoints)
|
||||
for(const std::int64_t Offset:offsets)
|
||||
{
|
||||
const std::int64_t zMicrometres=Endpoint+Offset;
|
||||
const double zVoxel=static_cast<double>(zMicrometres)/CoreVoxelMicrometres;
|
||||
const VoxelForgeCore::Vec3d position{0.0,0.0,zVoxel};
|
||||
const std::string actual=Shape.densityDispatchPathAt(position);
|
||||
const auto expected=expectedAt(zMicrometres);
|
||||
std::string expectedLabel;
|
||||
std::vector<std::string> expectedGenerators;
|
||||
for(const auto& Candidate:expected)
|
||||
{
|
||||
if(!expectedLabel.empty())expectedLabel+=',';
|
||||
expectedLabel+=Candidate;
|
||||
const auto Separator=Candidate.find('/');
|
||||
const std::string Kind=Candidate.substr(0,Separator);
|
||||
if(std::find(expectedGenerators.begin(),expectedGenerators.end(),Kind)==expectedGenerators.end())
|
||||
expectedGenerators.push_back(Kind);
|
||||
}
|
||||
if(expected.empty())expectedLabel="uncovered";
|
||||
std::string actualGenerator;
|
||||
if(actual=="SurfaceWorld-native"||actual=="SurfaceWorld-legacy")actualGenerator="SurfaceWorld";
|
||||
else if(actual=="FloatingIslands-native")actualGenerator="FloatingIslands";
|
||||
else if(actual.rfind("compiled-analytic/",0)==0)actualGenerator=actual.substr(18);
|
||||
const bool mismatch=expected.empty()
|
||||
?actual!="compiled-analytic/fallback"
|
||||
:std::find(expectedGenerators.begin(),expectedGenerators.end(),actualGenerator)==expectedGenerators.end();
|
||||
const float density=Shape.densityAt(position);
|
||||
const bool solid=density<0.0f;
|
||||
++probes;disagreements+=mismatch?1u:0u;
|
||||
std::printf("[ResolvedLayoutProbe] endpoint_um=%lld offset_um=%lld z_m=%.3f expected=%s actual=%s density=%.7g occupancy=%s disagreement=%s\n",
|
||||
static_cast<long long>(Endpoint),static_cast<long long>(Offset),
|
||||
static_cast<double>(zMicrometres)/1000000.0,expectedLabel.c_str(),actual.c_str(),density,
|
||||
solid?"solid":"air",mismatch?"YES":"no");
|
||||
}
|
||||
std::printf("[ResolvedLayoutProbeSummary] probes=%llu disagreements=%llu status=%s\n",
|
||||
static_cast<unsigned long long>(probes),static_cast<unsigned long long>(disagreements),
|
||||
disagreements==0?"PASS":"DEVIATIONS_RECORDED");
|
||||
Streamer.Stop();
|
||||
return 0;
|
||||
}
|
||||
const auto ShapeReady=Clock::now();
|
||||
const double ShapeSeconds=std::chrono::duration<double>(ShapeReady-ShapeBuildStart).count();
|
||||
if(bAuditTileShapeReadiness)
|
||||
|
||||
@@ -525,4 +525,161 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
return true;
|
||||
}
|
||||
|
||||
inline const char* GeneratorKind(ECaveGeneratorType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork:return "TunnelNetwork";
|
||||
case ECaveGeneratorType::FlatPlain:return "FlatPlain";
|
||||
case ECaveGeneratorType::CrystalChamber:return "CrystalChamber";
|
||||
case ECaveGeneratorType::Maze:return "Maze";
|
||||
case ECaveGeneratorType::SurfaceWorld:return "SurfaceWorld";
|
||||
case ECaveGeneratorType::VerticalShafts:return "VerticalShafts";
|
||||
case ECaveGeneratorType::FloatingIslands:return "FloatingIslands";
|
||||
case ECaveGeneratorType::Underwater:return "Underwater";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
inline const char* TransitionKind(EVoxelStrateTransition type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case EVoxelStrateTransition::Gradient:return "Gradient";
|
||||
case EVoxelStrateTransition::Hard:return "Hard";
|
||||
case EVoxelStrateTransition::Interleaved:return "Interleaved";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
inline bool BuildResolvedLayoutSnapshot(const FStandaloneOwnerWorld& owner,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& assembly,std::string_view recipeVariant,
|
||||
VoxelForgeCore::ResolvedLayoutSnapshotV1& out,std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
if(!owner.Manager){error="resolved layout builder requires the initialized director";return false;}
|
||||
(void)recipeVariant;
|
||||
out={};out.worldIdentity="owner-world/seed/"+std::to_string(owner.Seed);
|
||||
out.ownerVoxelSizeMicrometres=owner.OwnerVoxelSizeMicrons;
|
||||
out.coreVoxelSizeMicrometres=owner.CoreVoxelSizeMicrons;
|
||||
const std::int64_t coreMicrons=std::max<std::uint32_t>(1,owner.CoreVoxelSizeMicrons);
|
||||
const std::int64_t managerChunkMicrons=CHUNK_SIZE*static_cast<std::int64_t>(owner.OwnerVoxelSizeMicrons);
|
||||
const auto fixedToMicrons=[&](std::int64_t fixed)
|
||||
{return static_cast<std::int64_t>(std::llround(static_cast<double>(fixed)
|
||||
*static_cast<double>(coreMicrons)/(WorldAssemblySubvoxelsPerVoxel)));};
|
||||
for(const FStrateSlot& slot:owner.Manager->GetLayout())
|
||||
{
|
||||
if(!slot.Definition)continue;
|
||||
const auto type=owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ));
|
||||
ResolvedLayoutStratumV1 s;s.strateIndex=slot.StrateIndex;s.generatorKind=GeneratorKind(type);
|
||||
s.stableId=out.worldIdentity+"/strate/"+std::to_string(slot.StrateIndex)
|
||||
+"/generator/"+GeneratorKind(type);
|
||||
s.recipeId=type==ECaveGeneratorType::SurfaceWorld?"SurfaceWorld.Heightfield2D"
|
||||
:type==ECaveGeneratorType::FloatingIslands?"FloatingIslands.ClusterField":"legacy."+std::string(GeneratorKind(type));
|
||||
s.recipeVersion=type==ECaveGeneratorType::SurfaceWorld?assembly.recipe.schemaVersion
|
||||
:type==ECaveGeneratorType::FloatingIslands&&!assembly.floatingIslands.empty()
|
||||
?assembly.floatingIslands.front().manifest.recipe.geometryVersion:0;
|
||||
s.voxelSizeMicrometres=owner.CoreVoxelSizeMicrons;
|
||||
s.latticeOriginMicrometres={0,0,0};
|
||||
std::int64_t bottom=static_cast<std::int64_t>(slot.BottomChunkZ)*CHUNK_SIZE*coreMicrons;
|
||||
std::int64_t top=static_cast<std::int64_t>(slot.TopChunkZ+1)*CHUNK_SIZE*coreMicrons;
|
||||
if(type==ECaveGeneratorType::FloatingIslands)
|
||||
{
|
||||
const auto it=std::find_if(assembly.floatingIslands.begin(),assembly.floatingIslands.end(),
|
||||
[&](const auto& p){return p.strateIndex==slot.StrateIndex;});
|
||||
if(it!=assembly.floatingIslands.end())
|
||||
{bottom=fixedToMicrons(it->manifest.recipe.layoutMinimumZSubvoxels);
|
||||
top=fixedToMicrons(it->manifest.recipe.layoutMaximumZSubvoxels);}
|
||||
}
|
||||
s.interiorBottomMicrometres=bottom;s.interiorTopMicrometres=top;
|
||||
s.content={"content",bottom,top,true,true,false,"current generator interior","none","none"};
|
||||
s.transitionMode=TransitionKind(slot.Definition->TransitionType);
|
||||
s.transitionBlendChunks=slot.Definition->TransitionBlendChunks;
|
||||
s.transitionWidthMicrometres=static_cast<std::int64_t>(s.transitionBlendChunks)*managerChunkMicrons;
|
||||
s.transitionWarpAmplitudeMicrometres=s.transitionMode=="Interleaved"
|
||||
?2*managerChunkMicrons:0;
|
||||
s.transitionInterpenetrationMicrometres=0;
|
||||
s.transitionHonouredInCore=false;
|
||||
s.transitionDispatchPath="upper strate owns lower interface; UVoxelStrateManager::GetGenerationParams can blend legacy vectors, but Core's compiled field selects per-strate/native recipe fields without transition blending";
|
||||
s.managerLookup="UVoxelStrateManager::GetGeneratorTypeForChunk; upper strate TransitionType owns lower interface";
|
||||
s.managerChunkWidthMicrometres=managerChunkMicrons;
|
||||
s.dispatchChunkWidthMicrometres=type==ECaveGeneratorType::FloatingIslands
|
||||
?static_cast<std::int64_t>(CHUNK_SIZE)*owner.OwnerVoxelSizeMicrons
|
||||
:static_cast<std::int64_t>(CHUNK_SIZE)*coreMicrons;
|
||||
if(type==ECaveGeneratorType::SurfaceWorld)
|
||||
{s.boundDispatchPath="BuildSurfaceStratumPlan: (chunkZ*CHUNK_SIZE) Core voxels; 32 Core voxels per chunk";
|
||||
s.queryDispatchPath="FStandaloneShapeWorld::NativeSurfaceDensityAt: floor(Z/CHUNK_SIZE), manager lookup; 32 Core voxels per chunk";}
|
||||
else if(type==ECaveGeneratorType::FloatingIslands)
|
||||
{s.boundDispatchPath="BuildFloatingIslandStratumPlan: owner chunk * 32 * owner/Core voxel ratio";
|
||||
s.queryDispatchPath="FStandaloneShapeWorld::NativeFloatingIslandDensityAt: floor(Z/(32*owner/Core ratio)), manager lookup";}
|
||||
else
|
||||
{s.boundDispatchPath="legacy strata slot bounds scaled by Core chunk width";
|
||||
s.queryDispatchPath="legacy Core field; manager lookup uses floor(Z/CHUNK_SIZE)";}
|
||||
const std::int64_t legacySealMicrons=static_cast<std::int64_t>(std::llround(
|
||||
std::max(0.0f,slot.Definition->GenerationParams.BoundarySealThickness)
|
||||
*owner.OwnerVoxelSizeMicrons));
|
||||
if(type==ECaveGeneratorType::FloatingIslands)
|
||||
{
|
||||
const auto it=std::find_if(assembly.floatingIslands.begin(),assembly.floatingIslands.end(),
|
||||
[&](const auto& p){return p.strateIndex==slot.StrateIndex;});
|
||||
const std::int64_t seal=it==assembly.floatingIslands.end()?0:fixedToMicrons(
|
||||
it->manifest.recipe.boundarySealSubvoxels);
|
||||
s.ceiling={"ceiling",top-seal,top,seal>0,true,false,
|
||||
"v3 shared upper boundary density seal; one sided; inset from upper edge","upper/interior side","VF_ApplyUpperBoundarySeal"};
|
||||
s.bedrock={"bedrock",bottom,bottom,false,true,false,
|
||||
"no lower boundary seal in geometryVersion 3","none","none"};
|
||||
if(it!=assembly.floatingIslands.end())
|
||||
{
|
||||
for(std::size_t n=0;n<it->manifest.plan.islands.size();++n)
|
||||
{const auto& a=it->manifest.plan.islands[n];ResolvedLayoutAnchorV1 anchor;
|
||||
anchor.kind="island_anchor";anchor.stableId="island/"+std::to_string(n);anchor.units="1/256 Core voxel";
|
||||
anchor.values={a.centerSubvoxels[0],a.centerSubvoxels[1],a.centerSubvoxels[2],a.radiusSubvoxels,a.topHalfSubvoxels};
|
||||
s.anchors.push_back(std::move(anchor));}
|
||||
}
|
||||
}
|
||||
else if(type==ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
s.ceiling={"ceiling",top,top,false,true,false,
|
||||
"no separate volumetric ceiling band; cap profile is internal to content","none",
|
||||
"Heightfield2D cap profile + AppendStratumBoundaryCeilings fallback mesh"};
|
||||
s.bedrock={"bedrock",bottom,bottom,false,true,false,"no lower bedrock band","none","none"};
|
||||
for(const auto& p:assembly.recipe.biomes)
|
||||
if(p.hasCapProfile)
|
||||
{ResolvedLayoutAnchorV1 a;a.kind="cap_profile";a.stableId="biome/"+std::to_string(p.stableId);
|
||||
a.units="1/256 Core voxel";a.values={p.capHeightSubvoxels,p.capUndulationSubvoxels,
|
||||
p.capWavelengthSubvoxels,p.capDetailAmplitudeSubvoxels,p.capRidgeAmplitudeSubvoxels};s.anchors.push_back(std::move(a));}
|
||||
const auto plan=std::find_if(assembly.plan.strates.begin(),assembly.plan.strates.end(),
|
||||
[&](const auto& p){return p.strateIndex==slot.StrateIndex;});
|
||||
if(plan!=assembly.plan.strates.end())
|
||||
{
|
||||
for(std::size_t n=0;n<plan->route.size();++n)
|
||||
{const auto& p=plan->route[n];ResolvedLayoutAnchorV1 a;a.kind="route_point";a.stableId="route/"+std::to_string(n);
|
||||
a.units="1/256 Core voxel";a.values={p.positionSubvoxels[0],p.positionSubvoxels[1],p.positionSubvoxels[2],p.radiusSubvoxels};
|
||||
s.anchors.push_back(std::move(a));}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
s.ceiling={"ceiling",top,top+legacySealMicrons,false,true,false,
|
||||
"manager configures a two-sided legacy seal, but Core's compiled analytic field does not apply this density band",
|
||||
"configured upper side","legacy VF_ApplyBoundarySeal; absent from current Core density path"};
|
||||
s.bedrock={"bedrock",bottom-legacySealMicrons,bottom,false,true,false,
|
||||
"manager configures a two-sided legacy seal, but Core's compiled analytic field does not apply this density band; breakable by default",
|
||||
"configured lower side","legacy VF_ApplyBoundarySeal; absent from current Core density path"};
|
||||
}
|
||||
for(std::size_t n=0;n<owner.Manager->GetPassages().Num();++n)
|
||||
{
|
||||
const auto& passage=owner.Manager->GetPassages()[static_cast<int32>(n)];
|
||||
if(passage.UpperStrateIndex!=slot.StrateIndex&&passage.LowerStrateIndex!=slot.StrateIndex)continue;
|
||||
ResolvedLayoutAnchorV1 a;a.kind="passage";a.stableId="passage/"+std::to_string(n);
|
||||
a.units="milli owner voxels";a.values={std::llround(passage.UpperPoint.X*1000.0),
|
||||
std::llround(passage.UpperPoint.Y*1000.0),std::llround(passage.UpperPoint.Z*1000.0),
|
||||
std::llround(passage.LowerPoint.X*1000.0),std::llround(passage.LowerPoint.Y*1000.0),
|
||||
std::llround(passage.LowerPoint.Z*1000.0)};s.anchors.push_back(std::move(a));
|
||||
}
|
||||
out.strates.push_back(std::move(s));
|
||||
}
|
||||
return out.finalize(error);
|
||||
}
|
||||
|
||||
} // namespace StandaloneWorldAssemblyPrototype
|
||||
Reference in new issue
Block a user