From 8753f6dbe45cad5348d0a2aebbe2d6b8f22238e4 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Tue, 20 Oct 2026 10:22:00 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX --- ARCHITECTURE.md | 30 ++ CODEMAP.md | 3 +- .../Private/VoxelForgeCoreAdapter.cpp | 193 ++++++++++ .../include/VoxelForgeCore/VoxelForgeCore.h | 92 +++++ Standalone/Core/src/ResolvedLayoutInternal.h | 330 ++++++++++++++++++ Standalone/Core/src/ShapeWorldInternal.h | 26 ++ Standalone/Core/src/VoxelForgeCore.cpp | 5 + .../WorldAssemblerM3/Linux21/REPORT.md | 203 +++++++++++ .../Linux21/RunEvidenceL21.py | 165 +++++++++ Standalone/Source/StreamBenchmarkHeadless.cpp | 59 +++- Standalone/Source/StreamBenchmarkPipeline.h | 87 ++++- Standalone/Source/WorldAssemblyPrototype.h | 157 +++++++++ 12 files changed, 1347 insertions(+), 3 deletions(-) create mode 100644 Standalone/Core/src/ResolvedLayoutInternal.h create mode 100644 Standalone/Research/WorldAssemblerM3/Linux21/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux21/RunEvidenceL21.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8fd0fb1..f940416 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/CODEMAP.md b/CODEMAP.md index 433dfb3..32491d0 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -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. | diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp index 3a9064e..9e4fe3d 100644 --- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp @@ -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::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::llround(static_cast(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(Slot.BottomChunkZ) + * CHUNK_SIZE * ExpectedCoreVoxelMicrons; + Item.interiorTopMicrometres = static_cast(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(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::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(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::llround(Passage.UpperPoint.X * 1000.0)), + static_cast(std::llround(Passage.UpperPoint.Y * 1000.0)), + static_cast(std::llround(Passage.UpperPoint.Z * 1000.0)), + static_cast(std::llround(Passage.LowerPoint.X * 1000.0)), + static_cast(std::llround(Passage.LowerPoint.Y * 1000.0)), + static_cast(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 LayoutObject; + const TSharedRef> 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()) { diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 13b38d4..e591094 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -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 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 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 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 strates; + std::string stableIdentity; + + bool finalize(std::string& error); + bool validate(ResolvedLayoutValidationModeV1 mode, + std::vector& issues, + std::string& error) const; + std::string ToJson() const; + static std::optional 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 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; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h new file mode 100644 index 0000000..dde57b6 --- /dev/null +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -0,0 +1,330 @@ +#pragma once + +#include "Json.h" +#include "VoxelForgeCore/VoxelForgeCore.h" + +#include +#include +#include + +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(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<b.interiorTopMicrometres:a.order(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 issues; + return validate(ResolvedLayoutValidationModeV1::compatibility,issues,error); +} + +bool ResolvedLayoutSnapshotV1::validate(ResolvedLayoutValidationModeV1 mode, + std::vector& 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;ieffectiveTransitionWidth + ||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(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+1strates[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.bottomMicrometress.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.topMicrometresb.interiorTopMicrometres) + add("uncovered_interval",a.stableId+" to "+b.stableId+" leaves "+std::to_string( + a.interiorBottomMicrometres-b.interiorTopMicrometres)+" um uncovered"); + else if(a.interiorBottomMicrometreseffectiveTransitionWidth) + 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.bottomMicrometreseffectiveTransitionWidth) + 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::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(LayoutInt(root,"schema_version")); + s.worldIdentity=LayoutString(root,"world_identity"); + s.ownerVoxelSizeMicrometres=static_cast(LayoutInt(root,"owner_voxel_size_um")); + s.coreVoxelSizeMicrometres=static_cast(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(LayoutInt(v,"order"));x.strateIndex=static_cast(LayoutInt(v,"strate_index")); + x.generatorKind=LayoutString(v,"generator_kind");x.recipeId=LayoutString(v,"recipe_id"); + x.recipeVersion=static_cast(LayoutInt(v,"recipe_version")); + x.interiorBottomMicrometres=LayoutInt(v,"interior_bottom_um");x.interiorTopMicrometres=LayoutInt(v,"interior_top_um"); + x.voxelSizeMicrometres=static_cast(LayoutInt(v,"voxel_size_um")); + const auto& origin=v.Get("lattice_origin_um").AsArray();for(std::size_t i=0;i<3&&i(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 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 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; +} +} diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index deb2d87..442ab32 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -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(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); diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 0ff684e..46dcdb0 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -2,6 +2,7 @@ #include "StandaloneSnapshotAdapter.h" #include "VoxelForgeCore/VoxelForgeCore.h" #include "WorldAssemblerInternal.h" +#include "ResolvedLayoutInternal.h" #include "FloatingIslandsInternal.h" #include @@ -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(position.x),static_cast(position.y),static_cast(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(position.x),static_cast(position.y),static_cast(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(position.x),static_cast(position.y),static_cast(position.z)):0.0f; } DetailFieldSample World::detailSampleAt(Vec3d position) const diff --git a/Standalone/Research/WorldAssemblerM3/Linux21/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux21/REPORT.md new file mode 100644 index 0000000..ef51b1d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux21/REPORT.md @@ -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//generator/`. 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). diff --git a/Standalone/Research/WorldAssemblerM3/Linux21/RunEvidenceL21.py b/Standalone/Research/WorldAssemblerM3/Linux21/RunEvidenceL21.py new file mode 100644 index 0000000..663db0f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux21/RunEvidenceL21.py @@ -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) diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 973591b..658e5ab 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -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(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(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 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); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 4d93c4f..1b221b2 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -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 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+1strates.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(1,World.CoreVoxelSizeMicrons); + const auto expectedAt=[&](std::int64_t zMicrometres) + { + std::vector candidates; + for(const auto& Stratum:ResolvedLayout->strates) + { + const auto contains=[zMicrometres](const VoxelForgeCore::ResolvedLayoutBandV1& Band) + {return Band.presentInCurrentField&&zMicrometres>=Band.bottomMicrometres + &&zMicrometres offsets{{ + -static_cast(World.CoreVoxelSizeMicrons),0, + static_cast(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(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 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(Endpoint),static_cast(Offset), + static_cast(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(probes),static_cast(disagreements), + disagreements==0?"PASS":"DEVIATIONS_RECORDED"); + Streamer.Stop(); + return 0; + } const auto ShapeReady=Clock::now(); const double ShapeSeconds=std::chrono::duration(ShapeReady-ShapeBuildStart).count(); if(bAuditTileShapeReadiness) diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index e581ccf..9ff15fa 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -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(1,owner.CoreVoxelSizeMicrons); + const std::int64_t managerChunkMicrons=CHUNK_SIZE*static_cast(owner.OwnerVoxelSizeMicrons); + const auto fixedToMicrons=[&](std::int64_t fixed) + {return static_cast(std::llround(static_cast(fixed) + *static_cast(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(slot.BottomChunkZ)*CHUNK_SIZE*coreMicrons; + std::int64_t top=static_cast(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(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(CHUNK_SIZE)*owner.OwnerVoxelSizeMicrons + :static_cast(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::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;nmanifest.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;nroute.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;nGetPassages().Num();++n) + { + const auto& passage=owner.Manager->GetPassages()[static_cast(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