From 53a1e69af733b1ebe1b43e9f4fbd1a79b2691656 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sat, 10 Oct 2026 11:36:32 +0200 Subject: [PATCH] wip(assembler): M3 round 1 -- native FloatingIslands recipe in Core (standalone) Bounds 517 boxes, 0 missed, negative control caught; capsule route 1044 centers, raised-route control fails; W1/W8/W16 stable (field bf3ce7da876431e2); 39 ms native vs 64 ms legacy. My review: islands read as thin flat pads with a radial pinch under them, not rock; next round fixes the look. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- Standalone/Core/API.md | 20 + .../include/VoxelForgeCore/VoxelForgeCore.h | 160 +- Standalone/Core/src/FloatingIslandsInternal.h | 1474 +++++++++++++++++ Standalone/Core/src/VoxelForgeCore.cpp | 1 + .../gpu_field_port/LUNA_DIALOGUE.md | 40 + .../Round1/BuildFloatingIslandsAudit.ps1 | 118 ++ .../Round1/FloatingIslandsAudit.cpp | 728 ++++++++ .../WorldAssemblerM3/Round1/REPORT.md | 174 ++ .../Round1/RenderFloatingIslandCaptures.py | 236 +++ .../Round1/results/audit.json | 21 + .../Round1/results/captures/contact_sheet.jpg | Bin 0 -> 68601 bytes .../results/captures/edge_cliff_matched.jpg | Bin 0 -> 14693 bytes .../results/captures/overview_matched.jpg | Bin 0 -> 12798 bytes .../Round1/results/captures/route_matched.jpg | Bin 0 -> 14572 bytes .../captures/tile_lod_seam_matched.jpg | Bin 0 -> 18890 bytes .../results/captures/underside_matched.jpg | Bin 0 -> 17282 bytes 16 files changed, 2971 insertions(+), 1 deletion(-) create mode 100644 Standalone/Core/src/FloatingIslandsInternal.h create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/BuildFloatingIslandsAudit.ps1 create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/FloatingIslandsAudit.cpp create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/RenderFloatingIslandCaptures.py create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/results/audit.json create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/results/captures/contact_sheet.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/results/captures/edge_cliff_matched.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/results/captures/overview_matched.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/results/captures/route_matched.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/results/captures/tile_lod_seam_matched.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Round1/results/captures/underside_matched.jpg diff --git a/Standalone/Core/API.md b/Standalone/Core/API.md index 157c4e6..aee46af 100644 --- a/Standalone/Core/API.md +++ b/Standalone/Core/API.md @@ -144,6 +144,26 @@ at each swept center. Automatic changes must be composed first and pass this aud accepted. Player carve/fill edits remain unrestricted. This validates analytic field collision and support; it does not cook an Unreal physics mesh. +## Floating-island feature family v1 (standalone Core) + +`FloatingIslandRecipeV1` and `FloatingIslandManifestV1` define a separate, reusable finite 3D feature +family. Coordinates, dimensions, and density values use signed 1/256-voxel units; negative density +is solid. The recipe carries its seed, fixed vertical layout, site density, island count and spacing, +radius range, top flattening, taper, bounded contour warp, roughness, blend, connector dimensions and +route constraints. `PlanFloatingIslandCluster` emits stable island and connector IDs, a connected +cluster graph, conservative influence AABBs, and a centerline route. At least the primary chain is +always planned; `islandDensityQ16` controls up to two optional side sites. Connectors carry a sharp-tag +ready bit for a later hybrid-mesher adapter. + +`FloatingIslandFieldV1::densitySubvoxels` evaluates the canonical island and connector field. +`interval` returns unknown for boxes overlapping any influence AABB and a certified all-air range +outside them. AABBs include contour displacement, roughness, taper height and smoothing reach. +`AuditFloatingIslandBounds` checks sampled boxes and void beside connectors and includes a deliberately +shrunken island-bound negative control. `AuditFloatingIslandRoute` samples the player capsule and +support along the serialized route. The current field block is not yet composed into +`WorldAssemblyManifest` or the Unreal adapter; M3 round 1 proves the standalone Core representation. +The frozen owner evaluator is used as a visual and timing oracle, not as a hash target. + ## Queries and tiles `densityAt(position)` returns signed density; negative is solid and nonnegative is air. Server and diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index e0ae913..7699cd2 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -153,7 +153,8 @@ namespace VoxelForgeCore enum class FieldChannel : std::uint32_t { rock = 1u << 0, height = 1u << 1, biome = 1u << 2, water = 1u << 3, - waterSurface = 1u << 4, route = 1u << 5, surfaceTags = 1u << 6 + waterSurface = 1u << 4, route = 1u << 5, surfaceTags = 1u << 6, + island = 1u << 7 }; constexpr std::uint32_t ChannelBit(FieldChannel channel) noexcept @@ -294,6 +295,163 @@ namespace VoxelForgeCore std::string ToJson() const; }; + // Reusable finite 3D island features. The recipe owns fixed-scale shape ranges; the plan + // serializes the seeded anchors, connected cluster graph, route and sharp-ready connectors. + // Reutilisable dans toute recette : le plan porte les ancres, le graphe, la route et les plans nets. + struct FloatingIslandRecipeV1 + { + std::uint32_t schemaVersion = 1; + std::uint32_t unitsPerVoxel = static_cast(WorldAssemblySubvoxelsPerVoxel); + std::uint32_t seed = 0; + std::int64_t layoutOriginXSubvoxels = 0; + std::int64_t layoutOriginYSubvoxels = 0; + std::int64_t layoutCenterTopZSubvoxels = -128 * WorldAssemblySubvoxelsPerVoxel; + std::int64_t layoutMinimumZSubvoxels = -256 * WorldAssemblySubvoxelsPerVoxel; + std::int64_t layoutMaximumZSubvoxels = 0; + std::uint32_t primaryIslandCount = 5; + std::int64_t islandDensityQ16 = 1ll << 15; + std::int64_t spacingSubvoxels = 112 * WorldAssemblySubvoxelsPerVoxel; + std::int64_t minimumRadiusSubvoxels = 40 * WorldAssemblySubvoxelsPerVoxel; + std::int64_t maximumRadiusSubvoxels = 48 * WorldAssemblySubvoxelsPerVoxel; + std::int64_t thicknessRatioQ16 = 39322; // 0.60 + std::int64_t verticalJitterQ16 = 26214; // 0.40 + std::int64_t topFlattenQ16 = 51118; // 0.78 + std::int64_t taperEndMinimumQ16 = 29491; // 0.45 + std::int64_t taperEndMaximumQ16 = 45875; // 0.70 + std::int64_t blendRadiusSubvoxels = 4 * WorldAssemblySubvoxelsPerVoxel; + std::int64_t contourWarpSubvoxels = 15 * WorldAssemblySubvoxelsPerVoxel; + std::int64_t roughnessSubvoxels = WorldAssemblySubvoxelsPerVoxel / 2; + std::int64_t boundarySealSubvoxels = 4 * WorldAssemblySubvoxelsPerVoxel; + std::int64_t bridgeHalfWidthSubvoxels = 4 * WorldAssemblySubvoxelsPerVoxel; + std::int64_t bridgeThicknessSubvoxels = 3 * WorldAssemblySubvoxelsPerVoxel; + WorldAssemblyRouteConstraintsV1 routeConstraints; + std::uint64_t contentHash = 0; + }; + + struct FloatingIslandAnchorV1 + { + std::uint64_t stableId = 0; + std::uint32_t ordinal = 0; + std::array centerSubvoxels{}; + std::int64_t radiusSubvoxels = 0; + std::int64_t topHalfSubvoxels = 0; + std::int64_t topZSubvoxels = 0; + std::int64_t bottomZSubvoxels = 0; + std::int64_t taperEndQ16 = 0; + std::array influenceMinimumSubvoxels{}; + std::array influenceMaximumSubvoxels{}; + }; + + struct FloatingIslandConnectorV1 + { + std::uint64_t stableId = 0; + std::uint64_t fromIslandId = 0; + std::uint64_t toIslandId = 0; + std::array startSubvoxels{}; + std::array endSubvoxels{}; + std::int64_t halfWidthSubvoxels = 0; + std::int64_t thicknessSubvoxels = 0; + std::array influenceMinimumSubvoxels{}; + std::array influenceMaximumSubvoxels{}; + bool sharpTagReady = true; + }; + + struct FloatingIslandPlanV1 + { + std::vector islands; + std::vector clusterEdges; + std::vector route; + std::uint64_t contentHash = 0; + }; + + struct VFCORE_API FloatingIslandManifestV1 + { + FloatingIslandRecipeV1 recipe; + FloatingIslandPlanV1 plan; + std::uint64_t contentHash = 0; + + static std::optional FromJson( + std::string_view json, std::string& error); + bool finalize(std::string& error); + bool validate(std::string& error) const; + std::string ToJson() const; + }; + + struct FloatingIslandBoundsAuditResult + { + std::uint64_t boxesTested = 0; + std::uint64_t pointSamples = 0; + std::uint64_t missedBounds = 0; + std::uint64_t unknownBounds = 0; + std::uint64_t islandBoundsTested = 0; + std::uint64_t connectorBoundsTested = 0; + std::uint64_t voidBoxesProvedAir = 0; + std::uint64_t voidPointSamples = 0; + bool negativeControlDetected = false; + bool passed = false; + }; + + struct FloatingIslandRouteAuditResult + { + std::uint64_t segmentsTested = 0; + std::uint64_t centerSamples = 0; + std::uint64_t capsuleFieldSamples = 0; + std::uint64_t blockedCapsuleSamples = 0; + std::uint64_t supportProbeSamples = 0; + std::uint64_t unsupportedCenterSamples = 0; + std::uint64_t gradeFailures = 0; + std::int64_t minimumClearanceSubvoxels = 0; + std::uint64_t firstBlockedSegment = ~std::uint64_t{0}; + std::uint64_t firstUnsupportedSegment = ~std::uint64_t{0}; + std::uint64_t firstGradeFailureSegment = ~std::uint64_t{0}; + std::array firstBlockedPositionSubvoxels{}; + std::array firstUnsupportedCenterSubvoxels{}; + bool passed = false; + }; + + struct FloatingIslandInfluenceBounds + { + bool finite = false; + std::array minimumSubvoxels{}; + std::array maximumSubvoxels{}; + }; + + struct FieldInterval; + struct FieldDetailEnvelope; + struct FieldBlockContract; + + class VFCORE_API FloatingIslandFieldV1 + { + public: + FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe, + const FloatingIslandPlanV1& plan) noexcept; + bool isValid() const noexcept; + std::int64_t densitySubvoxels(std::int64_t xSubvoxels, + std::int64_t ySubvoxels, std::int64_t zSubvoxels) const noexcept; + FieldInterval interval(std::int64_t minimumXSubvoxels, + std::int64_t minimumYSubvoxels, std::int64_t minimumZSubvoxels, + std::int64_t maximumXSubvoxels, std::int64_t maximumYSubvoxels, + std::int64_t maximumZSubvoxels) const noexcept; + FloatingIslandInfluenceBounds influenceBounds() const noexcept; + FieldDetailEnvelope detailEnvelope() const noexcept; + FieldBlockContract contract() const noexcept; + const FloatingIslandRecipeV1* recipe() const noexcept { return recipe_; } + const FloatingIslandPlanV1* plan() const noexcept { return plan_; } + + private: + const FloatingIslandRecipeV1* recipe_ = nullptr; + const FloatingIslandPlanV1* plan_ = nullptr; + }; + + // Planner output is ordinary reviewed data; clients can load it without re-running the planner. + VFCORE_API std::optional PlanFloatingIslandCluster( + const FloatingIslandRecipeV1& recipe, std::string& error); + VFCORE_API FloatingIslandBoundsAuditResult AuditFloatingIslandBounds( + const FloatingIslandFieldV1& field, std::uint32_t boxCount = 256, + std::uint64_t seed = 0x49534c414e444231ull); + VFCORE_API FloatingIslandRouteAuditResult AuditFloatingIslandRoute( + const FloatingIslandFieldV1& field, std::int64_t routeOffsetSubvoxels = 0); + struct HeightfieldPointSample { std::int64_t groundHeightSubvoxels = 0; diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h new file mode 100644 index 0000000..35ec1ac --- /dev/null +++ b/Standalone/Core/src/FloatingIslandsInternal.h @@ -0,0 +1,1474 @@ +#pragma once + +#include "Json.h" +#include "VoxelForgeCore/VoxelForgeCore.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace VoxelForgeCore +{ +namespace floating_island_detail +{ + constexpr std::int64_t Q = WorldAssemblySubvoxelsPerVoxel; + constexpr std::int64_t Q16 = 1ll << 16; + constexpr std::int64_t MaxScalar = 1000000000000ll; + constexpr std::int64_t MaxDensity = MaxScalar; + constexpr double Pi = 3.141592653589793238462643383279502884; + + inline std::uint64_t StartHash() { return 14695981039346656037ull; } + + inline void HashWord(std::uint64_t& hash, std::uint64_t word, int bytes) + { + for (int i = 0; i < bytes; ++i) + { + hash ^= static_cast(word >> (i * 8)); + hash *= 1099511628211ull; + } + } + + inline std::uint64_t Mix(std::uint64_t value) + { + value += 0x9e3779b97f4a7c15ull; + value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ull; + value = (value ^ (value >> 27)) * 0x94d049bb133111ebull; + return value ^ (value >> 31); + } + + inline std::uint64_t Roll(std::uint32_t seed, std::uint64_t site, std::uint64_t salt) + { return Mix((static_cast(seed) << 32) ^ site ^ salt); } + + inline std::uint32_t UnitQ16(std::uint32_t seed, std::uint64_t site, std::uint64_t salt) + { return static_cast(Roll(seed, site, salt) >> 48); } + + inline std::int64_t SignedRange(std::uint32_t seed, std::uint64_t site, + std::uint64_t salt, std::int64_t magnitude) + { + const std::uint32_t unit = UnitQ16(seed, site, salt); + return (static_cast(unit) * 2 * magnitude / Q16) - magnitude; + } + + inline std::string HashText(std::uint64_t hash) + { + constexpr char Hex[] = "0123456789abcdef"; + std::string output(16, '0'); + for (int i = 15; i >= 0; --i) + { + output[static_cast(i)] = Hex[hash & 0xfu]; + hash >>= 4; + } + return output; + } + + inline bool ReadInteger(const VFJson::Value& value, std::int64_t& output) + { + if (!value.IsNumber()) return false; + const double number = value.Number(); + if (!std::isfinite(number) || std::trunc(number) != number + || std::abs(number) > 9007199254740991.0) return false; + output = static_cast(number); + return true; + } + + inline bool ReadU32(const VFJson::Value& value, std::uint32_t& output) + { + std::int64_t parsed = 0; + if (!ReadInteger(value, parsed) || parsed < 0 + || parsed > std::numeric_limits::max()) return false; + output = static_cast(parsed); + return true; + } + + inline bool ReadHash(const VFJson::Value& value, std::uint64_t& output) + { + if (!value.IsString()) return false; + std::string_view text = value.String(); + if (text.starts_with("0x") || text.starts_with("0X")) text.remove_prefix(2); + if (text.empty() || text.size() > 16) return false; + const auto result = std::from_chars(text.data(), text.data() + text.size(), output, 16); + return result.ec == std::errc{} && result.ptr == text.data() + text.size(); + } + + inline bool ReadArray(const VFJson::Value& value, std::array& output) + { + if (!value.IsArray() || value.AsArray().size() != output.size()) return false; + for (std::size_t i = 0; i < output.size(); ++i) + if (!ReadInteger(value.AsArray()[i], output[i])) return false; + return true; + } + + inline bool ValidScalar(std::int64_t value) + { return value >= -MaxScalar && value <= MaxScalar; } + + inline double Q16ToDouble(std::int64_t value) + { return static_cast(value) / static_cast(Q16); } + + inline std::int64_t RoundQ(double value) + { + if (!std::isfinite(value)) return value < 0.0 ? -MaxDensity : MaxDensity; + return static_cast(std::llround(std::clamp(value, + -static_cast(MaxDensity), static_cast(MaxDensity)))); + } + + inline double SmoothStep(double value) + { + value = std::clamp(value, 0.0, 1.0); + return value * value * (3.0 - 2.0 * value); + } + + inline double SmoothMin(double a, double b, double k) + { + if (!(k > 0.0)) return std::min(a, b); + const double h = std::max(k - std::abs(a - b), 0.0) / k; + return std::min(a, b) - h * h * k * 0.25; + } + + inline std::uint64_t StableId(std::uint32_t seed, std::uint64_t site, std::uint64_t salt) + { + std::uint64_t hash = StartHash(); + HashWord(hash, seed, 4); HashWord(hash, site, 8); HashWord(hash, salt, 8); + return hash == 0 ? 1 : hash; + } + + inline std::uint64_t HashRouteConstraints(const WorldAssemblyRouteConstraintsV1& value) + { + std::uint64_t hash = StartHash(); + HashWord(hash, value.version, 4); + HashWord(hash, static_cast(value.capsuleRadiusSubvoxels), 8); + HashWord(hash, static_cast(value.capsuleHeightSubvoxels), 8); + HashWord(hash, static_cast(value.maximumGradeQ16), 8); + HashWord(hash, static_cast(value.sweepSpacingSubvoxels), 8); + HashWord(hash, static_cast(value.supportProbeRadiusSubvoxels), 8); + HashWord(hash, static_cast(value.supportSearchDepthSubvoxels), 8); + HashWord(hash, static_cast(value.minimumClearanceSubvoxels), 8); + HashWord(hash, value.minimumSupportProbes, 4); + return hash; + } + + inline std::uint64_t HashRecipe(const FloatingIslandRecipeV1& recipe) + { + std::uint64_t hash = StartHash(); + HashWord(hash, recipe.schemaVersion, 4); + HashWord(hash, recipe.unitsPerVoxel, 4); + HashWord(hash, recipe.seed, 4); + HashWord(hash, static_cast(recipe.layoutOriginXSubvoxels), 8); + HashWord(hash, static_cast(recipe.layoutOriginYSubvoxels), 8); + HashWord(hash, static_cast(recipe.layoutCenterTopZSubvoxels), 8); + HashWord(hash, static_cast(recipe.layoutMinimumZSubvoxels), 8); + HashWord(hash, static_cast(recipe.layoutMaximumZSubvoxels), 8); + HashWord(hash, recipe.primaryIslandCount, 4); + HashWord(hash, static_cast(recipe.islandDensityQ16), 8); + HashWord(hash, static_cast(recipe.spacingSubvoxels), 8); + HashWord(hash, static_cast(recipe.minimumRadiusSubvoxels), 8); + HashWord(hash, static_cast(recipe.maximumRadiusSubvoxels), 8); + HashWord(hash, static_cast(recipe.thicknessRatioQ16), 8); + HashWord(hash, static_cast(recipe.verticalJitterQ16), 8); + HashWord(hash, static_cast(recipe.topFlattenQ16), 8); + HashWord(hash, static_cast(recipe.taperEndMinimumQ16), 8); + HashWord(hash, static_cast(recipe.taperEndMaximumQ16), 8); + HashWord(hash, static_cast(recipe.blendRadiusSubvoxels), 8); + HashWord(hash, static_cast(recipe.contourWarpSubvoxels), 8); + HashWord(hash, static_cast(recipe.roughnessSubvoxels), 8); + HashWord(hash, static_cast(recipe.boundarySealSubvoxels), 8); + HashWord(hash, static_cast(recipe.bridgeHalfWidthSubvoxels), 8); + HashWord(hash, static_cast(recipe.bridgeThicknessSubvoxels), 8); + HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8); + return hash; + } + + inline std::uint64_t HashPlan(const FloatingIslandPlanV1& plan) + { + std::uint64_t hash = StartHash(); + HashWord(hash, plan.islands.size(), 4); + for (const auto& island : plan.islands) + { + HashWord(hash, island.stableId, 8); HashWord(hash, island.ordinal, 4); + for (auto value : island.centerSubvoxels) HashWord(hash, static_cast(value), 8); + HashWord(hash, static_cast(island.radiusSubvoxels), 8); + HashWord(hash, static_cast(island.topHalfSubvoxels), 8); + HashWord(hash, static_cast(island.topZSubvoxels), 8); + HashWord(hash, static_cast(island.bottomZSubvoxels), 8); + HashWord(hash, static_cast(island.taperEndQ16), 8); + for (auto value : island.influenceMinimumSubvoxels) HashWord(hash, static_cast(value), 8); + for (auto value : island.influenceMaximumSubvoxels) HashWord(hash, static_cast(value), 8); + } + HashWord(hash, plan.clusterEdges.size(), 4); + for (const auto& edge : plan.clusterEdges) + { + HashWord(hash, edge.stableId, 8); HashWord(hash, edge.fromIslandId, 8); + HashWord(hash, edge.toIslandId, 8); + for (auto value : edge.startSubvoxels) HashWord(hash, static_cast(value), 8); + for (auto value : edge.endSubvoxels) HashWord(hash, static_cast(value), 8); + HashWord(hash, static_cast(edge.halfWidthSubvoxels), 8); + HashWord(hash, static_cast(edge.thicknessSubvoxels), 8); + for (auto value : edge.influenceMinimumSubvoxels) HashWord(hash, static_cast(value), 8); + for (auto value : edge.influenceMaximumSubvoxels) HashWord(hash, static_cast(value), 8); + HashWord(hash, edge.sharpTagReady ? 1u : 0u, 1); + } + HashWord(hash, plan.route.size(), 4); + for (const auto& point : plan.route) + { + HashWord(hash, point.stableFeatureId, 8); + for (auto value : point.positionSubvoxels) HashWord(hash, static_cast(value), 8); + HashWord(hash, static_cast(point.radiusSubvoxels), 8); + } + return hash; + } + + inline std::uint64_t HashManifest(const FloatingIslandManifestV1& manifest) + { + std::uint64_t hash = StartHash(); + HashWord(hash, manifest.recipe.contentHash, 8); + HashWord(hash, manifest.plan.contentHash, 8); + return hash; + } + + inline bool ValidateRecipe(const FloatingIslandRecipeV1& recipe, std::string& error) + { + const std::array values = { + recipe.layoutOriginXSubvoxels, recipe.layoutOriginYSubvoxels, + recipe.layoutCenterTopZSubvoxels, recipe.layoutMinimumZSubvoxels, + recipe.layoutMaximumZSubvoxels, recipe.islandDensityQ16, + recipe.spacingSubvoxels, recipe.minimumRadiusSubvoxels, + recipe.maximumRadiusSubvoxels, recipe.thicknessRatioQ16, + recipe.verticalJitterQ16, recipe.topFlattenQ16, + recipe.taperEndMinimumQ16, recipe.taperEndMaximumQ16, + recipe.blendRadiusSubvoxels, recipe.contourWarpSubvoxels, + recipe.roughnessSubvoxels, recipe.boundarySealSubvoxels, + recipe.bridgeHalfWidthSubvoxels, recipe.bridgeThicknessSubvoxels, + recipe.routeConstraints.capsuleRadiusSubvoxels, + recipe.routeConstraints.capsuleHeightSubvoxels, + recipe.routeConstraints.maximumGradeQ16}; + if (recipe.schemaVersion != 1 || recipe.unitsPerVoxel != Q + || recipe.primaryIslandCount < 3 || recipe.primaryIslandCount > 9 + || recipe.layoutMaximumZSubvoxels <= recipe.layoutMinimumZSubvoxels + || recipe.layoutMaximumZSubvoxels - recipe.layoutMinimumZSubvoxels > 2048 * Q + || std::any_of(values.begin(), values.end(), [](auto value) { return !ValidScalar(value); })) + { error = "floating-island recipe version, scale, layout or scalar range is invalid"; return false; } + const auto& constraints = recipe.routeConstraints; + if (recipe.islandDensityQ16 < 6554 || recipe.islandDensityQ16 > (1ll << 15) + || recipe.spacingSubvoxels < 64 * Q || recipe.spacingSubvoxels > 256 * Q + || recipe.minimumRadiusSubvoxels < 24 * Q + || recipe.maximumRadiusSubvoxels < recipe.minimumRadiusSubvoxels + || recipe.maximumRadiusSubvoxels > 64 * Q + || recipe.thicknessRatioQ16 < 29491 || recipe.thicknessRatioQ16 > 49152 + || recipe.verticalJitterQ16 < 13107 || recipe.verticalJitterQ16 > 39322 + || recipe.topFlattenQ16 < 39322 || recipe.topFlattenQ16 > 52429 + || recipe.taperEndMinimumQ16 < 26214 || recipe.taperEndMaximumQ16 > 52429 + || recipe.taperEndMinimumQ16 > recipe.taperEndMaximumQ16 + || recipe.blendRadiusSubvoxels < 2 * Q || recipe.blendRadiusSubvoxels > 6 * Q + || recipe.contourWarpSubvoxels <= 0 + || recipe.contourWarpSubvoxels > recipe.maximumRadiusSubvoxels / 2 + || recipe.roughnessSubvoxels < 0 + || recipe.roughnessSubvoxels > recipe.minimumRadiusSubvoxels / 4 + || recipe.boundarySealSubvoxels < 2 * Q + || recipe.bridgeHalfWidthSubvoxels < constraints.capsuleRadiusSubvoxels + Q + || recipe.bridgeThicknessSubvoxels < 2 * Q + || constraints.version != 1 || constraints.capsuleRadiusSubvoxels <= 0 + || constraints.capsuleHeightSubvoxels < 2 * constraints.capsuleRadiusSubvoxels + || constraints.maximumGradeQ16 <= 0 || constraints.maximumGradeQ16 > Q16 + || constraints.sweepSpacingSubvoxels <= 0 || constraints.sweepSpacingSubvoxels > Q / 2 + || !ValidScalar(constraints.sweepSpacingSubvoxels) + || !ValidScalar(constraints.supportProbeRadiusSubvoxels) + || !ValidScalar(constraints.supportSearchDepthSubvoxels) + || !ValidScalar(constraints.minimumClearanceSubvoxels) + || constraints.supportProbeRadiusSubvoxels <= 0 + || constraints.supportSearchDepthSubvoxels <= 0 + || constraints.minimumClearanceSubvoxels < 0 + || constraints.minimumSupportProbes == 0 || constraints.minimumSupportProbes > 5) + { error = "floating-island shape or route parameters are outside the supported range"; return false; } + return true; + } + + inline bool Contains(const std::array& minimum, + const std::array& maximum, + const std::array& point) + { + for (std::size_t axis = 0; axis < 3; ++axis) + if (point[axis] < minimum[axis] || point[axis] > maximum[axis]) return false; + return true; + } + + inline bool Intersects(const std::array& minimumA, + const std::array& maximumA, + const std::array& minimumB, + const std::array& maximumB) + { + for (std::size_t axis = 0; axis < 3; ++axis) + if (maximumA[axis] < minimumB[axis] || minimumA[axis] > maximumB[axis]) return false; + return true; + } + + inline std::int64_t IslandBlendMargin(const FloatingIslandRecipeV1& recipe) + { return 3 * recipe.blendRadiusSubvoxels; } + + inline double LobeOffset(const FloatingIslandRecipeV1& recipe, + const FloatingIslandAnchorV1& island, + double dx, double dy, double zSubvoxels) + { + const double angle = std::atan2(dy, dx); + const double z = zSubvoxels / static_cast(Q); + const double seed = static_cast(Roll(recipe.seed, island.stableId, 0x4c4f4245ull) + & 0xffffffffull) * (2.0 * Pi / 4294967296.0); + const double a = std::sin(3.0 * angle + seed + z * 0.017); + const double b = std::sin(5.0 * angle - seed * 0.73 - z * 0.023); + const double c = std::sin(8.0 * angle + seed * 1.41 + z * 0.011); + return static_cast(recipe.contourWarpSubvoxels) * (0.50 * a + 0.30 * b + 0.20 * c); + } + + inline double Roughness(const FloatingIslandRecipeV1& recipe, + const FloatingIslandAnchorV1& island, + double x, double y, double z) + { + const double phase = static_cast(Roll(recipe.seed, island.stableId, 0x524f5547ull) + & 0xffffffffull) * (2.0 * Pi / 4294967296.0); + const double xv = x / static_cast(Q); + const double yv = y / static_cast(Q); + const double zv = z / static_cast(Q); + const double noise = 0.52 * std::sin(xv * 0.071 + yv * 0.053 + zv * 0.089 + phase) + + 0.31 * std::sin(xv * 0.127 - yv * 0.097 + zv * 0.061 - phase * 0.73) + + 0.17 * std::cos(xv * 0.043 + yv * 0.139 - zv * 0.113 + phase * 1.39); + return static_cast(recipe.roughnessSubvoxels) * noise; + } + + inline double IslandTopSurface(const FloatingIslandRecipeV1& recipe, + const FloatingIslandAnchorV1& island, + double x, double y) + { + const double dx = x - island.centerSubvoxels[0]; + const double dy = y - island.centerSubvoxels[1]; + const double radial = std::hypot(dx, dy); + const double effectiveRadius = std::max(1.0, + static_cast(island.radiusSubvoxels) + LobeOffset(recipe, island, dx, dy, + static_cast(island.topZSubvoxels))); + const double edge = std::clamp(radial / effectiveRadius, 0.0, 1.0); + const double drop = (1.0 - Q16ToDouble(recipe.topFlattenQ16)) + * static_cast(island.topHalfSubvoxels) * 2.0 * edge * edge; + return static_cast(island.topZSubvoxels) - drop; + } + + inline double IslandDensity(const FloatingIslandRecipeV1& recipe, + const FloatingIslandAnchorV1& island, + double x, double y, double z) + { + const double dx = x - static_cast(island.centerSubvoxels[0]); + const double dy = y - static_cast(island.centerSubvoxels[1]); + const double radial = std::hypot(dx, dy); + const double topZ = static_cast(island.topZSubvoxels); + const double bottomZ = static_cast(island.bottomZSubvoxels); + const double verticalSpan = std::max(topZ - bottomZ, 1.0); + const double height = std::clamp((z - bottomZ) / verticalSpan, 0.0, 1.0); + const double taperEnd = Q16ToDouble(island.taperEndQ16); + const double taper = SmoothStep(std::clamp(height / std::max(taperEnd, 0.01), 0.0, 1.0)); + const double effectiveRadius = std::max(0.0, + static_cast(island.radiusSubvoxels) + LobeOffset(recipe, island, dx, dy, z)); + const double envelope = effectiveRadius * taper; + const double top = IslandTopSurface(recipe, island, + static_cast(island.centerSubvoxels[0]) + dx, + static_cast(island.centerSubvoxels[1]) + dy); + const double sdf = std::max({z - top, radial - envelope, bottomZ - z}); + return sdf + Roughness(recipe, island, x, y, z); + } + + inline double ConnectorDensity(const FloatingIslandConnectorV1& edge, + double x, double y, double z) + { + const double ax = static_cast(edge.startSubvoxels[0]); + const double ay = static_cast(edge.startSubvoxels[1]); + const double az = static_cast(edge.startSubvoxels[2]); + const double dx = static_cast(edge.endSubvoxels[0]) - ax; + const double dy = static_cast(edge.endSubvoxels[1]) - ay; + const double length = std::hypot(dx, dy); + if (!(length > 0.0)) return MaxDensity; + const double ux = dx / length, uy = dy / length; + const double px = x - ax, py = y - ay; + const double along = px * ux + py * uy; + const double side = std::abs(-px * uy + py * ux); + const double qx = side - static_cast(edge.halfWidthSubvoxels); + const double qy = std::max(-along, along - length); + const double outside = std::hypot(std::max(qx, 0.0), std::max(qy, 0.0)); + const double horizontal = outside + std::min(std::max(qx, qy), 0.0); + const double t = std::clamp(along / length, 0.0, 1.0); + const double deck = az + (static_cast(edge.endSubvoxels[2]) - az) * t; + return std::max({horizontal, z - deck, + deck - static_cast(edge.thicknessSubvoxels) - z}); + } + + inline std::int64_t FieldDensity(const FloatingIslandRecipeV1& recipe, + const FloatingIslandPlanV1& plan, + std::int64_t x, std::int64_t y, std::int64_t z) + { + if (plan.islands.empty()) return MaxDensity; + const double blend = static_cast(recipe.blendRadiusSubvoxels); + double result = static_cast(MaxDensity); + for (const auto& island : plan.islands) + { + double group = IslandDensity(recipe, island, + static_cast(x), static_cast(y), static_cast(z)); + for (const auto& edge : plan.clusterEdges) + { + if (edge.fromIslandId != island.stableId) continue; + group = SmoothMin(group, ConnectorDensity(edge, + static_cast(x), static_cast(y), static_cast(z)), blend); + } + result = std::min(result, group); + } + return std::clamp(RoundQ(result), -MaxDensity, MaxDensity); + } + + inline void SetIslandBounds(const FloatingIslandRecipeV1& recipe, + FloatingIslandAnchorV1& island) + { + const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels; + const std::int64_t reach = island.radiusSubvoxels + recipe.contourWarpSubvoxels + pad; + island.influenceMinimumSubvoxels = {island.centerSubvoxels[0] - reach, + island.centerSubvoxels[1] - reach, island.bottomZSubvoxels - pad}; + island.influenceMaximumSubvoxels = {island.centerSubvoxels[0] + reach, + island.centerSubvoxels[1] + reach, island.topZSubvoxels + pad}; + } + + inline void SetConnectorBounds(const FloatingIslandRecipeV1& recipe, + FloatingIslandConnectorV1& edge) + { + const std::int64_t pad = recipe.blendRadiusSubvoxels + recipe.roughnessSubvoxels; + const std::int64_t xyPad = edge.halfWidthSubvoxels + pad; + edge.influenceMinimumSubvoxels = { + std::min(edge.startSubvoxels[0], edge.endSubvoxels[0]) - xyPad, + std::min(edge.startSubvoxels[1], edge.endSubvoxels[1]) - xyPad, + std::min(edge.startSubvoxels[2], edge.endSubvoxels[2]) - edge.thicknessSubvoxels - pad}; + edge.influenceMaximumSubvoxels = { + std::max(edge.startSubvoxels[0], edge.endSubvoxels[0]) + xyPad, + std::max(edge.startSubvoxels[1], edge.endSubvoxels[1]) + xyPad, + std::max(edge.startSubvoxels[2], edge.endSubvoxels[2]) + pad}; + } + + inline const FloatingIslandAnchorV1* FindIsland(const FloatingIslandPlanV1& plan, + std::uint64_t id) + { + const auto found = std::find_if(plan.islands.begin(), plan.islands.end(), + [id](const auto& island) { return island.stableId == id; }); + return found == plan.islands.end() ? nullptr : &*found; + } + + inline bool ValidateManifest(const FloatingIslandManifestV1& manifest, std::string& error) + { + if (!ValidateRecipe(manifest.recipe, error)) return false; + const auto& recipe = manifest.recipe; + const auto& plan = manifest.plan; + if (plan.islands.size() < recipe.primaryIslandCount + || plan.islands.size() > recipe.primaryIslandCount + 2 + || plan.clusterEdges.size() + 1 < plan.islands.size() + || plan.clusterEdges.size() > plan.islands.size() + 1 + || plan.route.size() < 2 || plan.route.size() > 4096) + { error = "floating-island plan has an invalid bounded island graph or route"; return false; } + + std::unordered_set islandIds; + std::unordered_set ordinals; + for (const auto& island : plan.islands) + { + if (island.stableId == 0 || !islandIds.insert(island.stableId).second + || !ordinals.insert(island.ordinal).second + || island.radiusSubvoxels < recipe.minimumRadiusSubvoxels + || island.radiusSubvoxels > recipe.maximumRadiusSubvoxels + || island.topHalfSubvoxels <= 0 || island.topZSubvoxels <= island.bottomZSubvoxels + || island.taperEndQ16 < recipe.taperEndMinimumQ16 + || island.taperEndQ16 > recipe.taperEndMaximumQ16 + || island.centerSubvoxels[2] != island.topZSubvoxels - island.topHalfSubvoxels) + { error = "floating-island anchor has an invalid or unstable identity or profile"; return false; } + for (std::size_t axis = 0; axis < 3; ++axis) + { + if (!ValidScalar(island.centerSubvoxels[axis]) + || island.influenceMinimumSubvoxels[axis] > island.influenceMaximumSubvoxels[axis]) + { error = "floating-island anchor has invalid coordinates or an inverted bound"; return false; } + } + const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels; + const std::int64_t reach = island.radiusSubvoxels + recipe.contourWarpSubvoxels + pad; + if (island.influenceMinimumSubvoxels[0] > island.centerSubvoxels[0] - reach + || island.influenceMaximumSubvoxels[0] < island.centerSubvoxels[0] + reach + || island.influenceMinimumSubvoxels[1] > island.centerSubvoxels[1] - reach + || island.influenceMaximumSubvoxels[1] < island.centerSubvoxels[1] + reach + || island.influenceMinimumSubvoxels[2] > island.bottomZSubvoxels - pad + || island.influenceMaximumSubvoxels[2] < island.topZSubvoxels + pad + || island.bottomZSubvoxels < recipe.layoutMinimumZSubvoxels + recipe.boundarySealSubvoxels + || island.topZSubvoxels > recipe.layoutMaximumZSubvoxels - recipe.boundarySealSubvoxels) + { error = "floating-island bound omits its taper, lobe warp, blend or finite layout margin"; return false; } + } + for (std::uint32_t ordinal = 0; ordinal < recipe.primaryIslandCount; ++ordinal) + if (!ordinals.contains(ordinal)) + { error = "floating-island route chain is missing a primary seeded anchor"; return false; } + + std::unordered_set edgeIds; + std::unordered_map> adjacency; + for (const auto& edge : plan.clusterEdges) + { + if (edge.stableId == 0 || !edgeIds.insert(edge.stableId).second + || edge.fromIslandId == edge.toIslandId || !islandIds.contains(edge.fromIslandId) + || !islandIds.contains(edge.toIslandId) || !edge.sharpTagReady + || edge.halfWidthSubvoxels < recipe.routeConstraints.capsuleRadiusSubvoxels + Q + || edge.thicknessSubvoxels < 2 * Q) + { error = "floating-island graph edge is invalid, unconnected or missing its sharp tag"; return false; } + for (std::size_t axis = 0; axis < 3; ++axis) + if (!ValidScalar(edge.startSubvoxels[axis]) || !ValidScalar(edge.endSubvoxels[axis]) + || edge.influenceMinimumSubvoxels[axis] > edge.influenceMaximumSubvoxels[axis]) + { error = "floating-island connector has invalid endpoint or influence data"; return false; } + const std::int64_t pad = recipe.blendRadiusSubvoxels + recipe.roughnessSubvoxels; + const std::int64_t xyPad = edge.halfWidthSubvoxels + pad; + if (edge.influenceMinimumSubvoxels[0] > std::min(edge.startSubvoxels[0], edge.endSubvoxels[0]) - xyPad + || edge.influenceMaximumSubvoxels[0] < std::max(edge.startSubvoxels[0], edge.endSubvoxels[0]) + xyPad + || edge.influenceMinimumSubvoxels[1] > std::min(edge.startSubvoxels[1], edge.endSubvoxels[1]) - xyPad + || edge.influenceMaximumSubvoxels[1] < std::max(edge.startSubvoxels[1], edge.endSubvoxels[1]) + xyPad + || edge.influenceMinimumSubvoxels[2] > std::min(edge.startSubvoxels[2], edge.endSubvoxels[2]) + - edge.thicknessSubvoxels - pad + || edge.influenceMaximumSubvoxels[2] < std::max(edge.startSubvoxels[2], edge.endSubvoxels[2]) + pad) + { error = "floating-island connector bound omits its swept bridge, blend or thickness"; return false; } + adjacency[edge.fromIslandId].push_back(edge.toIslandId); + adjacency[edge.toIslandId].push_back(edge.fromIslandId); + } + std::unordered_set reached; + std::vector pending{plan.islands.front().stableId}; + while (!pending.empty()) + { + const std::uint64_t current = pending.back(); pending.pop_back(); + if (!reached.insert(current).second) continue; + for (const auto next : adjacency[current]) if (!reached.contains(next)) pending.push_back(next); + } + if (reached.size() != plan.islands.size()) + { error = "floating-island cluster graph is disconnected"; return false; } + + std::unordered_set routeIds; + for (const auto& point : plan.route) + { + if (point.stableFeatureId == 0 || !routeIds.insert(point.stableFeatureId).second + || point.radiusSubvoxels < recipe.routeConstraints.capsuleRadiusSubvoxels + || !std::all_of(point.positionSubvoxels.begin(), point.positionSubvoxels.end(), ValidScalar)) + { error = "floating-island route point has invalid identity, clearance or position"; return false; } + const std::int64_t halfHeight = recipe.routeConstraints.capsuleHeightSubvoxels / 2; + if (point.positionSubvoxels[2] - halfHeight < recipe.layoutMinimumZSubvoxels + || point.positionSubvoxels[2] + halfHeight > recipe.layoutMaximumZSubvoxels) + { error = "floating-island route capsule escapes its finite vertical layout"; return false; } + } + for (std::size_t i = 1; i < plan.route.size(); ++i) + { + double square = 0.0; + for (std::size_t axis = 0; axis < 3; ++axis) + { + const std::int64_t delta = plan.route[i].positionSubvoxels[axis] + - plan.route[i - 1].positionSubvoxels[axis]; + if (std::abs(delta) > 4096 * Q) + { error = "floating-island route segment exceeds its bounded sweep length"; return false; } + square += static_cast(delta) * static_cast(delta); + } + if (!(square > 0.0)) + { error = "floating-island route has a zero-length segment"; return false; } + } + return true; + } + + inline double TopLevel(const FloatingIslandRecipeV1& recipe, std::uint64_t site, + double centerZ, std::int64_t radius, double maxRise) + { + const std::int64_t signedDelta = SignedRange(recipe.seed, site, 0x4c4556454cull, + static_cast(std::llround(maxRise))); + return centerZ + static_cast(signedDelta); + } + + inline FloatingIslandAnchorV1 MakeIsland(const FloatingIslandRecipeV1& recipe, + std::uint32_t ordinal, std::int64_t x, std::int64_t y, std::int64_t topZ, + std::uint64_t siteSalt) + { + FloatingIslandAnchorV1 island; + island.ordinal = ordinal; + island.stableId = StableId(recipe.seed, siteSalt, 0x49534c414e44ull); + const std::uint32_t radiusRoll = UnitQ16(recipe.seed, siteSalt, 0x524144495553ull); + island.radiusSubvoxels = recipe.minimumRadiusSubvoxels + + (recipe.maximumRadiusSubvoxels - recipe.minimumRadiusSubvoxels) + * radiusRoll / 65535; + island.topHalfSubvoxels = std::max(1, island.radiusSubvoxels / 5); + island.topZSubvoxels = topZ; + island.bottomZSubvoxels = topZ + - island.radiusSubvoxels * recipe.thicknessRatioQ16 / Q16; + island.centerSubvoxels = {x, y, topZ - island.topHalfSubvoxels}; + island.taperEndQ16 = recipe.taperEndMinimumQ16 + + (recipe.taperEndMaximumQ16 - recipe.taperEndMinimumQ16) + * UnitQ16(recipe.seed, siteSalt, 0x5441504552ull) / 65535; + SetIslandBounds(recipe, island); + return island; + } + + inline FloatingIslandConnectorV1 MakeConnector(const FloatingIslandRecipeV1& recipe, + const FloatingIslandAnchorV1& from, const FloatingIslandAnchorV1& to, + std::uint64_t siteSalt) + { + FloatingIslandConnectorV1 edge; + edge.stableId = StableId(recipe.seed, siteSalt, 0x425249444745ull); + edge.fromIslandId = from.stableId; + edge.toIslandId = to.stableId; + const double dx = static_cast(to.centerSubvoxels[0] - from.centerSubvoxels[0]); + const double dy = static_cast(to.centerSubvoxels[1] - from.centerSubvoxels[1]); + const double length = std::hypot(dx, dy); + const double ux = dx / std::max(length, 1.0); + const double uy = dy / std::max(length, 1.0); + // Land each connector well inside the broad top so the full capsule footprint remains + // supported across the bridge-to-island transition, including bounded surface roughness. + // Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord. + const double fromReach = static_cast(from.radiusSubvoxels) * 0.62; + const double toReach = static_cast(to.radiusSubvoxels) * 0.62; + const double sx = from.centerSubvoxels[0] + ux * fromReach; + const double sy = from.centerSubvoxels[1] + uy * fromReach; + const double ex = to.centerSubvoxels[0] - ux * toReach; + const double ey = to.centerSubvoxels[1] - uy * toReach; + const double sz = IslandTopSurface(recipe, from, sx, sy); + const double ez = IslandTopSurface(recipe, to, ex, ey); + edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)}; + edge.endSubvoxels = {RoundQ(ex), RoundQ(ey), RoundQ(ez)}; + edge.halfWidthSubvoxels = recipe.bridgeHalfWidthSubvoxels; + edge.thicknessSubvoxels = recipe.bridgeThicknessSubvoxels; + SetConnectorBounds(recipe, edge); + return edge; + } + + inline std::optional FindSurfaceZ(const FloatingIslandRecipeV1& recipe, + const FloatingIslandPlanV1& plan, std::int64_t x, std::int64_t y) + { + const std::int64_t step = std::max(1, recipe.routeConstraints.sweepSpacingSubvoxels); + std::int64_t aboveZ = recipe.layoutMaximumZSubvoxels + 8 * Q; + std::int64_t aboveDensity = FieldDensity(recipe, plan, x, y, aboveZ); + if (aboveDensity < 0) return std::nullopt; + for (std::int64_t z = aboveZ - step; z >= recipe.layoutMinimumZSubvoxels - 8 * Q; z -= step) + { + const std::int64_t density = FieldDensity(recipe, plan, x, y, z); + if (density < 0) + { + std::int64_t outside = aboveZ, inside = z; + while (outside - inside > 1) + { + const std::int64_t middle = inside + (outside - inside) / 2; + if (FieldDensity(recipe, plan, x, y, middle) < 0) inside = middle; + else outside = middle; + } + return outside; + } + aboveZ = z; + aboveDensity = density; + } + return std::nullopt; + } + + inline std::optional FindCapsuleFootingTopZ( + const FloatingIslandRecipeV1& recipe, const FloatingIslandPlanV1& plan, + std::int64_t x, std::int64_t y) + { + const double radius = static_cast(recipe.routeConstraints.capsuleRadiusSubvoxels); + constexpr std::array, 17> footprint = {{{0.0, 0.0}, + {0.6, 0.0}, {-0.6, 0.0}, {0.0, 0.6}, {0.0, -0.6}, + {0.424264, 0.424264}, {-0.424264, 0.424264}, {0.424264, -0.424264}, {-0.424264, -0.424264}, + {0.95, 0.0}, {-0.95, 0.0}, {0.0, 0.95}, {0.0, -0.95}, + {0.671751, 0.671751}, {-0.671751, 0.671751}, {0.671751, -0.671751}, {-0.671751, -0.671751}}}; + std::int64_t highest = std::numeric_limits::min(); + for (const auto& offset : footprint) + { + const std::int64_t sampleX = RoundQ(static_cast(x) + offset[0] * radius); + const std::int64_t sampleY = RoundQ(static_cast(y) + offset[1] * radius); + const auto surface = FindSurfaceZ(recipe, plan, sampleX, sampleY); + if (!surface) return std::nullopt; + highest = std::max(highest, *surface); + } + return highest; + } + + inline bool ReadConstraints(const VFJson::Value& value, WorldAssemblyRouteConstraintsV1& output) + { + std::uint32_t version = 0, probes = 0; + if (!ReadU32(value.Get("version"), version) + || !ReadInteger(value.Get("capsule_radius_q"), output.capsuleRadiusSubvoxels) + || !ReadInteger(value.Get("capsule_height_q"), output.capsuleHeightSubvoxels) + || !ReadInteger(value.Get("maximum_grade_q16"), output.maximumGradeQ16) + || !ReadInteger(value.Get("sweep_spacing_q"), output.sweepSpacingSubvoxels) + || !ReadInteger(value.Get("support_probe_radius_q"), output.supportProbeRadiusSubvoxels) + || !ReadInteger(value.Get("support_search_depth_q"), output.supportSearchDepthSubvoxels) + || !ReadInteger(value.Get("minimum_clearance_q"), output.minimumClearanceSubvoxels) + || !ReadU32(value.Get("minimum_support_probes"), probes)) return false; + output.version = version; output.minimumSupportProbes = probes; + return true; + } + + inline void WriteConstraints(std::ostream& out, const WorldAssemblyRouteConstraintsV1& value) + { + out << "\"route_constraints\":{\"version\":" << value.version + << ",\"capsule_radius_q\":" << value.capsuleRadiusSubvoxels + << ",\"capsule_height_q\":" << value.capsuleHeightSubvoxels + << ",\"maximum_grade_q16\":" << value.maximumGradeQ16 + << ",\"sweep_spacing_q\":" << value.sweepSpacingSubvoxels + << ",\"support_probe_radius_q\":" << value.supportProbeRadiusSubvoxels + << ",\"support_search_depth_q\":" << value.supportSearchDepthSubvoxels + << ",\"minimum_clearance_q\":" << value.minimumClearanceSubvoxels + << ",\"minimum_support_probes\":" << value.minimumSupportProbes << '}'; + } + + struct RandomStream + { + std::uint64_t state; + std::uint64_t next() + { + state = Mix(state); + return state; + } + }; +} + +std::optional FloatingIslandManifestV1::FromJson( + std::string_view json, std::string& error) +{ + VFJson::Value root; + if (!VFJson::Parser(std::string(json)).Parse(root, error)) return std::nullopt; + FloatingIslandManifestV1 manifest; + std::uint32_t schema = 0; + const auto& recipeValue = root.Get("recipe"); + const auto& planValue = root.Get("plan"); + std::uint64_t manifestHash = 0, recipeHash = 0, planHash = 0; + if (!floating_island_detail::ReadU32(root.Get("schema_version"), schema) + || !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash) + || !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion) + || !floating_island_detail::ReadU32(recipeValue.Get("units_per_voxel"), manifest.recipe.unitsPerVoxel) + || !floating_island_detail::ReadU32(recipeValue.Get("seed"), manifest.recipe.seed) + || !floating_island_detail::ReadInteger(recipeValue.Get("origin_x_q"), manifest.recipe.layoutOriginXSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("origin_y_q"), manifest.recipe.layoutOriginYSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("center_top_z_q"), manifest.recipe.layoutCenterTopZSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("layout_min_z_q"), manifest.recipe.layoutMinimumZSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("layout_max_z_q"), manifest.recipe.layoutMaximumZSubvoxels) + || !floating_island_detail::ReadU32(recipeValue.Get("primary_island_count"), manifest.recipe.primaryIslandCount) + || !floating_island_detail::ReadInteger(recipeValue.Get("island_density_q16"), manifest.recipe.islandDensityQ16) + || !floating_island_detail::ReadInteger(recipeValue.Get("spacing_q"), manifest.recipe.spacingSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("minimum_radius_q"), manifest.recipe.minimumRadiusSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("maximum_radius_q"), manifest.recipe.maximumRadiusSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("thickness_ratio_q16"), manifest.recipe.thicknessRatioQ16) + || !floating_island_detail::ReadInteger(recipeValue.Get("vertical_jitter_q16"), manifest.recipe.verticalJitterQ16) + || !floating_island_detail::ReadInteger(recipeValue.Get("top_flatten_q16"), manifest.recipe.topFlattenQ16) + || !floating_island_detail::ReadInteger(recipeValue.Get("taper_end_min_q16"), manifest.recipe.taperEndMinimumQ16) + || !floating_island_detail::ReadInteger(recipeValue.Get("taper_end_max_q16"), manifest.recipe.taperEndMaximumQ16) + || !floating_island_detail::ReadInteger(recipeValue.Get("blend_radius_q"), manifest.recipe.blendRadiusSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("contour_warp_q"), manifest.recipe.contourWarpSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("roughness_q"), manifest.recipe.roughnessSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("boundary_seal_q"), manifest.recipe.boundarySealSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("bridge_half_width_q"), manifest.recipe.bridgeHalfWidthSubvoxels) + || !floating_island_detail::ReadInteger(recipeValue.Get("bridge_thickness_q"), manifest.recipe.bridgeThicknessSubvoxels) + || !floating_island_detail::ReadHash(recipeValue.Get("content_hash"), recipeHash) + || !floating_island_detail::ReadConstraints(recipeValue.Get("route_constraints"), manifest.recipe.routeConstraints) + || !floating_island_detail::ReadHash(planValue.Get("content_hash"), planHash) + || schema != 1) + { error = "floating-island manifest is missing a required versioned recipe or plan field"; return std::nullopt; } + manifest.recipe.contentHash = recipeHash; + manifest.contentHash = manifestHash; + manifest.plan.contentHash = planHash; + + const auto& islands = planValue.Get("islands").AsArray(); + for (const auto& value : islands) + { + FloatingIslandAnchorV1 island; + if (!floating_island_detail::ReadHash(value.Get("stable_id"), island.stableId) + || !floating_island_detail::ReadU32(value.Get("ordinal"), island.ordinal) + || !floating_island_detail::ReadArray(value.Get("center_q"), island.centerSubvoxels) + || !floating_island_detail::ReadInteger(value.Get("radius_q"), island.radiusSubvoxels) + || !floating_island_detail::ReadInteger(value.Get("top_half_q"), island.topHalfSubvoxels) + || !floating_island_detail::ReadInteger(value.Get("top_z_q"), island.topZSubvoxels) + || !floating_island_detail::ReadInteger(value.Get("bottom_z_q"), island.bottomZSubvoxels) + || !floating_island_detail::ReadInteger(value.Get("taper_end_q16"), island.taperEndQ16) + || !floating_island_detail::ReadArray(value.Get("bounds_min_q"), island.influenceMinimumSubvoxels) + || !floating_island_detail::ReadArray(value.Get("bounds_max_q"), island.influenceMaximumSubvoxels)) + { error = "floating-island manifest contains a malformed seeded anchor"; return std::nullopt; } + manifest.plan.islands.push_back(island); + } + const auto& edges = planValue.Get("cluster_edges").AsArray(); + for (const auto& value : edges) + { + FloatingIslandConnectorV1 edge; + if (!floating_island_detail::ReadHash(value.Get("stable_id"), edge.stableId) + || !floating_island_detail::ReadHash(value.Get("from_island_id"), edge.fromIslandId) + || !floating_island_detail::ReadHash(value.Get("to_island_id"), edge.toIslandId) + || !floating_island_detail::ReadArray(value.Get("start_q"), edge.startSubvoxels) + || !floating_island_detail::ReadArray(value.Get("end_q"), edge.endSubvoxels) + || !floating_island_detail::ReadInteger(value.Get("half_width_q"), edge.halfWidthSubvoxels) + || !floating_island_detail::ReadInteger(value.Get("thickness_q"), edge.thicknessSubvoxels) + || !floating_island_detail::ReadArray(value.Get("bounds_min_q"), edge.influenceMinimumSubvoxels) + || !floating_island_detail::ReadArray(value.Get("bounds_max_q"), edge.influenceMaximumSubvoxels) + || !std::holds_alternative(value.Get("sharp_tag_ready").Data)) + { error = "floating-island manifest contains a malformed graph connector"; return std::nullopt; } + edge.sharpTagReady = value.Get("sharp_tag_ready").Boolean(); + manifest.plan.clusterEdges.push_back(edge); + } + const auto& route = planValue.Get("route").AsArray(); + for (const auto& value : route) + { + WorldAssemblyRoutePointV1 point; + if (!floating_island_detail::ReadHash(value.Get("feature_id"), point.stableFeatureId) + || !floating_island_detail::ReadArray(value.Get("position_q"), point.positionSubvoxels) + || !floating_island_detail::ReadInteger(value.Get("radius_q"), point.radiusSubvoxels)) + { error = "floating-island manifest contains a malformed route waypoint"; return std::nullopt; } + manifest.plan.route.push_back(point); + } + if (!manifest.validate(error)) return std::nullopt; + return manifest; +} + +bool FloatingIslandManifestV1::finalize(std::string& error) +{ + if (!floating_island_detail::ValidateManifest(*this, error)) return false; + recipe.contentHash = floating_island_detail::HashRecipe(recipe); + plan.contentHash = floating_island_detail::HashPlan(plan); + contentHash = floating_island_detail::HashManifest(*this); + return true; +} + +bool FloatingIslandManifestV1::validate(std::string& error) const +{ + FloatingIslandManifestV1 copy = *this; + const std::uint64_t expectedRecipe = recipe.contentHash; + const std::uint64_t expectedPlan = plan.contentHash; + const std::uint64_t expectedManifest = contentHash; + if (!copy.finalize(error)) return false; + if (expectedRecipe != copy.recipe.contentHash || expectedPlan != copy.plan.contentHash + || expectedManifest != copy.contentHash) + { error = "floating-island recipe, cluster plan or manifest hash does not match its data"; return false; } + return true; +} + +std::string FloatingIslandManifestV1::ToJson() const +{ + std::string error; + FloatingIslandManifestV1 value = *this; + if (!value.finalize(error)) return {}; + const auto& recipe = value.recipe; + const auto& plan = value.plan; + std::ostringstream out; + out << "{\"schema_version\":1,\"content_hash\":\"" + << floating_island_detail::HashText(value.contentHash) << "\",\"recipe\":{" + << "\"schema_version\":" << recipe.schemaVersion + << ",\"units_per_voxel\":" << recipe.unitsPerVoxel + << ",\"seed\":" << recipe.seed + << ",\"origin_x_q\":" << recipe.layoutOriginXSubvoxels + << ",\"origin_y_q\":" << recipe.layoutOriginYSubvoxels + << ",\"center_top_z_q\":" << recipe.layoutCenterTopZSubvoxels + << ",\"layout_min_z_q\":" << recipe.layoutMinimumZSubvoxels + << ",\"layout_max_z_q\":" << recipe.layoutMaximumZSubvoxels + << ",\"primary_island_count\":" << recipe.primaryIslandCount + << ",\"island_density_q16\":" << recipe.islandDensityQ16 + << ",\"spacing_q\":" << recipe.spacingSubvoxels + << ",\"minimum_radius_q\":" << recipe.minimumRadiusSubvoxels + << ",\"maximum_radius_q\":" << recipe.maximumRadiusSubvoxels + << ",\"thickness_ratio_q16\":" << recipe.thicknessRatioQ16 + << ",\"vertical_jitter_q16\":" << recipe.verticalJitterQ16 + << ",\"top_flatten_q16\":" << recipe.topFlattenQ16 + << ",\"taper_end_min_q16\":" << recipe.taperEndMinimumQ16 + << ",\"taper_end_max_q16\":" << recipe.taperEndMaximumQ16 + << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels + << ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels + << ",\"roughness_q\":" << recipe.roughnessSubvoxels + << ",\"boundary_seal_q\":" << recipe.boundarySealSubvoxels + << ",\"bridge_half_width_q\":" << recipe.bridgeHalfWidthSubvoxels + << ",\"bridge_thickness_q\":" << recipe.bridgeThicknessSubvoxels << ','; + floating_island_detail::WriteConstraints(out, recipe.routeConstraints); + out << ",\"content_hash\":\"" << floating_island_detail::HashText(recipe.contentHash) + << "\"},\"plan\":{\"content_hash\":\"" + << floating_island_detail::HashText(plan.contentHash) << "\",\"islands\":["; + for (std::size_t i = 0; i < plan.islands.size(); ++i) + { + if (i) out << ','; + const auto& island = plan.islands[i]; + out << "{\"stable_id\":\"" << floating_island_detail::HashText(island.stableId) + << "\",\"ordinal\":" << island.ordinal << ",\"center_q\":[" + << island.centerSubvoxels[0] << ',' << island.centerSubvoxels[1] << ',' << island.centerSubvoxels[2] + << "],\"radius_q\":" << island.radiusSubvoxels + << ",\"top_half_q\":" << island.topHalfSubvoxels + << ",\"top_z_q\":" << island.topZSubvoxels + << ",\"bottom_z_q\":" << island.bottomZSubvoxels + << ",\"taper_end_q16\":" << island.taperEndQ16 + << ",\"bounds_min_q\":[" << island.influenceMinimumSubvoxels[0] << ',' + << island.influenceMinimumSubvoxels[1] << ',' << island.influenceMinimumSubvoxels[2] + << "],\"bounds_max_q\":[" << island.influenceMaximumSubvoxels[0] << ',' + << island.influenceMaximumSubvoxels[1] << ',' << island.influenceMaximumSubvoxels[2] << "]}"; + } + out << "],\"cluster_edges\":["; + for (std::size_t i = 0; i < plan.clusterEdges.size(); ++i) + { + if (i) out << ','; + const auto& edge = plan.clusterEdges[i]; + out << "{\"stable_id\":\"" << floating_island_detail::HashText(edge.stableId) + << "\",\"from_island_id\":\"" << floating_island_detail::HashText(edge.fromIslandId) + << "\",\"to_island_id\":\"" << floating_island_detail::HashText(edge.toIslandId) + << "\",\"start_q\":[" << edge.startSubvoxels[0] << ',' << edge.startSubvoxels[1] << ',' + << edge.startSubvoxels[2] << "],\"end_q\":[" << edge.endSubvoxels[0] << ',' + << edge.endSubvoxels[1] << ',' << edge.endSubvoxels[2] + << "],\"half_width_q\":" << edge.halfWidthSubvoxels + << ",\"thickness_q\":" << edge.thicknessSubvoxels + << ",\"bounds_min_q\":[" << edge.influenceMinimumSubvoxels[0] << ',' + << edge.influenceMinimumSubvoxels[1] << ',' << edge.influenceMinimumSubvoxels[2] + << "],\"bounds_max_q\":[" << edge.influenceMaximumSubvoxels[0] << ',' + << edge.influenceMaximumSubvoxels[1] << ',' << edge.influenceMaximumSubvoxels[2] + << "],\"sharp_tag_ready\":" << (edge.sharpTagReady ? "true" : "false") << '}'; + } + out << "],\"route\":["; + for (std::size_t i = 0; i < plan.route.size(); ++i) + { + if (i) out << ','; + const auto& point = plan.route[i]; + out << "{\"feature_id\":\"" << floating_island_detail::HashText(point.stableFeatureId) + << "\",\"position_q\":[" << point.positionSubvoxels[0] << ',' + << point.positionSubvoxels[1] << ',' << point.positionSubvoxels[2] + << "],\"radius_q\":" << point.radiusSubvoxels << '}'; + } + out << "]}}"; + return out.str(); +} + +std::optional PlanFloatingIslandCluster( + const FloatingIslandRecipeV1& inputRecipe, std::string& error) +{ + FloatingIslandManifestV1 manifest; + manifest.recipe = inputRecipe; + auto& recipe = manifest.recipe; + if (recipe.routeConstraints.version == 0) + { + recipe.routeConstraints.version = 1; + recipe.routeConstraints.capsuleRadiusSubvoxels = 348; + recipe.routeConstraints.capsuleHeightSubvoxels = 1802; + recipe.routeConstraints.maximumGradeQ16 = 17560; + recipe.routeConstraints.sweepSpacingSubvoxels = 128; + recipe.routeConstraints.supportProbeRadiusSubvoxels = 230; + recipe.routeConstraints.supportSearchDepthSubvoxels = 512; + recipe.routeConstraints.minimumClearanceSubvoxels = 128; + recipe.routeConstraints.minimumSupportProbes = 3; + } + if (!floating_island_detail::ValidateRecipe(recipe, error)) return std::nullopt; + + auto& islands = manifest.plan.islands; + islands.reserve(static_cast(recipe.primaryIslandCount) + 2); + const std::uint32_t count = recipe.primaryIslandCount; + const std::uint32_t middle = count / 2; + std::vector tops(count, recipe.layoutCenterTopZSubvoxels); + std::vector radii(count); + for (std::uint32_t i = 0; i < count; ++i) + { + const std::uint64_t site = 0x100000000ull + i; + const std::uint32_t radiusRoll = floating_island_detail::UnitQ16(recipe.seed, site, 0x524144495553ull); + radii[i] = recipe.minimumRadiusSubvoxels + + (recipe.maximumRadiusSubvoxels - recipe.minimumRadiusSubvoxels) * radiusRoll / 65535; + } + const double maxGrade = floating_island_detail::Q16ToDouble(recipe.routeConstraints.maximumGradeQ16); + const double levelFactor = floating_island_detail::Q16ToDouble(recipe.verticalJitterQ16) * 1.5; + const auto riseLimit = [&](std::uint32_t a, std::uint32_t b) + { + const double gap = static_cast(recipe.spacingSubvoxels) + - 0.78 * (static_cast(radii[a]) + static_cast(radii[b])); + return std::max(0.0, gap * maxGrade * levelFactor); + }; + for (std::uint32_t i = middle + 1; i < count; ++i) + { + const auto site = 0x100000000ull + i; + tops[i] = tops[i - 1] + floating_island_detail::SignedRange(recipe.seed, site, + 0x4c4556454cull, static_cast(std::ceil(riseLimit(i - 1, i)))); + } + for (std::int32_t i = static_cast(middle) - 1; i >= 0; --i) + { + const auto site = 0x100000000ull + static_cast(i); + tops[static_cast(i)] = tops[static_cast(i + 1)] + + floating_island_detail::SignedRange(recipe.seed, site, 0x4c4556454cull, + static_cast(std::ceil(riseLimit(static_cast(i), + static_cast(i + 1))))); + } + const std::int64_t rowJitter = recipe.spacingSubvoxels / 10; + const std::int64_t startX = recipe.layoutOriginXSubvoxels + - static_cast(middle) * recipe.spacingSubvoxels; + for (std::uint32_t i = 0; i < count; ++i) + { + const std::uint64_t site = 0x100000000ull + i; + const std::int64_t yOffset = i == middle ? 0 + : floating_island_detail::SignedRange(recipe.seed, site, 0x594a4954544552ull, rowJitter); + auto island = floating_island_detail::MakeIsland(recipe, i, + startX + static_cast(i) * recipe.spacingSubvoxels, + recipe.layoutOriginYSubvoxels + yOffset, tops[i], site); + island.radiusSubvoxels = radii[i]; + island.topHalfSubvoxels = std::max(1, island.radiusSubvoxels / 5); + island.centerSubvoxels[2] = island.topZSubvoxels - island.topHalfSubvoxels; + island.bottomZSubvoxels = island.topZSubvoxels + - island.radiusSubvoxels * recipe.thicknessRatioQ16 / floating_island_detail::Q16; + floating_island_detail::SetIslandBounds(recipe, island); + islands.push_back(island); + } + + auto& edges = manifest.plan.clusterEdges; + for (std::uint32_t i = 0; i + 1 < count; ++i) + edges.push_back(floating_island_detail::MakeConnector(recipe, islands[i], islands[i + 1], + 0x200000000ull + i)); + + // Two optional side sites make density a real recipe parameter. The primary chain is never + // probabilistic, so a sparse roll cannot remove the guaranteed entrance-to-exit path. + // Deux sites lateraux dependent de la densite; la chaine principale reste toujours garantie. + for (std::uint32_t branch = 0; branch < 2; ++branch) + { + const std::uint64_t site = 0x300000000ull + branch; + if (floating_island_detail::UnitQ16(recipe.seed, site, 0x53495445524f4c4cull) + >= static_cast(recipe.islandDensityQ16)) continue; + const std::uint32_t hostIndex = branch == 0 ? 0 : count - 1; + const auto& host = islands[hostIndex]; + const std::int64_t yOffset = branch == 0 ? -recipe.spacingSubvoxels : recipe.spacingSubvoxels; + const std::int64_t gap = recipe.spacingSubvoxels - static_cast( + std::llround(0.78 * static_cast(host.radiusSubvoxels + recipe.minimumRadiusSubvoxels))); + const std::int64_t rise = static_cast(std::ceil(std::max(0.0, + static_cast(gap) * maxGrade * levelFactor))); + const std::int64_t topZ = host.topZSubvoxels + + floating_island_detail::SignedRange(recipe.seed, site, 0x4252414e43484c56ull, rise); + const std::uint32_t radiusRoll = floating_island_detail::UnitQ16(recipe.seed, site, 0x524144495553ull); + const std::int64_t branchRadius = recipe.minimumRadiusSubvoxels + + (recipe.maximumRadiusSubvoxels - recipe.minimumRadiusSubvoxels) * radiusRoll / 65535; + auto island = floating_island_detail::MakeIsland(recipe, + recipe.primaryIslandCount + branch, + host.centerSubvoxels[0], host.centerSubvoxels[1] + yOffset, topZ, site); + island.radiusSubvoxels = branchRadius; + island.topHalfSubvoxels = std::max(1, branchRadius / 5); + island.centerSubvoxels[2] = topZ - island.topHalfSubvoxels; + island.bottomZSubvoxels = topZ - branchRadius * recipe.thicknessRatioQ16 + / floating_island_detail::Q16; + floating_island_detail::SetIslandBounds(recipe, island); + const std::uint64_t islandId = island.stableId; + islands.push_back(island); + edges.push_back(floating_island_detail::MakeConnector(recipe, host, islands.back(), + 0x400000000ull + branch)); + (void)islandId; + } + + FloatingIslandFieldV1 planningField(recipe, manifest.plan); + if (!planningField.isValid()) + { error = "planned floating-island features did not pass the Core field contract"; return std::nullopt; } + auto& route = manifest.plan.route; + const std::int64_t capsuleHalf = recipe.routeConstraints.capsuleHeightSubvoxels / 2; + const std::int64_t routeRadius = 4 * floating_island_detail::Q; + std::uint32_t routeOrdinal = 0; + for (std::uint32_t segment = 0; segment + 1 < count; ++segment) + { + const auto& a = islands[segment]; + const auto& b = islands[segment + 1]; + const double dx = static_cast(b.centerSubvoxels[0] - a.centerSubvoxels[0]); + const double dy = static_cast(b.centerSubvoxels[1] - a.centerSubvoxels[1]); + const std::uint32_t pieces = static_cast(std::max(1.0, + std::ceil(std::hypot(dx, dy) / (4.0 * floating_island_detail::Q)))); + for (std::uint32_t piece = segment == 0 ? 0 : 1; piece <= pieces; ++piece) + { + const double t = static_cast(piece) / pieces; + const std::int64_t x = floating_island_detail::RoundQ( + static_cast(a.centerSubvoxels[0]) + dx * t); + const std::int64_t y = floating_island_detail::RoundQ( + static_cast(a.centerSubvoxels[1]) + dy * t); + const auto surface = floating_island_detail::FindCapsuleFootingTopZ( + recipe, manifest.plan, x, y); + if (!surface) + { error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; } + WorldAssemblyRoutePointV1 point; + point.stableFeatureId = floating_island_detail::StableId(recipe.seed, + 0x500000000ull + routeOrdinal, 0x524f555445ull); + point.positionSubvoxels = {x, y, *surface + capsuleHalf + + recipe.routeConstraints.minimumClearanceSubvoxels}; + point.radiusSubvoxels = routeRadius; + route.push_back(point); + ++routeOrdinal; + } + } + // Find the lowest route profile that stays above every sampled footing surface while obeying + // the grade limit. This raises valleys instead of allowing a short terrain bump to create a + // collision or a grade spike in the serialized walk line. + // Profil minimal au-dessus des appuis : les bosses locales ne percent pas la capsule. + const double maximumGrade = floating_island_detail::Q16ToDouble( + recipe.routeConstraints.maximumGradeQ16) * 0.95; + std::vector baseHeights; + baseHeights.reserve(route.size()); + for (const auto& point : route) baseHeights.push_back(point.positionSubvoxels[2]); + for (std::size_t i = 0; i < route.size(); ++i) + { + double profile = static_cast(baseHeights[i]); + for (std::size_t j = 0; j < route.size(); ++j) + { + const double dx = static_cast(route[i].positionSubvoxels[0] + - route[j].positionSubvoxels[0]); + const double dy = static_cast(route[i].positionSubvoxels[1] + - route[j].positionSubvoxels[1]); + profile = std::max(profile, static_cast(baseHeights[j]) + - maximumGrade * std::hypot(dx, dy)); + } + route[i].positionSubvoxels[2] = floating_island_detail::RoundQ(profile); + } + if (!manifest.finalize(error)) return std::nullopt; + const std::string encoded = manifest.ToJson(); + const auto roundTrip = FloatingIslandManifestV1::FromJson(encoded, error); + if (!roundTrip || roundTrip->ToJson() != encoded || roundTrip->contentHash != manifest.contentHash) + { if (error.empty()) error = "floating-island plan did not canonical-round-trip"; return std::nullopt; } + return manifest; +} + +FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe, + const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan) +{} + +bool FloatingIslandFieldV1::isValid() const noexcept +{ + if (!recipe_ || !plan_ || recipe_->schemaVersion != 1 + || recipe_->unitsPerVoxel != floating_island_detail::Q + || recipe_->primaryIslandCount < 3 || plan_->islands.size() < recipe_->primaryIslandCount + || plan_->islands.size() > recipe_->primaryIslandCount + 2 + || recipe_->minimumRadiusSubvoxels <= 0 || recipe_->maximumRadiusSubvoxels < recipe_->minimumRadiusSubvoxels + || recipe_->blendRadiusSubvoxels < 0 || recipe_->contourWarpSubvoxels < 0) return false; + return std::all_of(plan_->islands.begin(), plan_->islands.end(), [](const auto& island) + { + return island.stableId != 0 && island.radiusSubvoxels > 0 + && island.topZSubvoxels > island.bottomZSubvoxels; + }); +} + +std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x, + std::int64_t y, std::int64_t z) const noexcept +{ + if (!isValid() || !floating_island_detail::ValidScalar(x) + || !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z)) + return floating_island_detail::MaxDensity; + return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z); +} + +FieldInterval FloatingIslandFieldV1::interval(std::int64_t minX, std::int64_t minY, + std::int64_t minZ, std::int64_t maxX, std::int64_t maxY, std::int64_t maxZ) const noexcept +{ + FieldInterval result; + if (!isValid() || minX > maxX || minY > maxY || minZ > maxZ + || !floating_island_detail::ValidScalar(minX) || !floating_island_detail::ValidScalar(minY) + || !floating_island_detail::ValidScalar(minZ) || !floating_island_detail::ValidScalar(maxX) + || !floating_island_detail::ValidScalar(maxY) || !floating_island_detail::ValidScalar(maxZ)) return result; + const std::array minimum{minX, minY, minZ}; + const std::array maximum{maxX, maxY, maxZ}; + for (const auto& island : plan_->islands) + if (floating_island_detail::Intersects(minimum, maximum, + island.influenceMinimumSubvoxels, island.influenceMaximumSubvoxels)) return result; + for (const auto& edge : plan_->clusterEdges) + if (floating_island_detail::Intersects(minimum, maximum, + edge.influenceMinimumSubvoxels, edge.influenceMaximumSubvoxels)) return result; + // Root is void. A box outside every lobe/taper/blend/connector influence bound proves air. + // La racine est le vide : une boite hors des influences bornees prouve l'air. + result.known = true; + result.minimumDensitySubvoxels = 0; + result.maximumDensitySubvoxels = floating_island_detail::MaxDensity; + return result; +} + +FloatingIslandInfluenceBounds FloatingIslandFieldV1::influenceBounds() const noexcept +{ + FloatingIslandInfluenceBounds result; + if (!isValid()) return result; + result.finite = true; + result.minimumSubvoxels.fill(std::numeric_limits::max()); + result.maximumSubvoxels.fill(std::numeric_limits::min()); + const auto include = [&](const auto& feature) + { + for (std::size_t axis = 0; axis < 3; ++axis) + { + result.minimumSubvoxels[axis] = std::min(result.minimumSubvoxels[axis], feature.influenceMinimumSubvoxels[axis]); + result.maximumSubvoxels[axis] = std::max(result.maximumSubvoxels[axis], feature.influenceMaximumSubvoxels[axis]); + } + }; + for (const auto& island : plan_->islands) include(island); + for (const auto& edge : plan_->clusterEdges) include(edge); + return result; +} + +FieldDetailEnvelope FloatingIslandFieldV1::detailEnvelope() const noexcept +{ + FieldDetailEnvelope result; + if (!isValid()) return result; + result.maximumAbsoluteDisplacementSubvoxels = recipe_->contourWarpSubvoxels + + recipe_->roughnessSubvoxels + floating_island_detail::IslandBlendMargin(*recipe_); + result.minimumFeatureSizeSubvoxels = std::numeric_limits::max(); + for (const auto& island : plan_->islands) + result.minimumFeatureSizeSubvoxels = std::min(result.minimumFeatureSizeSubvoxels, + island.radiusSubvoxels * 2); + return result; +} + +FieldBlockContract FloatingIslandFieldV1::contract() const noexcept +{ + FieldBlockContract result; + result.readsChannels = ChannelBit(FieldChannel::island); + result.writesChannels = ChannelBit(FieldChannel::rock) | ChannelBit(FieldChannel::route) + | ChannelBit(FieldChannel::surfaceTags); + result.evaluatorVersion = 1; + return result; +} + +FloatingIslandBoundsAuditResult AuditFloatingIslandBounds(const FloatingIslandFieldV1& field, + std::uint32_t boxCount, std::uint64_t seed) +{ + FloatingIslandBoundsAuditResult result; + if (!field.isValid() || !field.recipe() || !field.plan()) return result; + const auto& recipe = *field.recipe(); + const auto& plan = *field.plan(); + const auto bounds = field.influenceBounds(); + if (!bounds.finite) return result; + for (const auto& island : plan.islands) ++result.islandBoundsTested; + for (const auto& edge : plan.clusterEdges) ++result.connectorBoundsTested; + + floating_island_detail::RandomStream random{seed}; + const auto randomBetween = [&](std::int64_t low, std::int64_t high) + { + if (high <= low) return low; + return low + static_cast(random.next() % static_cast(high - low + 1)); + }; + constexpr std::array samples{0, 1, 2}; + for (std::uint32_t boxIndex = 0; boxIndex < boxCount; ++boxIndex) + { + std::array minimum{}, maximum{}; + for (std::size_t axis = 0; axis < 3; ++axis) + { + const std::int64_t pad = 48 * floating_island_detail::Q; + const std::int64_t low = bounds.minimumSubvoxels[axis] - pad; + const std::int64_t high = bounds.maximumSubvoxels[axis] + pad; + const std::int64_t extent = std::max(1, randomBetween(1, + 20 * floating_island_detail::Q)); + const std::int64_t start = randomBetween(low, high); + minimum[axis] = start; + maximum[axis] = std::min(high + extent, start + extent); + } + const auto interval = field.interval(minimum[0], minimum[1], minimum[2], + maximum[0], maximum[1], maximum[2]); + ++result.boxesTested; + if (!interval.known) ++result.unknownBounds; + for (const auto sx : samples) + for (const auto sy : samples) + for (const auto sz : samples) + { + const std::array point{ + minimum[0] + (maximum[0] - minimum[0]) * sx / 2, + minimum[1] + (maximum[1] - minimum[1]) * sy / 2, + minimum[2] + (maximum[2] - minimum[2]) * sz / 2}; + const std::int64_t density = field.densitySubvoxels(point[0], point[1], point[2]); + ++result.pointSamples; + if (interval.known && (density < interval.minimumDensitySubvoxels + || density > interval.maximumDensitySubvoxels)) ++result.missedBounds; + if (density < 0) + { + bool enclosed = false; + for (const auto& island : plan.islands) + enclosed = enclosed || floating_island_detail::Contains(island.influenceMinimumSubvoxels, + island.influenceMaximumSubvoxels, point); + for (const auto& edge : plan.clusterEdges) + enclosed = enclosed || floating_island_detail::Contains(edge.influenceMinimumSubvoxels, + edge.influenceMaximumSubvoxels, point); + if (!enclosed) ++result.missedBounds; + } + } + } + + // Probe the void beside each primary connector. These boxes should be certifiably air. + for (std::size_t i = 0; i < plan.clusterEdges.size(); ++i) + { + const auto& edge = plan.clusterEdges[i]; + const double dx = static_cast(edge.endSubvoxels[0] - edge.startSubvoxels[0]); + const double dy = static_cast(edge.endSubvoxels[1] - edge.startSubvoxels[1]); + const double length = std::hypot(dx, dy); + if (!(length > 0.0)) continue; + const double offset = static_cast(recipe.maximumRadiusSubvoxels + + recipe.contourWarpSubvoxels + 4 * recipe.blendRadiusSubvoxels + + 3 * floating_island_detail::Q); + const std::int64_t x = floating_island_detail::RoundQ( + (edge.startSubvoxels[0] + edge.endSubvoxels[0]) * 0.5 - dy / length * offset); + const std::int64_t y = floating_island_detail::RoundQ( + (edge.startSubvoxels[1] + edge.endSubvoxels[1]) * 0.5 + dx / length * offset); + const std::int64_t z = (edge.startSubvoxels[2] + edge.endSubvoxels[2]) / 2; + constexpr std::int64_t half = floating_island_detail::Q / 2; + const auto interval = field.interval(x - half, y - half, z - half, + x + half, y + half, z + half); + ++result.boxesTested; + if (!interval.known) { ++result.unknownBounds; continue; } + if (interval.minimumDensitySubvoxels < 0) ++result.missedBounds; + else ++result.voidBoxesProvedAir; + for (const auto sx : samples) + for (const auto sy : samples) + for (const auto sz : samples) + { + const std::int64_t density = field.densitySubvoxels( + x - half + half * sx, y - half + half * sy, z - half + half * sz); + ++result.pointSamples; ++result.voidPointSamples; + if (density < 0 || density < interval.minimumDensitySubvoxels + || density > interval.maximumDensitySubvoxels) ++result.missedBounds; + } + } + + // Negative control: deliberately omit the lobe-warp and blend reach from an island box. + // Controles negatifs : une fausse borne oublie le lobe et le fondu, puis doit etre rejetee. + for (const auto& island : plan.islands) + { + const std::int64_t badReach = island.radiusSubvoxels + recipe.roughnessSubvoxels; + const std::array badMin{island.centerSubvoxels[0] - badReach, + island.centerSubvoxels[1] - badReach, island.bottomZSubvoxels - recipe.roughnessSubvoxels}; + const std::array badMax{island.centerSubvoxels[0] + badReach, + island.centerSubvoxels[1] + badReach, island.topZSubvoxels + recipe.roughnessSubvoxels}; + const double radial = static_cast(island.radiusSubvoxels) + + static_cast(recipe.contourWarpSubvoxels) * 0.72; + const double z = static_cast(island.topZSubvoxels) + - static_cast(island.topHalfSubvoxels) * 0.95; + for (int angleIndex = 0; angleIndex < 512 && !result.negativeControlDetected; ++angleIndex) + { + const double angle = floating_island_detail::Pi * 2.0 * angleIndex / 512.0; + const std::array point{ + floating_island_detail::RoundQ(island.centerSubvoxels[0] + radial * std::cos(angle)), + floating_island_detail::RoundQ(island.centerSubvoxels[1] + radial * std::sin(angle)), + floating_island_detail::RoundQ(z)}; + if (floating_island_detail::Contains(badMin, badMax, point)) continue; + bool hiddenByAnotherFeatureBound = false; + for (const auto& other : plan.islands) + if (other.stableId != island.stableId + && floating_island_detail::Contains(other.influenceMinimumSubvoxels, + other.influenceMaximumSubvoxels, point)) + { hiddenByAnotherFeatureBound = true; break; } + if (!hiddenByAnotherFeatureBound) + for (const auto& edge : plan.clusterEdges) + if (floating_island_detail::Contains(edge.influenceMinimumSubvoxels, + edge.influenceMaximumSubvoxels, point)) + { hiddenByAnotherFeatureBound = true; break; } + if (hiddenByAnotherFeatureBound) continue; + if (field.densitySubvoxels(point[0], point[1], point[2]) < 0) + { + const auto wrongInterval = floating_island_detail::Contains(badMin, badMax, point) + ? FieldInterval{} : FieldInterval{true, 0, floating_island_detail::MaxDensity}; + result.negativeControlDetected = wrongInterval.known + && wrongInterval.minimumDensitySubvoxels >= 0; + } + } + } + result.passed = result.missedBounds == 0 && result.negativeControlDetected + && result.voidBoxesProvedAir > 0; + return result; +} + +FloatingIslandRouteAuditResult AuditFloatingIslandRoute(const FloatingIslandFieldV1& field, + std::int64_t routeOffsetSubvoxels) +{ + FloatingIslandRouteAuditResult result; + if (!field.isValid() || !field.recipe() || !field.plan() || field.plan()->route.size() < 2) return result; + const auto& recipe = *field.recipe(); + const auto& route = field.plan()->route; + const auto& constraints = recipe.routeConstraints; + const double maxGrade = floating_island_detail::Q16ToDouble(constraints.maximumGradeQ16); + const double radius = static_cast(constraints.capsuleRadiusSubvoxels); + const double height = static_cast(constraints.capsuleHeightSubvoxels); + const double halfHeight = height * 0.5; + const double cylinderHalf = std::max(0.0, halfHeight - radius); + const double spacing = static_cast(constraints.sweepSpacingSubvoxels); + double minimumClearance = std::numeric_limits::infinity(); + const std::array, 17> disk = {{ + {0.0, 0.0}, {0.6, 0.0}, {-0.6, 0.0}, {0.0, 0.6}, {0.0, -0.6}, + {0.424264, 0.424264}, {-0.424264, 0.424264}, {0.424264, -0.424264}, {-0.424264, -0.424264}, + {0.95, 0.0}, {-0.95, 0.0}, {0.0, 0.95}, {0.0, -0.95}, + {0.671751, 0.671751}, {-0.671751, 0.671751}, {0.671751, -0.671751}, {-0.671751, -0.671751}}}; + for (std::size_t segment = 0; segment + 1 < route.size(); ++segment) + { + ++result.segmentsTested; + const auto& a = route[segment].positionSubvoxels; + const auto& b = route[segment + 1].positionSubvoxels; + const double dx = static_cast(b[0] - a[0]); + const double dy = static_cast(b[1] - a[1]); + const double dz = static_cast(b[2] - a[2]); + const double horizontal = std::hypot(dx, dy); + if (!(horizontal > 0.0) || std::abs(dz) / horizontal > maxGrade) + { + if (result.firstGradeFailureSegment == ~std::uint64_t{0}) + result.firstGradeFailureSegment = segment; + ++result.gradeFailures; + } + const double length = std::sqrt(dx * dx + dy * dy + dz * dz); + const std::uint64_t centers = std::max(1, + static_cast(std::ceil(length / std::max(spacing, 1.0)))); + for (std::uint64_t center = 0; center <= centers; ++center) + { + const double t = static_cast(center) / centers; + const double cx = static_cast(a[0]) + dx * t; + const double cy = static_cast(a[1]) + dy * t; + const double cz = static_cast(a[2]) + dz * t + routeOffsetSubvoxels; + ++result.centerSamples; + const std::uint32_t verticalSteps = static_cast( + std::max(1.0, std::ceil(height / std::max(spacing, 1.0)))); + for (std::uint32_t vz = 0; vz <= verticalSteps; ++vz) + { + const double offsetZ = -halfHeight + height * vz / verticalSteps; + const double capDistance = std::max(0.0, std::abs(offsetZ) - cylinderHalf); + const double sectionRadius = std::sqrt(std::max(0.0, radius * radius - capDistance * capDistance)); + for (const auto& sample : disk) + { + const double sx = cx + sample[0] * sectionRadius; + const double sy = cy + sample[1] * sectionRadius; + const std::int64_t density = field.densitySubvoxels( + floating_island_detail::RoundQ(sx), floating_island_detail::RoundQ(sy), + floating_island_detail::RoundQ(cz + offsetZ)); + ++result.capsuleFieldSamples; + minimumClearance = std::min(minimumClearance, static_cast(density)); + if (density < -1) + { + if (result.firstBlockedSegment == ~std::uint64_t{0}) + { + result.firstBlockedSegment = segment; + result.firstBlockedPositionSubvoxels = { + floating_island_detail::RoundQ(sx), floating_island_detail::RoundQ(sy), + floating_island_detail::RoundQ(cz + offsetZ)}; + } + ++result.blockedCapsuleSamples; + } + } + } + + const double foot = cz - halfHeight; + const double probeRadius = static_cast(constraints.supportProbeRadiusSubvoxels); + const std::array, 5> probes{{ + {0.0, 0.0}, {probeRadius, 0.0}, {-probeRadius, 0.0}, + {0.0, probeRadius}, {0.0, -probeRadius}}}; + std::uint32_t supported = 0; + for (const auto& probe : probes) + { + bool foundSupport = false; + for (double depth = static_cast(constraints.minimumClearanceSubvoxels) + + 0.5 * floating_island_detail::Q; + depth <= static_cast(constraints.minimumClearanceSubvoxels + + constraints.supportSearchDepthSubvoxels) + 1.0; + depth += 0.5 * floating_island_detail::Q) + { + const std::int64_t density = field.densitySubvoxels( + floating_island_detail::RoundQ(cx + probe[0]), + floating_island_detail::RoundQ(cy + probe[1]), + floating_island_detail::RoundQ(foot - depth)); + ++result.supportProbeSamples; + if (density < 0) { foundSupport = true; break; } + } + if (foundSupport) ++supported; + } + if (supported < constraints.minimumSupportProbes) + { + if (result.firstUnsupportedSegment == ~std::uint64_t{0}) + { + result.firstUnsupportedSegment = segment; + result.firstUnsupportedCenterSubvoxels = { + floating_island_detail::RoundQ(cx), floating_island_detail::RoundQ(cy), + floating_island_detail::RoundQ(cz)}; + } + ++result.unsupportedCenterSamples; + } + } + } + result.minimumClearanceSubvoxels = std::isfinite(minimumClearance) + ? floating_island_detail::RoundQ(minimumClearance) : 0; + result.passed = result.segmentsTested > 0 && result.centerSamples > 0 + && result.blockedCapsuleSamples == 0 && result.unsupportedCenterSamples == 0 + && result.gradeFailures == 0; + return result; +} + +} diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 9c874c7..8eeb2ca 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 "FloatingIslandsInternal.h" #include #include diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md index 19811d0..54d7b78 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md @@ -987,3 +987,43 @@ I also regenerated the five MC doorway failure phases with independently sampled The report has the full metrics, audit logs, skirt captures, source inventory, and failed-run ledger at [WorldAssemblerM2/REPORT.md](../../WorldAssemblerM2/REPORT.md). UBT failed twice while I corrected an undeclared check variable and standalone compatibility indexing, then passed; there was no `0xC0000142` retry, new UECC folder, owner editor/project access, `.uasset` edit, commit, push, or stash. The temporary skirt-export helper was removed; no permanent source file was added. — Luna + +## Luna, M3 round 1 — FloatingIslands standalone Core proof + +Hi Claude, + +The reusable `FloatingIslandRecipeV1` family now lives in Core, with seeded stable island IDs, +cluster planning, lobe-warped outlines, flat-ish tops, tapered closed undersides, finite influence +boxes, sharp-tag-ready connectors, and a planner-reserved supported route. The frozen owner +FloatingIslands strate was the visual oracle. On its matched legacy probe, native deliberately +uses bounded harmonic contour warp instead of legacy query-space 3D fBM, closes the nominal +underside, guarantees a five-island route backbone, and constrains height changes for capsule +grade. The seed produced six islands and five connectors; consequently this local native slice is +denser and more routeable than the sparse stochastic legacy slice. I am not claiming visual parity. + +The final bounds audit passed 517 boxes / 13,932 point samples with zero missed bounds, 108 +intersecting boxes correctly left unknown, four void boxes certified as air, and its shrunken-island +negative control detected. The final composed route passed 1,044 capsule centers / 283,968 field +samples, with no rock collisions, unsupported centers, or grade failures. Raising it 8 voxels caused +all 1,044 support checks to fail, as intended. + +Plan and field output repeat at W1/W8/W16. The recipe, plan, manifest, and field hashes are +`111825edfebea17f`, `65675554b7aa947e`, `03a4685554edce1a`, and `bf3ce7da876431e2`. Existing +M1 round-1 and SurfaceWorld round-4 manifest hashes reparsed unchanged. On the same 32,768-point +workload, legacy took 63.62 ms and native 39.31 ms (0.62× legacy time); the sign mismatch was +2.512% over the sampled, mostly-air region, with 90 legacy-solid versus 885 native-solid samples. + +The CPU-only matched contact sheet includes overview, underside, edge/cliff, tile/LOD reference +planes, and route views; each capture is under 300 KB. The tile/LOD view is a reference-plane +inspection, not a production seam-closure test. No viewer, GPU, owner editor, or owner project build +was used. The Unreal manifest adapter and in-game/cooked route checks remain for later M3 rounds. + +The final standalone audit and Core `Target All` build passed. The report and run ledger are at +[`WorldAssemblerM3/Round1/REPORT.md`](../../WorldAssemblerM3/Round1/REPORT.md), with the +[matched contact sheet](../../WorldAssemblerM3/Round1/results/captures/contact_sheet.jpg). The +report lists all compile/harness corrections and five route iterations; no run crashed, no +`0xC0000142` retry occurred, and no build or test approached the 15-minute limit. Four new source +files are listed there. No `Source/` or `Standalone/Source/` file, owner project file, `.uasset`, +commit, push, or stash was touched. + +— Luna diff --git a/Standalone/Research/WorldAssemblerM3/Round1/BuildFloatingIslandsAudit.ps1 b/Standalone/Research/WorldAssemblerM3/Round1/BuildFloatingIslandsAudit.ps1 new file mode 100644 index 0000000..820bcd3 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round1/BuildFloatingIslandsAudit.ps1 @@ -0,0 +1,118 @@ +[CmdletBinding()] +param( + [string]$ProbePath = (Join-Path $PSScriptRoot '..\..\OwnerShapeWorld\core_strate_coverage\probes\type_6_FloatingIslands.json'), + [string]$OutputDirectory = (Join-Path $PSScriptRoot 'results') +) + +$ErrorActionPreference = 'Stop' +$ScriptProcess = Get-Process -Id $PID +$ScriptProcess.PriorityClass = 'BelowNormal' +$StandaloneRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..')) +$BuildRoot = Join-Path $StandaloneRoot 'Build\VoxelForgeCore' +$CoreLibrary = Join-Path $BuildRoot 'VoxelForgeCore.lib' +$AuditSource = Join-Path $PSScriptRoot 'FloatingIslandsAudit.cpp' +$AuditExe = Join-Path $BuildRoot 'FloatingIslandsAudit.exe' +$ProbePath = [IO.Path]::GetFullPath($ProbePath) +$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) +$VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat' +if (-not (Test-Path -LiteralPath $CoreLibrary -PathType Leaf)) { throw "Core library is missing: $CoreLibrary" } +if (-not (Test-Path -LiteralPath $ProbePath -PathType Leaf)) { throw "Legacy probe is missing: $ProbePath" } +if (-not (Test-Path -LiteralPath $VcVars -PathType Leaf)) { throw "MSVC environment script is missing: $VcVars" } +[IO.Directory]::CreateDirectory($OutputDirectory) | Out-Null + +$IncludeArgs = @( + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Core\include')), + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Core\src')), + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Compat')), + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Source')), + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Source\VoxelForge\Public')), + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Source\VoxelForge\Private')) +) +$LegacyObjectNames = @( + 'UnrealPerlin.obj','StandaloneDataAssetGlue.obj','VoxelBiomeDefinition.obj','VoxelCaveMorphology.obj', + 'VoxelDensityAblation.obj','VoxelDensityOpStack.obj','VoxelDensityProfile.obj','VoxelDiffLayer.obj', + 'VoxelGenerator.obj','VoxelHeightOpStack.obj','VoxelMarchingCubesMesher.obj','VoxelSettings.obj', + 'VoxelStrateComposer.obj','VoxelStrateManager.obj','VoxelStrateMeasure.obj','VoxelTerrainOpDefinition.obj', + 'VoxelWormField.obj','GpuFarViewerLinkStubs.obj' +) +foreach ($ObjectName in $LegacyObjectNames) { + if (-not (Test-Path -LiteralPath (Join-Path $BuildRoot $ObjectName) -PathType Leaf)) { + throw "Required legacy bridge object is missing: $ObjectName; run Standalone/Build.ps1 -Target All first." + } +} + +$ResponsePath = Join-Path $BuildRoot 'floating-islands-audit.rsp' +$ResponseLines = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc','/D_CRT_SECURE_NO_WARNINGS') + + $IncludeArgs + @(('/Fe"{0}"' -f $AuditExe),('"{0}"' -f $AuditSource)) + + @($LegacyObjectNames | ForEach-Object { '"{0}"' -f $_ }) + @( + ('"{0}"' -f $CoreLibrary),'/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib' + ) +[IO.File]::WriteAllLines($ResponsePath,$ResponseLines,[Text.UTF8Encoding]::new($false)) +Push-Location $BuildRoot +try { + $CompileCommand = 'call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$ResponsePath + & $env:ComSpec /d /s /c $CompileCommand + if ($LASTEXITCODE -eq -1073741502) { throw 'Audit build exited with 0xC0000142; retry once using this same wrapper.' } + if ($LASTEXITCODE -ne 0) { throw "Floating-island audit build failed with exit code $LASTEXITCODE" } + if (-not (Test-Path -LiteralPath $AuditExe -PathType Leaf)) { throw "Audit executable was not produced: $AuditExe" } +} finally { Pop-Location } + +$RunLedger = Join-Path $OutputDirectory 'run_ledger.txt' +$RunRecords = [Collections.Generic.List[string]]::new() +$Succeeded = $false +for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) { + $StartInfo = [Diagnostics.ProcessStartInfo]::new() + $StartInfo.FileName = $AuditExe + $StartInfo.Arguments = '"{0}" "{1}"' -f $ProbePath.Replace('"','\"'),$OutputDirectory.Replace('"','\"') + $StartInfo.WorkingDirectory = $BuildRoot + $StartInfo.UseShellExecute = $false + $StartInfo.CreateNoWindow = $true + $StartInfo.RedirectStandardOutput = $true + $StartInfo.RedirectStandardError = $true + $Process = [Diagnostics.Process]::new() + $Process.StartInfo = $StartInfo + [void]$Process.Start() + try { $Process.PriorityClass = 'BelowNormal' } catch { } + $StdoutTask = $Process.StandardOutput.ReadToEndAsync() + $StderrTask = $Process.StandardError.ReadToEndAsync() + $Clock = [Diagnostics.Stopwatch]::StartNew() + [int64]$PeakPrivateBytes = 0 + $LimitFailure = $null + while (-not $Process.WaitForExit(250)) { + $Process.Refresh() + $PeakPrivateBytes = [Math]::Max($PeakPrivateBytes,$Process.PrivateMemorySize64) + if ($PeakPrivateBytes -gt 8GB) { + $LimitFailure = 'process private bytes exceeded 8 GiB' + $Process.Kill() + break + } + if ($Clock.Elapsed.TotalMinutes -ge 15.0) { + $LimitFailure = 'audit test exceeded the 15-minute time box' + $Process.Kill() + break + } + } + $Process.WaitForExit() + $Clock.Stop() + $Process.Refresh() + $PeakPrivateBytes = [Math]::Max($PeakPrivateBytes,$Process.PrivateMemorySize64) + $Stdout = $StdoutTask.GetAwaiter().GetResult() + $Stderr = $StderrTask.GetAwaiter().GetResult() + if ($Stdout) { Write-Output $Stdout.TrimEnd() } + if ($Stderr) { [Console]::Error.WriteLine($Stderr.TrimEnd()) } + $ExitCode = $Process.ExitCode + $Status = if ($LimitFailure) { $LimitFailure } elseif ($ExitCode -eq 0) { 'passed' } else { "failed exit=$ExitCode" } + $RunRecords.Add("attempt=$Attempt status=$Status elapsed_s=$([Math]::Round($Clock.Elapsed.TotalSeconds,2)) peak_private_MiB=$([Math]::Round($PeakPrivateBytes/1MB,1)) exit=$ExitCode") + if ($LimitFailure) { break } + if ($ExitCode -eq 0) { $Succeeded = $true; break } + if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) { + $RunRecords.Add('retry_reason=0xC0000142 single permitted retry') + continue + } + break +} +[IO.File]::WriteAllLines($RunLedger,$RunRecords,[Text.UTF8Encoding]::new($false)) +Get-Content -LiteralPath $RunLedger | Write-Output +if (-not $Succeeded) { throw "Floating-island audit run failed or exceeded a process limit. See $RunLedger" } +Write-Output "audit_executable=$AuditExe" +Write-Output "audit_results=$OutputDirectory" diff --git a/Standalone/Research/WorldAssemblerM3/Round1/FloatingIslandsAudit.cpp b/Standalone/Research/WorldAssemblerM3/Round1/FloatingIslandsAudit.cpp new file mode 100644 index 0000000..23d3a02 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round1/FloatingIslandsAudit.cpp @@ -0,0 +1,728 @@ +#include "CoreMinimal.h" +#include "OwnerWorldLoader.h" +#include "VoxelForgeCore/VoxelForgeCore.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifdef TEXT +#undef TEXT +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#ifdef TEXT +#undef TEXT +#endif +#endif + +namespace +{ + using namespace VoxelForgeCore; + constexpr std::int64_t Q = WorldAssemblySubvoxelsPerVoxel; + using Clock = std::chrono::steady_clock; + using Point = std::array; + + struct OracleAnchor + { + Point surfaceQ{}; + std::uint32_t supportScore = 0; + }; + + struct SampleStats + { + std::string hash; + double legacyMilliseconds = 0.0; + double nativeMilliseconds = 0.0; + std::uint64_t signMismatches = 0; + std::uint64_t legacySolidSamples = 0; + std::uint64_t nativeSolidSamples = 0; + std::uint64_t legacyOnlySolidSamples = 0; + std::uint64_t nativeOnlySolidSamples = 0; + double legacyChecksum = 0.0; + double nativeChecksum = 0.0; + }; + + struct Triangle + { + std::array values{}; // Three xyz vertices followed by one flat normal. + }; + + struct MeshResult + { + std::uint64_t gridSamples = 0; + std::uint64_t triangles = 0; + std::uint64_t bytes = 0; + }; + + struct ExistingHashes + { + std::string round1Manifest; + std::string surfaceWorldRound4Manifest; + }; + + std::uint64_t Mix(std::uint64_t value) + { + value += 0x9e3779b97f4a7c15ull; + value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ull; + value = (value ^ (value >> 27)) * 0x94d049bb133111ebull; + return value ^ (value >> 31); + } + + std::uint64_t Next(std::uint64_t& state) + { + state += 0x9e3779b97f4a7c15ull; + return Mix(state); + } + + std::int64_t VoxQ(double value) + { return static_cast(std::llround(value * static_cast(Q))); } + + std::string Hex64(std::uint64_t value) + { + std::ostringstream output; + output << std::hex << std::setfill('0') << std::setw(16) << value; + return output.str(); + } + + std::string HashValues(const std::vector& values) + { + std::uint64_t hash = 14695981039346656037ull; + for (const auto value : values) + for (int byte = 0; byte < 8; ++byte) + { + hash ^= static_cast(static_cast(value) >> (byte * 8)); + hash *= 1099511628211ull; + } + return Hex64(hash); + } + + ExistingHashes VerifyExistingManifests(const std::filesystem::path& standaloneRoot) + { + const auto read = [](const std::filesystem::path& path) + { + std::ifstream input(path, std::ios::binary); + if (!input) throw std::runtime_error("cannot read existing manifest: " + path.string()); + return std::string(std::istreambuf_iterator(input), std::istreambuf_iterator()); + }; + ExistingHashes hashes; + std::string error; + const auto round1 = WorldAssemblyManifest::FromJson(read( + standaloneRoot / "Research/WorldAssemblerM1/round1-manifest.json"), error); + if (!round1 || !round1->validate(error) + || Hex64(round1->contentHash) != "c87ae279b0e26f53") + throw std::runtime_error("existing M1 manifest hash changed or failed validation: " + error); + hashes.round1Manifest = Hex64(round1->contentHash); + error.clear(); + const auto surface = WorldAssemblyManifest::FromJson(read( + standaloneRoot / "Research/WorldAssemblerM1/round4/world_assembly.json"), error); + if (!surface || !surface->validate(error) + || Hex64(surface->contentHash) != "8933693d9560ba9f") + throw std::runtime_error("existing SurfaceWorld round-4 hash changed or failed validation: " + error); + hashes.surfaceWorldRound4Manifest = Hex64(surface->contentHash); + return hashes; + } + + float LegacyDensity(const FStandaloneOwnerWorld& owner, const Point& q) + { + return owner.Generator->GetDensityAt( + static_cast(q[0] / static_cast(Q)), + static_cast(q[1] / static_cast(Q)), + static_cast(q[2] / static_cast(Q))); + } + + OracleAnchor FindLegacyAnchor(const FStandaloneOwnerWorld& owner) + { + OracleAnchor best; + const float highZ = -560.0f; + const float lowZ = -2480.0f; + constexpr float ScanStep = 8.0f; + for (int y = -448; y <= 448; y += 16) + for (int x = -448; x <= 448; x += 16) + { + float airZ = highZ; + float above = owner.Generator->GetDensityAt(static_cast(x), + static_cast(y), airZ); + if (above < 0.0f) continue; + for (float solidZ = highZ - ScanStep; solidZ >= lowZ; solidZ -= ScanStep) + { + const float density = owner.Generator->GetDensityAt(static_cast(x), + static_cast(y), solidZ); + if (density < 0.0f) + { + float outside = airZ, inside = solidZ; + for (int iteration = 0; iteration < 12; ++iteration) + { + const float middle = (outside + inside) * 0.5f; + if (owner.Generator->GetDensityAt(static_cast(x), + static_cast(y), middle) < 0.0f) inside = middle; + else outside = middle; + } + const std::int64_t topQ = VoxQ(outside); + std::uint32_t score = 0; + for (int dy = -16; dy <= 16; dy += 8) + for (int dx = -16; dx <= 16; dx += 8) + if (owner.Generator->GetDensityAt(static_cast(x + dx), + static_cast(y + dy), + static_cast((topQ - 4 * Q) / static_cast(Q))) < 0.0f) + ++score; + if (score > best.supportScore + || (score == best.supportScore + && std::abs(x) + std::abs(y) + < std::abs(best.surfaceQ[0] / Q) + std::abs(best.surfaceQ[1] / Q))) + { + best.surfaceQ = {static_cast(x) * Q, + static_cast(y) * Q, topQ}; + best.supportScore = score; + } + break; // The first air-to-solid crossing is the island top. + } + airZ = solidZ; + } + } + return best; + } + + std::vector MakeSamples(const FloatingIslandInfluenceBounds& bounds, + std::size_t count, std::uint64_t seed) + { + std::vector points(count); + std::uint64_t state = seed; + for (auto& point : points) + for (std::size_t axis = 0; axis < 3; ++axis) + { + const std::uint64_t span = static_cast( + bounds.maximumSubvoxels[axis] - bounds.minimumSubvoxels[axis] + 1); + point[axis] = bounds.minimumSubvoxels[axis] + + static_cast(Next(state) % std::max(span, 1)); + } + return points; + } + + SampleStats HashWithWorkers(const FloatingIslandFieldV1& field, + const FStandaloneOwnerWorld& owner, const std::vector& points, + std::uint32_t workers, bool measure) + { + std::vector values(points.size()); + std::vector threads; + threads.reserve(workers); + const auto evaluate = [&](std::uint32_t worker) + { + for (std::size_t i = worker; i < points.size(); i += workers) + values[i] = field.densitySubvoxels(points[i][0], points[i][1], points[i][2]); + }; + for (std::uint32_t worker = 0; worker < workers; ++worker) + threads.emplace_back(evaluate, worker); + for (auto& thread : threads) thread.join(); + + SampleStats stats; + stats.hash = HashValues(values); + if (!measure) return stats; + std::vector legacyValues(points.size()); + const auto beginLegacy = Clock::now(); + double legacyChecksum = 0.0; + for (std::size_t i = 0; i < points.size(); ++i) + { + const float value = LegacyDensity(owner, points[i]); + legacyValues[i] = value; + legacyChecksum += value; + } + const auto endLegacy = Clock::now(); + const auto beginNative = Clock::now(); + double nativeChecksum = 0.0; + for (const Point& point : points) + nativeChecksum += static_cast(field.densitySubvoxels( + point[0], point[1], point[2])) / static_cast(Q); + const auto endNative = Clock::now(); + for (std::size_t i = 0; i < values.size(); ++i) + { + const bool legacySolid = legacyValues[i] < 0.0f; + const bool nativeSolid = values[i] < 0; + if (legacySolid) ++stats.legacySolidSamples; + if (nativeSolid) ++stats.nativeSolidSamples; + if (legacySolid && !nativeSolid) ++stats.legacyOnlySolidSamples; + if (nativeSolid && !legacySolid) ++stats.nativeOnlySolidSamples; + if (legacySolid != nativeSolid) ++stats.signMismatches; + } + stats.legacyMilliseconds = std::chrono::duration(endLegacy - beginLegacy).count(); + stats.nativeMilliseconds = std::chrono::duration(endNative - beginNative).count(); + stats.legacyChecksum = legacyChecksum; + stats.nativeChecksum = nativeChecksum; + return stats; + } + + using D3 = std::array; + D3 Add(D3 a, D3 b) { return {a[0] + b[0], a[1] + b[1], a[2] + b[2]}; } + D3 Sub(D3 a, D3 b) { return {a[0] - b[0], a[1] - b[1], a[2] - b[2]}; } + D3 Mul(D3 a, double s) { return {a[0] * s, a[1] * s, a[2] * s}; } + double Dot(D3 a, D3 b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } + D3 Cross(D3 a, D3 b) + { return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]}; } + double Length(D3 a) { return std::sqrt(Dot(a, a)); } + D3 Normalize(D3 a) + { + const double length = Length(a); + return length > 1e-12 ? Mul(a, 1.0 / length) : D3{0.0, 0.0, 1.0}; + } + + double SampleNative(const FloatingIslandFieldV1& field, const Point& q) + { return static_cast(field.densitySubvoxels(q[0], q[1], q[2])) / static_cast(Q); } + + double SampleLegacy(const FStandaloneOwnerWorld& owner, const Point& q) + { return static_cast(LegacyDensity(owner, q)); } + + double TetGradient(const std::array& points, const std::array& values, D3& output) + { + const D3 a = Sub(points[1], points[0]); + const D3 b = Sub(points[2], points[0]); + const D3 c = Sub(points[3], points[0]); + const D3 bc = Cross(b, c), ca = Cross(c, a), ab = Cross(a, b); + const double determinant = Dot(a, bc); + if (std::abs(determinant) < 1e-12) { output = {0.0, 0.0, 1.0}; return 0.0; } + output = Mul(Add(Add(Mul(bc, values[1] - values[0]), + Mul(ca, values[2] - values[0])), Mul(ab, values[3] - values[0])), 1.0 / determinant); + return Length(output); + } + + MeshResult ExtractMesh(const std::filesystem::path& outputPath, + Point minimumQ, Point maximumQ, std::int64_t stepQ, + const std::function& density) + { + const std::size_t nx = static_cast((maximumQ[0] - minimumQ[0]) / stepQ + 1); + const std::size_t ny = static_cast((maximumQ[1] - minimumQ[1]) / stepQ + 1); + const std::size_t nz = static_cast((maximumQ[2] - minimumQ[2]) / stepQ + 1); + const std::size_t gridCount = nx * ny * nz; + if (gridCount > 4'000'000) throw std::runtime_error("mesh grid exceeds the 4M-sample cap"); + std::vector grid(gridCount); + const auto gridIndex = [=](std::size_t x, std::size_t y, std::size_t z) + { return (z * ny + y) * nx + x; }; + for (std::size_t z = 0; z < nz; ++z) + for (std::size_t y = 0; y < ny; ++y) + for (std::size_t x = 0; x < nx; ++x) + { + const Point q{minimumQ[0] + static_cast(x) * stepQ, + minimumQ[1] + static_cast(y) * stepQ, + minimumQ[2] + static_cast(z) * stepQ}; + grid[gridIndex(x, y, z)] = static_cast(density(q)); + } + + constexpr std::array, 8> Corner = {{{0,0,0},{1,0,0},{1,1,0},{0,1,0}, + {0,0,1},{1,0,1},{1,1,1},{0,1,1}}}; + constexpr std::array, 6> Tets = {{{0,5,1,6},{0,1,2,6},{0,2,3,6}, + {0,3,7,6},{0,7,4,6},{0,4,5,6}}}; + constexpr std::array, 6> TetEdges = {{{0,1},{0,2},{0,3},{1,2},{1,3},{2,3}}}; + std::vector triangles; + triangles.reserve(8192); + for (std::size_t z = 0; z + 1 < nz; ++z) + for (std::size_t y = 0; y + 1 < ny; ++y) + for (std::size_t x = 0; x + 1 < nx; ++x) + { + std::array cubePoints{}; + std::array cubeValues{}; + double low = std::numeric_limits::infinity(); + double high = -std::numeric_limits::infinity(); + for (std::size_t corner = 0; corner < Corner.size(); ++corner) + { + const auto& offset = Corner[corner]; + cubePoints[corner] = { + static_cast(minimumQ[0] + static_cast(x + offset[0]) * stepQ) / Q, + static_cast(minimumQ[1] + static_cast(y + offset[1]) * stepQ) / Q, + static_cast(minimumQ[2] + static_cast(z + offset[2]) * stepQ) / Q}; + cubeValues[corner] = grid[gridIndex(x + offset[0], y + offset[1], z + offset[2])]; + low = std::min(low, cubeValues[corner]); + high = std::max(high, cubeValues[corner]); + } + if (low >= 0.0 || high < 0.0) continue; + + for (const auto& tet : Tets) + { + std::array points{}; + std::array values{}; + for (std::size_t i = 0; i < 4; ++i) + { points[i] = cubePoints[tet[i]]; values[i] = cubeValues[tet[i]]; } + std::array polygon{}; + std::size_t polygonCount = 0; + for (const auto& edge : TetEdges) + { + const int ia = edge[0], ib = edge[1]; + if ((values[ia] < 0.0) == (values[ib] < 0.0)) continue; + const double t = values[ia] / (values[ia] - values[ib]); + const D3 point = Add(points[ia], Mul(Sub(points[ib], points[ia]), t)); + bool duplicate = false; + for (std::size_t i = 0; i < polygonCount; ++i) + if (Length(Sub(polygon[i], point)) < 1e-7) { duplicate = true; break; } + if (!duplicate && polygonCount < polygon.size()) polygon[polygonCount++] = point; + } + if (polygonCount < 3) continue; + D3 normal{}; + TetGradient(points, values, normal); + normal = Normalize(normal); + D3 center{}; + for (std::size_t i = 0; i < polygonCount; ++i) center = Add(center, polygon[i]); + center = Mul(center, 1.0 / polygonCount); + const D3 axis = std::abs(normal[2]) < 0.85 ? D3{0,0,1} : D3{0,1,0}; + const D3 right = Normalize(Cross(axis, normal)); + const D3 up = Normalize(Cross(normal, right)); + std::array, 4> ordered{}; + for (std::size_t i = 0; i < polygonCount; ++i) + { + const D3 delta = Sub(polygon[i], center); + ordered[i] = {std::atan2(Dot(delta, up), Dot(delta, right)), polygon[i]}; + } + std::sort(ordered.begin(), ordered.begin() + polygonCount, + [](const auto& a, const auto& b) { return a.first < b.first; }); + for (std::size_t i = 1; i + 1 < polygonCount; ++i) + { + D3 a = ordered[0].second, b = ordered[i].second, c = ordered[i + 1].second; + if (Dot(Cross(Sub(b, a), Sub(c, a)), normal) < 0.0) std::swap(b, c); + Triangle triangle; + for (std::size_t axisIndex = 0; axisIndex < 3; ++axisIndex) + { + triangle.values[axisIndex] = static_cast(a[axisIndex]); + triangle.values[3 + axisIndex] = static_cast(b[axisIndex]); + triangle.values[6 + axisIndex] = static_cast(c[axisIndex]); + triangle.values[9 + axisIndex] = static_cast(normal[axisIndex]); + } + triangles.push_back(triangle); + } + } + } + + std::ofstream output(outputPath, std::ios::binary | std::ios::trunc); + if (!output) throw std::runtime_error("cannot create mesh capture: " + outputPath.string()); + const std::uint32_t count = static_cast(triangles.size()); + output.write("VFMI", 4); + output.write(reinterpret_cast(&count), sizeof(count)); + for (const auto& triangle : triangles) + output.write(reinterpret_cast(triangle.values.data()), + static_cast(sizeof(triangle.values))); + output.close(); + MeshResult result; + result.gridSamples = gridCount; + result.triangles = triangles.size(); + result.bytes = std::filesystem::file_size(outputPath); + return result; + } + + void WriteJson(const std::filesystem::path& path, const FloatingIslandManifestV1& manifest, + const FloatingIslandBoundsAuditResult& bounds, const FloatingIslandRouteAuditResult& route, + const FloatingIslandRouteAuditResult& raised, const SampleStats& sample, + const OracleAnchor& anchor, const std::array& meshes, + const std::array& fieldHashes, const ExistingHashes& oldHashes) + { + std::ofstream out(path, std::ios::trunc); + if (!out) throw std::runtime_error("cannot create audit result JSON"); + out << std::setprecision(12); + out << "{\n \"recipe_hash\":\"" << Hex64(manifest.recipe.contentHash) + << "\",\n \"plan_hash\":\"" << Hex64(manifest.plan.contentHash) + << "\",\n \"manifest_hash\":\"" << Hex64(manifest.contentHash) + << "\",\n \"unchanged_existing_hashes\":{\"m1_round1_manifest\":\"" + << oldHashes.round1Manifest << "\",\"surfaceworld_round4_manifest\":\"" + << oldHashes.surfaceWorldRound4Manifest << "\"}" + << ",\n \"plan_islands\":" << manifest.plan.islands.size() + << ",\n \"plan_connectors\":" << manifest.plan.clusterEdges.size() + << ",\n \"route_points\":" << manifest.plan.route.size() + << ",\n \"legacy_anchor_voxels\":[" << anchor.surfaceQ[0] / static_cast(Q) + << ',' << anchor.surfaceQ[1] / static_cast(Q) << ',' + << anchor.surfaceQ[2] / static_cast(Q) << "],\n \"legacy_anchor_plateau_samples\":" + << anchor.supportScore << ",\n \"recipe_parameters\":{\"seed\":" << manifest.recipe.seed + << ",\"density_q16\":" << manifest.recipe.islandDensityQ16 + << ",\"spacing_voxels\":" << manifest.recipe.spacingSubvoxels / static_cast(Q) + << ",\"radius_voxels\":[" << manifest.recipe.minimumRadiusSubvoxels / static_cast(Q) + << ',' << manifest.recipe.maximumRadiusSubvoxels / static_cast(Q) + << "],\"thickness_ratio_q16\":" << manifest.recipe.thicknessRatioQ16 + << ",\"vertical_jitter_q16\":" << manifest.recipe.verticalJitterQ16 + << ",\"top_flatten_q16\":" << manifest.recipe.topFlattenQ16 + << ",\"contour_warp_voxels\":" << manifest.recipe.contourWarpSubvoxels / static_cast(Q) + << ",\"roughness_voxels\":" << manifest.recipe.roughnessSubvoxels / static_cast(Q) + << ",\"blend_voxels\":" << manifest.recipe.blendRadiusSubvoxels / static_cast(Q) << "},\n \"stable_island_ids\":["; + for (std::size_t i = 0; i < manifest.plan.islands.size(); ++i) + { if (i) out << ','; out << '\"' << Hex64(manifest.plan.islands[i].stableId) << '\"'; } + out << "],\n \"stable_connector_ids\":["; + for (std::size_t i = 0; i < manifest.plan.clusterEdges.size(); ++i) + { if (i) out << ','; out << '\"' << Hex64(manifest.plan.clusterEdges[i].stableId) << '\"'; } + out << "],\n \"bounds\":{\"passed\":" << (bounds.passed ? "true" : "false") + << ",\"negative_control_detected\":" << (bounds.negativeControlDetected ? "true" : "false") + << ",\"boxes\":" << bounds.boxesTested << ",\"samples\":" << bounds.pointSamples + << ",\"unknown\":" << bounds.unknownBounds << ",\"missed\":" << bounds.missedBounds + << ",\"island_boxes\":" << bounds.islandBoundsTested + << ",\"connector_boxes\":" << bounds.connectorBoundsTested + << ",\"void_boxes_proved_air\":" << bounds.voidBoxesProvedAir << "},\n \"route\":{\"passed\":" + << (route.passed ? "true" : "false") << ",\"segments\":" << route.segmentsTested + << ",\"centers\":" << route.centerSamples << ",\"capsule_samples\":" + << route.capsuleFieldSamples << ",\"blocked\":" << route.blockedCapsuleSamples + << ",\"support_samples\":" << route.supportProbeSamples << ",\"unsupported\":" + << route.unsupportedCenterSamples << ",\"grade_failures\":" << route.gradeFailures + << ",\"minimum_clearance_subvoxels\":" << route.minimumClearanceSubvoxels + << "},\n \"raised_route_negative_control\":{\"detected\":" + << ((!raised.passed && (raised.unsupportedCenterSamples || raised.blockedCapsuleSamples)) ? "true" : "false") + << ",\"passed\":" << (raised.passed ? "true" : "false") + << ",\"unsupported\":" << raised.unsupportedCenterSamples + << ",\"blocked\":" << raised.blockedCapsuleSamples << "},\n \"determinism\":{\"W1\":\"" + << fieldHashes[0] << "\",\"W8\":\"" << fieldHashes[1] << "\",\"W16\":\"" + << fieldHashes[2] << "\",\"plan_repeat\":true,\"hash_samples\":32768},\n \"timing\":{\"same_queries\":32768,\"legacy_ms\":" + << sample.legacyMilliseconds << ",\"native_ms\":" << sample.nativeMilliseconds + << ",\"native_over_legacy\":" << (sample.nativeMilliseconds / std::max(sample.legacyMilliseconds, 1e-9)) + << ",\"sign_mismatches\":" << sample.signMismatches << ",\"sign_mismatch_percent\":" + << (100.0 * sample.signMismatches / 32768.0) + << ",\"legacy_solid_samples\":" << sample.legacySolidSamples + << ",\"native_solid_samples\":" << sample.nativeSolidSamples + << ",\"legacy_only_solid_samples\":" << sample.legacyOnlySolidSamples + << ",\"native_only_solid_samples\":" << sample.nativeOnlySolidSamples << "},\n \"mesh\":["; + constexpr std::array names{"cluster_legacy", "cluster_native", "detail_legacy", "detail_native"}; + for (std::size_t i = 0; i < meshes.size(); ++i) + { + if (i) out << ','; + out << "{\"name\":\"" << names[i] << "\",\"grid_samples\":" << meshes[i].gridSamples + << ",\"triangles\":" << meshes[i].triangles << ",\"bytes\":" << meshes[i].bytes << '}'; + } + out << "],\n \"route_points_voxels\":["; + for (std::size_t i = 0; i < manifest.plan.route.size(); ++i) + { + if (i) out << ','; + const auto& point = manifest.plan.route[i]; + out << '[' << point.positionSubvoxels[0] / static_cast(Q) << ',' + << point.positionSubvoxels[1] / static_cast(Q) << ',' + << point.positionSubvoxels[2] / static_cast(Q) << ']'; + } + out << "]\n}\n"; + } +} + +int main(int argc, char** argv) +{ +#if defined(_WIN32) + SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS); + SetProcessAffinityMask(GetCurrentProcess(), static_cast(0xFFFF)); +#endif + try + { + if (argc != 3) + { std::cerr << "usage: FloatingIslandsAudit \n"; return 2; } + const std::filesystem::path probePath = argv[1]; + const std::filesystem::path outputDir = argv[2]; + std::filesystem::create_directories(outputDir); + const auto standaloneRoot = probePath.parent_path().parent_path().parent_path() + .parent_path().parent_path(); + const ExistingHashes oldHashes = VerifyExistingManifests(standaloneRoot); + + FStandaloneOwnerWorld owner; + std::string error; + if (!StandaloneOwnerLoader::Load(probePath.string(), owner, error)) + throw std::runtime_error("legacy probe load failed: " + error); + const OracleAnchor anchor = FindLegacyAnchor(owner); + if (anchor.supportScore < 9) + throw std::runtime_error("legacy scan found no broad FloatingIslands top in the frozen strate range"); + + FloatingIslandRecipeV1 recipe; + recipe.seed = 0; + recipe.layoutOriginXSubvoxels = anchor.surfaceQ[0]; + recipe.layoutOriginYSubvoxels = anchor.surfaceQ[1]; + recipe.layoutCenterTopZSubvoxels = anchor.surfaceQ[2]; + recipe.layoutMinimumZSubvoxels = anchor.surfaceQ[2] - 96 * Q; + recipe.layoutMaximumZSubvoxels = anchor.surfaceQ[2] + 96 * Q; + recipe.primaryIslandCount = 5; + recipe.islandDensityQ16 = 32768; + recipe.spacingSubvoxels = 112 * Q; + recipe.minimumRadiusSubvoxels = 24 * Q; + recipe.maximumRadiusSubvoxels = 48 * Q; + recipe.thicknessRatioQ16 = 39322; + recipe.verticalJitterQ16 = 39322; + recipe.topFlattenQ16 = 39322; + recipe.taperEndMinimumQ16 = 29491; + recipe.taperEndMaximumQ16 = 45875; + recipe.blendRadiusSubvoxels = 6 * Q; + recipe.contourWarpSubvoxels = VoxQ((24.0 + 48.0) * 0.35 * 0.5); + recipe.roughnessSubvoxels = 3 * Q; + recipe.boundarySealSubvoxels = 4 * Q; + recipe.bridgeHalfWidthSubvoxels = 4 * Q; + recipe.bridgeThicknessSubvoxels = 3 * Q; + + const auto beginPlanning = Clock::now(); + auto planned = PlanFloatingIslandCluster(recipe, error); + const auto endPlanning = Clock::now(); + if (!planned) throw std::runtime_error("native island planner failed: " + error); + const std::string canonical = planned->ToJson(); + for (int repeat = 0; repeat < 2; ++repeat) + { + std::string repeatError; + const auto repeated = PlanFloatingIslandCluster(recipe, repeatError); + if (!repeated || repeated->contentHash != planned->contentHash + || repeated->plan.contentHash != planned->plan.contentHash + || repeated->ToJson() != canonical) + throw std::runtime_error("repeated native plan hash or canonical JSON changed"); + } + const auto parsed = FloatingIslandManifestV1::FromJson(canonical, error); + if (!parsed || parsed->ToJson() != canonical || !parsed->validate(error)) + throw std::runtime_error("native manifest did not validate after round-trip: " + error); + + FloatingIslandFieldV1 field(planned->recipe, planned->plan); + if (!field.isValid()) throw std::runtime_error("native field rejected the planned island graph"); + const auto bounds = AuditFloatingIslandBounds(field, 512); + const auto route = AuditFloatingIslandRoute(field); + const auto raisedRoute = AuditFloatingIslandRoute(field, 8 * Q); + const auto influence = field.influenceBounds(); + const auto points = MakeSamples(influence, 32768, 0x4d33524f554e4431ull); + const auto w1 = HashWithWorkers(field, owner, points, 1, false); + const auto w8 = HashWithWorkers(field, owner, points, 8, false); + const auto w16 = HashWithWorkers(field, owner, points, 16, true); + const std::array hashes{w1.hash, w8.hash, w16.hash}; + if (w1.hash != w8.hash || w1.hash != w16.hash) + throw std::runtime_error("native field sample hash differs between W1/W8/W16"); + if (!bounds.passed) throw std::runtime_error("native island bounds audit or negative control failed"); + std::cout << "route_diagnostics pass=" << route.passed << " segments=" << route.segmentsTested + << " centers=" << route.centerSamples << " capsule_samples=" << route.capsuleFieldSamples + << " blocked=" << route.blockedCapsuleSamples << " support_samples=" << route.supportProbeSamples + << " unsupported=" << route.unsupportedCenterSamples << " grade_failures=" << route.gradeFailures + << " minimum_clearance_q=" << route.minimumClearanceSubvoxels + << " first_blocked_segment=" << route.firstBlockedSegment + << " first_unsupported_segment=" << route.firstUnsupportedSegment + << " first_grade_segment=" << route.firstGradeFailureSegment + << " unsupported_center_voxels=" << route.firstUnsupportedCenterSubvoxels[0] / static_cast(Q) + << ',' << route.firstUnsupportedCenterSubvoxels[1] / static_cast(Q) + << ',' << route.firstUnsupportedCenterSubvoxels[2] / static_cast(Q) << '\n'; + if (!route.passed) + { + const auto& routePoints = planned->plan.route; + const double maxGrade = static_cast(planned->recipe.routeConstraints.maximumGradeQ16) / 65536.0; + for (std::size_t i = 0; i + 1 < routePoints.size(); ++i) + { + const auto& a = routePoints[i].positionSubvoxels; + const auto& b = routePoints[i + 1].positionSubvoxels; + const double horizontal = std::hypot(static_cast(b[0] - a[0]), + static_cast(b[1] - a[1])); + const double grade = horizontal > 0.0 ? std::abs(static_cast(b[2] - a[2])) / horizontal : 1e9; + if (grade > maxGrade + 1e-7) + std::cout << "grade_segment=" << i << " ratio=" << grade << " limit=" << maxGrade + << " from=" << a[0] / static_cast(Q) << ',' << a[1] / static_cast(Q) + << ',' << a[2] / static_cast(Q) << " to=" << b[0] / static_cast(Q) + << ',' << b[1] / static_cast(Q) << ',' << b[2] / static_cast(Q) << '\n'; + } + for (std::size_t i = 0; i < planned->plan.islands.size(); ++i) + { + const auto& island = planned->plan.islands[i]; + std::cout << "island=" << i << " center=" << island.centerSubvoxels[0] / static_cast(Q) + << ',' << island.centerSubvoxels[1] / static_cast(Q) << ',' + << island.topZSubvoxels / static_cast(Q) << " radius=" + << island.radiusSubvoxels / static_cast(Q) << '\n'; + } + const auto& constraints = planned->recipe.routeConstraints; + const auto& unsupported = route.firstUnsupportedCenterSubvoxels; + const double foot = unsupported[2] / static_cast(Q) + - constraints.capsuleHeightSubvoxels / (2.0 * Q); + const double probeRadius = constraints.supportProbeRadiusSubvoxels / static_cast(Q); + const std::array, 5> supportPoints{{ + {0.0,0.0},{probeRadius,0.0},{-probeRadius,0.0},{0.0,probeRadius},{0.0,-probeRadius}}}; + for (std::size_t probeIndex = 0; probeIndex < supportPoints.size(); ++probeIndex) + { + double firstSolidDepth = -1.0; + for (double depth = (constraints.minimumClearanceSubvoxels + 0.5 * Q) / static_cast(Q); + depth <= (constraints.minimumClearanceSubvoxels + constraints.supportSearchDepthSubvoxels) + / static_cast(Q) + 0.001; depth += 0.5) + { + const auto density = field.densitySubvoxels( + std::llround(unsupported[0] + supportPoints[probeIndex][0] * Q), + std::llround(unsupported[1] + supportPoints[probeIndex][1] * Q), + std::llround((foot - depth) * Q)); + if (density < 0) { firstSolidDepth = depth; break; } + } + std::cout << "support_probe=" << probeIndex << " first_solid_depth_vox=" << firstSolidDepth << '\n'; + } + for (std::size_t i = 0; i < planned->plan.clusterEdges.size(); ++i) + { + const auto& edge = planned->plan.clusterEdges[i]; + std::cout << "connector=" << i << " start=" << edge.startSubvoxels[0] / static_cast(Q) + << ',' << edge.startSubvoxels[1] / static_cast(Q) << ',' + << edge.startSubvoxels[2] / static_cast(Q) << " end=" + << edge.endSubvoxels[0] / static_cast(Q) << ',' + << edge.endSubvoxels[1] / static_cast(Q) << ',' + << edge.endSubvoxels[2] / static_cast(Q) << '\n'; + } + } + if (!route.passed) throw std::runtime_error("native route capsule sweep failed"); + if (raisedRoute.passed || (raisedRoute.unsupportedCenterSamples == 0 + && raisedRoute.blockedCapsuleSamples == 0)) + throw std::runtime_error("raised-route negative control did not detect unsupported traversal"); + + const std::int64_t ox = anchor.surfaceQ[0], oy = anchor.surfaceQ[1], oz = anchor.surfaceQ[2]; + std::array meshes{}; + const Point clusterMin{ox - 320 * Q, oy - 208 * Q, oz - 80 * Q}; + const Point clusterMax{ox + 320 * Q, oy + 208 * Q, oz + 16 * Q}; + const Point detailMin{ox - 64 * Q, oy - 64 * Q, oz - 48 * Q}; + const Point detailMax{ox + 64 * Q, oy + 64 * Q, oz + 10 * Q}; + meshes[0] = ExtractMesh(outputDir / "cluster_legacy.vfmesh", clusterMin, clusterMax, 4 * Q, + [&](const Point& q) { return SampleLegacy(owner, q); }); + meshes[1] = ExtractMesh(outputDir / "cluster_native.vfmesh", clusterMin, clusterMax, 4 * Q, + [&](const Point& q) { return SampleNative(field, q); }); + meshes[2] = ExtractMesh(outputDir / "detail_legacy.vfmesh", detailMin, detailMax, 2 * Q, + [&](const Point& q) { return SampleLegacy(owner, q); }); + meshes[3] = ExtractMesh(outputDir / "detail_native.vfmesh", detailMin, detailMax, 2 * Q, + [&](const Point& q) { return SampleNative(field, q); }); + if (meshes[0].triangles == 0 || meshes[1].triangles == 0 + || meshes[2].triangles == 0 || meshes[3].triangles == 0) + throw std::runtime_error("matched legacy/native mesh extraction produced an empty field"); + + WriteJson(outputDir / "audit.json", *planned, bounds, route, raisedRoute, + w16, anchor, meshes, hashes, oldHashes); + std::cout << std::setprecision(4) + << "legacy_anchor_voxels=" << anchor.surfaceQ[0] / static_cast(Q) << ',' + << anchor.surfaceQ[1] / static_cast(Q) << ',' + << anchor.surfaceQ[2] / static_cast(Q) + << " plateau_samples=" << anchor.supportScore << '\n' + << "recipe_hash=" << Hex64(planned->recipe.contentHash) + << " plan_hash=" << Hex64(planned->plan.contentHash) + << " manifest_hash=" << Hex64(planned->contentHash) + << " prior_m1_hash=" << oldHashes.round1Manifest + << " prior_surfaceworld_hash=" << oldHashes.surfaceWorldRound4Manifest + << " islands=" << planned->plan.islands.size() + << " connectors=" << planned->plan.clusterEdges.size() + << " route_points=" << planned->plan.route.size() + << " planning_ms=" << std::chrono::duration(endPlanning - beginPlanning).count() << '\n' + << "bounds_pass=" << bounds.passed << " boxes=" << bounds.boxesTested + << " point_samples=" << bounds.pointSamples << " unknown=" << bounds.unknownBounds + << " missed=" << bounds.missedBounds << " void_air=" << bounds.voidBoxesProvedAir + << " negative_control=" << bounds.negativeControlDetected << '\n' + << "route_pass=" << route.passed << " segments=" << route.segmentsTested + << " centers=" << route.centerSamples << " capsule_samples=" << route.capsuleFieldSamples + << " blocked=" << route.blockedCapsuleSamples << " support_probes=" << route.supportProbeSamples + << " unsupported=" << route.unsupportedCenterSamples << " grade_failures=" << route.gradeFailures + << " clearance_q=" << route.minimumClearanceSubvoxels << '\n' + << "raised_route_detected=" << (!raisedRoute.passed && raisedRoute.unsupportedCenterSamples > 0) + << " unsupported=" << raisedRoute.unsupportedCenterSamples + << " blocked=" << raisedRoute.blockedCapsuleSamples << '\n' + << "field_hashes W1=" << w1.hash << " W8=" << w8.hash << " W16=" << w16.hash + << " legacy_ms=" << w16.legacyMilliseconds << " native_ms=" << w16.nativeMilliseconds + << " mismatch_percent=" << (100.0 * w16.signMismatches / 32768.0) + << " legacy_solid=" << w16.legacySolidSamples << " native_solid=" << w16.nativeSolidSamples + << " legacy_only=" << w16.legacyOnlySolidSamples << " native_only=" << w16.nativeOnlySolidSamples << '\n' + << "mesh_samples=" << meshes[0].gridSamples << ',' << meshes[1].gridSamples + << ',' << meshes[2].gridSamples << ',' << meshes[3].gridSamples + << " triangles=" << meshes[0].triangles << ',' << meshes[1].triangles + << ',' << meshes[2].triangles << ',' << meshes[3].triangles << '\n' + << "audit_json=" << (outputDir / "audit.json").string() << '\n'; + return 0; + } + catch (const std::exception& exception) + { + std::cerr << "FLOATING_ISLAND_AUDIT_FAIL: " << exception.what() << '\n'; + return 1; + } +} diff --git a/Standalone/Research/WorldAssemblerM3/Round1/REPORT.md b/Standalone/Research/WorldAssemblerM3/Round1/REPORT.md new file mode 100644 index 0000000..448f749 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round1/REPORT.md @@ -0,0 +1,174 @@ +# M3 Round 1 — FloatingIslands standalone Core proof + +**Result:** the new finite island field builds in Core and the standalone legacy bridge. The recipe, +bounds audit, worker-count field hashes and supported route pass for the frozen owner probe. The native +result is intentionally denser and more routeable than the stochastic legacy slice; this is a measured +visual difference, not a parity claim. + +## Native recipe and deliberate differences + +Added `FloatingIslandRecipeV1`, `FloatingIslandManifestV1`, `FloatingIslandFieldV1` and the planner to +Core. The island field is a reusable feature family on the `island` channel; it is not a branch inside +the owner FloatingIslands generator. The standalone manifest carries recipe and plan hashes, seeded +anchors, stable IDs, a connected cluster graph, finite influence boxes, bridge parameters and route +points. Coordinates and density use the existing fixed scale of 1/256 voxel; one Core voxel remains +25 cm and negative density is solid. + +The visual oracle was loaded from +[`type_6_FloatingIslands.json`](../../OwnerShapeWorld/core_strate_coverage/probes/type_6_FloatingIslands.json) +and queried through the legacy `GetDensityAt` evaluator. Its settings are seed 0, spacing 112 voxels, +density 0.5, radii 24–48 voxels, thickness ratio 0.6, vertical jitter 0.6, top flatten 0.6, +roughness 3 voxels, blend radius 6 voxels and boundary seal 4 voxels. The native recipe uses those +shape ranges and aligns its middle island to a detected legacy top at `(48, 16, -1904.035)` voxels. + +The native shape uses a flat-ish top, an explicit finite underside plane, tapered radial support and +a bounded 12.6016-voxel three-harmonic lobe warp. This intentionally replaces the legacy query-space +3D fBM warp with a stable bounded contour function. The legacy source has no explicit lower plane in +its island max-SDF; the native bottom closes the taper instead of carrying the legacy centerline +degeneracy below the nominal island bottom. Native surface roughness remains bounded to the recipe's +3-voxel envelope. + +The planner guarantees five primary route islands. `islandDensityQ16` selects up to two optional side +sites; this seed selected one, producing six islands and five sharp-tag-ready connectors. This differs +from the legacy stochastic density roll, which decides whether every grid site exists. The planner +keeps the route-connected islands in a narrower vertical band than the legacy strate: adjacent height +changes are clamped to the capsule grade contract, with a 5% grade reserve, and the serialized route +is fitted above its sampled capsule footprint. Connectors land 0.62 radii into the island tops, use a +4-voxel half-width and 3-voxel thickness, and extend the field through the landings. This is why the +local native slice contains substantially more rock than the old slice. The fixed-season water block +is not emitted here; player edits remain unrestricted, and this generation audit applies the route +law only to system-generated geometry. + +## Bounds and route audits + +Island AABBs include radius, maximum lobe displacement, taper Z, roughness and smoothing reach. +Connector AABBs include the swept bridge width, endpoint span, thickness, roughness and smoothing +reach. A box intersecting any feature influence box returns unknown; a box outside every feature box +gets the certified all-air interval. The audit sampled each local field box and the void beside the +connectors. + +| Audit | Result | +|---|---:| +| Island / connector influence boxes | 6 / 5 | +| Bounds boxes / point samples | 517 / 13,932 | +| Intersecting boxes left unknown | 108 | +| Missed bounds | 0 | +| Void boxes proven air | 4 | +| Shrunken-island-bound negative control | Detected | +| Bound audit | **Pass** | + +The bounds negative control removes the lobe and blend reach from a candidate island box, then finds +negative density outside that false box while excluding every other feature's valid influence box. + +The final-field route has 117 waypoints and 116 segments. Its analytic capsule sweep samples route +centers no farther apart than 0.5 voxel, 17 capsule cross-section positions and five support probes +per center; at least three support probes must find rock within the recorded search depth. + +| Route result | Value | +|---|---:| +| Swept centers / capsule field samples | 1,044 / 283,968 | +| Support probes | 6,344 | +| Capsule samples inside rock | 0 | +| Unsupported centers | 0 | +| Grade failures | 0 | +| Minimum sampled density | 40 subvoxels (0.15625 voxel) | +| Route sweep | **Pass** | + +The negative control raises the whole route by 8 voxels. It is rejected at all 1,044 swept centers for +lack of support (zero capsule collisions), so the support test detects the failure mode independently +of capsule intersection. + +## Determinism and existing hashes + +Planner calls repeated three times with identical canonical JSON and hashes. The 32,768-point native +field stream hashes identically at W1, W8 and W16. Stable IDs for this plan are: + +| Feature | Stable IDs | +|---|---| +| Islands | `e6e461e18b3a9c87`, `2bc1820baba047f6`, `adc259cbe2a395e9`, `62a5f29ab581c2b0`, `0412f78b71b39ceb`, `7855fb022cc561c9` | +| Connectors | `eaa5dd310e846520`, `034023599b466c51`, `08e698cf39fd6c22`, `3794e6d8e5033e8b`, `4962fa885545fb26` | + +The standalone check reparsed and validated the existing manifests without changing their content +hashes: M1 round-1 `c87ae279b0e26f53` and SurfaceWorld round-4 `8933693d9560ba9f`. The existing Core +field evaluators and M2 mesher implementation were not edited; the full M2 actor/mesher hash matrix +was not replayed in this standalone round. + +| New island content | Hash | +|---|---| +| Recipe | `111825edfebea17f` | +| Plan | `65675554b7aa947e` | +| Manifest | `03a4685554edce1a` | +| Field samples W1 / W8 / W16 | `bf3ce7da876431e2` each | + +## Matched captures and legacy timing + +Five CPU-rendered capture pairs use the same detected island anchor, orthographic camera and scale. +The cluster and close-up fields were sampled at 4-voxel and 2-voxel grids respectively. The +tile/LOD view overlays 32-voxel tile and 64-voxel LOD reference planes on the canonical field mesh; +it is a review view, not a production mesher seam-closure test. No viewer or GPU was launched. + +[`contact_sheet.jpg`](results/captures/contact_sheet.jpg) — 640×1,290 px, 68,601 bytes. Individual +matched pairs are in [`captures/`](results/captures/); each is 640×258 px and 12,798–18,890 bytes. +They show the overview, underside, lobed edge/cliff, tile/LOD references and planned route. The legacy +side of the route pair is unmarked because the legacy generator has no planner route. + +The same 32,768 quantized query positions were timed serially against the direct legacy owner evaluator +and `FloatingIslandFieldV1::densitySubvoxels`, excluding plan generation, mesh extraction, rendering +and Unreal scheduling: + +| Evaluator | Time | +|---|---:| +| Legacy owner bridge (`GetDensityAt`) | 63.62 ms | +| Native island field | 39.31 ms | +| Native / legacy | 0.618× (about 1.62× faster) | + +This is a point-query comparison for the island field, not a full-world or tile-meshing benchmark. In +the same sparse sample, 90 legacy samples and 885 native samples were solid: 14 were legacy-only and +809 native-only. The overall sign mismatch is 823/32,768 (2.512%), mostly because air dominates the +sample volume. The side-by-side captures and these occupancy counts show that percentage must not be +read as visual or occupancy parity; the routeable native cluster is denser in this narrow vertical +slice than the stochastic owner field. + +## Run ledger + +All builds and audit runs stayed below the 15-minute stop limit. No process crashed and no +`0xC0000142` retry occurred. The audit wrapper samples its test process; the final process peaked at +45.8 MiB private memory and the highest observed failed-audit process was 159.3 MiB. The existing +MSVC setup prints a French `vswhere.exe`-not-found warning before successfully finding `vcvars64.bat`; +the standalone build also emits existing float-conversion warnings in `VoxelStrateManager.cpp`. + +| Failed run | Failure | Correction / outcome | +|---|---|---| +| Initial `Target All` compile | New island methods referenced interval/detail types declared later; one `Q` use was outside its namespace. | Forward declarations and qualified fixed-scale constant; subsequent Core/All builds passed. | +| Second `Target All` compile | One remaining unqualified `Q` in `FloatingIslandFieldV1::isValid`. | Qualified it; subsequent builds passed. | +| First audit-wrapper invocation | Default probe path resolved one directory too high; audit executable did not start. | Corrected the probe path. | +| First audit-harness compile | Windows `min`/`max` and `TEXT` macros broke standard-library expressions. | Set `NOMINMAX` and removed the conflicting compatibility macro around `Windows.h`. | +| Route attempt 1 | 445 blocked capsule samples; 7 grade failures. | Fit route height to the full capsule footprint and start grade fitting. | +| Route attempt 2 | 0 blocked; 19 unsupported centers; 3 grade failures. | Added smaller route intervals and a grade reserve. | +| Route attempt 3 | 0 blocked; 23 unsupported centers; 10 grade failures. | Added precise first-failure diagnostics; set a 5% grade reserve. | +| Route attempt 4 | 0 blocked; 38 unsupported centers; grade passed. | Extended bridge landings into the island tops. | +| Route attempt 5 | 17 blocked samples; support and grade passed. | Raised the recorded capsule clearance from 0.25 to 0.5 voxel. | + +The final `Target All` build and final standalone audit passed. The final run is retained in +[`run_ledger.txt`](results/run_ledger.txt); audit metrics, plan data and query breakdown are in +[`audit.json`](results/audit.json). + +## Source inventory and round boundary + +New source files: + +1. `Standalone/Core/src/FloatingIslandsInternal.h` — Core field, serializer, planner, bounds and route audits. +2. `Standalone/Research/WorldAssemblerM3/Round1/FloatingIslandsAudit.cpp` — legacy comparison and standalone audit driver. +3. `Standalone/Research/WorldAssemblerM3/Round1/BuildFloatingIslandsAudit.ps1` — BelowNormal build/run wrapper with time and test-process memory caps. +4. `Standalone/Research/WorldAssemblerM3/Round1/RenderFloatingIslandCaptures.py` — CPU-only small matched capture renderer. + +Changed existing source/docs: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, +`Standalone/Core/src/VoxelForgeCore.cpp` (include only), `Standalone/Core/API.md`, and this report. +`Source/` and `Standalone/Source/` were not edited. No owner project files, editor, `.uasset`, commit, +push or stash were touched. + +This round ends at standalone Core proof. `FloatingIslandManifestV1` is not yet embedded in +`WorldAssemblyManifest` or the Unreal adapter. The reference-plane capture does not close production +tile/LOD seams, the route is not cooked into Unreal collision, and the feature emits no seasonal +water body. These belong to later M3 integration/look rounds; player-edit policy remains the owner's +unrestricted choice. diff --git a/Standalone/Research/WorldAssemblerM3/Round1/RenderFloatingIslandCaptures.py b/Standalone/Research/WorldAssemblerM3/Round1/RenderFloatingIslandCaptures.py new file mode 100644 index 0000000..dfe0875 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round1/RenderFloatingIslandCaptures.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Small CPU-only matched views of the legacy and native FloatingIslands fields.""" +from __future__ import annotations + +import ctypes +import json +import math +import struct +from pathlib import Path + +from PIL import Image, ImageDraw + + +ROOT = Path(__file__).resolve().parent +RESULTS = ROOT / "results" +CAPTURES = RESULTS / "captures" +CAPTURES.mkdir(parents=True, exist_ok=True) +WIDTH, HEIGHT = 320, 226 +BACKGROUND = (236, 240, 242) +LEGACY_COLOR = (133, 127, 105) +NATIVE_COLOR = (78, 135, 151) +ROUTE_COLOR = (223, 72, 45) +TILE_COLOR = (178, 60, 52) +LOD_COLOR = (211, 136, 45) +Q = 256.0 + + +def lower_priority() -> None: + if hasattr(ctypes, "windll"): + ctypes.windll.kernel32.SetPriorityClass(ctypes.windll.kernel32.GetCurrentProcess(), 0x00004000) + + +def read_mesh(path: Path): + raw = path.read_bytes() + if len(raw) < 8 or raw[:4] != b"VFMI": + raise RuntimeError(f"invalid field mesh: {path}") + count = struct.unpack_from(" 1.0e-12 else (0.0, 0.0, 1.0) + + +def dot(a, b): + return sum(x * y for x, y in zip(a, b)) + + +def cross(a, b): + return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) + + +def project(points, target, view_dir, scale=None): + direction = normalize(view_dir) + forward = tuple(-value for value in direction) + right = normalize(cross(forward, (0.0, 0.0, 1.0))) + up = normalize(cross(right, forward)) + camera_points = [] + for point in points: + delta = tuple(point[i] - target[i] for i in range(3)) + camera_points.append((dot(delta, right), dot(delta, up), dot(delta, forward))) + if scale is None: + span_x = max((p[0] for p in camera_points), default=1.0) - min((p[0] for p in camera_points), default=0.0) + span_y = max((p[1] for p in camera_points), default=1.0) - min((p[1] for p in camera_points), default=0.0) + scale = 0.86 * min((WIDTH - 30) / max(span_x, 1.0e-6), (HEIGHT - 30) / max(span_y, 1.0e-6)) + pixels = [ + (WIDTH * 0.5 + point[0] * scale, 31 + (HEIGHT - 31) * 0.5 - point[1] * scale, point[2]) + for point in camera_points + ] + return pixels, scale, right, up, forward + + +def common_scale(mesh_pair, target, view_dir): + vertices = mesh_pair[0][1] + mesh_pair[1][1] + _, scale, _, _, _ = project(vertices, target, view_dir) + return scale + + +def projected_line(a, b, target, basis, scale): + right, up, forward = basis + points = [] + for point in (a, b): + delta = tuple(point[i] - target[i] for i in range(3)) + points.append((WIDTH * 0.5 + dot(delta, right) * scale, + 31 + (HEIGHT - 31) * 0.5 - dot(delta, up) * scale)) + return points + + +def render(mesh, mesh_pair, method, target, view_dir, title, subtitle, *, route=None, guide=None): + triangles, vertices = mesh + scale = common_scale(mesh_pair, target, view_dir) + _, _, right, up, forward = project((), target, view_dir, scale) + background = Image.new("RGB", (WIDTH, HEIGHT + 32), BACKGROUND) + draw = ImageDraw.Draw(background) + draw.rectangle((0, 0, WIDTH - 1, 31), fill=(34, 46, 54)) + draw.text((8, 4), title, fill=(250, 252, 253)) + draw.text((8, 17), subtitle, fill=(190, 207, 215)) + pixels = background.load() + zbuffer = [float("inf")] * (WIDTH * (HEIGHT - 31)) + camera_fill = normalize(view_dir) + light = normalize((-0.25 + 1.2 * camera_fill[0], + -0.35 + 1.2 * camera_fill[1], + 0.45 + 1.2 * camera_fill[2])) + base = LEGACY_COLOR if method == "legacy" else NATIVE_COLOR + for tri in triangles: + points = [] + for offset in (0, 3, 6): + index = {0: 0, 3: 1, 6: 2}[offset] + vertex = tri[offset:offset + 3] + delta = tuple(vertex[i] - target[i] for i in range(3)) + points.append((WIDTH * 0.5 + dot(delta, right) * scale, + 31 + (HEIGHT - 31) * 0.5 - dot(delta, up) * scale, + dot(delta, forward))) + x0 = max(0, int(math.floor(min(p[0] for p in points)))) + x1 = min(WIDTH - 1, int(math.ceil(max(p[0] for p in points)))) + y0 = max(31, int(math.floor(min(p[1] for p in points)))) + y1 = min(HEIGHT, int(math.ceil(max(p[1] for p in points)))) + if x0 > x1 or y0 > y1: + continue + (ax, ay, az), (bx, by, bz), (cx, cy, cz) = points + denominator = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denominator) < 1.0e-10: + continue + normal = normalize(tri[9:12]) + shade = 0.50 + 0.50 * max(0.0, dot(normal, light)) + color = tuple(max(0, min(255, int(channel * shade))) for channel in base) + for y in range(y0, y1 + 1): + row = (y - 31) * WIDTH + for x in range(x0, x1 + 1): + sx, sy = x + 0.5, y + 0.5 + u = ((by - cy) * (sx - cx) + (cx - bx) * (sy - cy)) / denominator + v = ((cy - ay) * (sx - cx) + (ax - cx) * (sy - cy)) / denominator + w = 1.0 - u - v + if u < -1e-8 or v < -1e-8 or w < -1e-8: + continue + depth = u * az + v * bz + w * cz + index = row + x + if depth < zbuffer[index]: + zbuffer[index] = depth + pixels[x, y] = color + + basis = (right, up, forward) + if guide: + origin = guide["origin_voxels"] + top = guide["top_voxels"] + for x_delta, color, label in ((32.0, TILE_COLOR, "32v tile"), (64.0, LOD_COLOR, "64v LOD1")): + line = projected_line((origin[0] + x_delta, origin[1] - 190.0, top - 65.0), + (origin[0] + x_delta, origin[1] + 190.0, top + 14.0), + target, basis, scale) + draw.line(line, fill=color, width=2) + draw.text((max(2, min(WIDTH - 55, int(line[1][0]) + 2)), + max(33, min(HEIGHT - 11, int(line[1][1]) - 12))), label, fill=color) + if route: + points = [] + for point in route: + delta = tuple(point[i] - target[i] for i in range(3)) + points.append((WIDTH * 0.5 + dot(delta, right) * scale, + 31 + (HEIGHT - 31) * 0.5 - dot(delta, up) * scale)) + if len(points) >= 2: + draw.line(points, fill=(250, 248, 239), width=5, joint="curve") + draw.line(points, fill=ROUTE_COLOR, width=3, joint="curve") + for index in (0, len(points) - 1): + x, y = points[index] + draw.ellipse((x - 4, y - 4, x + 4, y + 4), fill=(250, 248, 239), outline=ROUTE_COLOR, width=2) + return background + + +def titled_pair(left, right, category): + pair = Image.new("RGB", (WIDTH * 2, HEIGHT + 32), (220, 226, 229)) + pair.paste(left, (0, 0)) + pair.paste(right, (WIDTH, 0)) + draw = ImageDraw.Draw(pair) + draw.rectangle((0, 0, WIDTH * 2 - 1, HEIGHT + 31), outline=(72, 84, 91), width=1) + draw.text((8, HEIGHT + 14), f"{category} | same target, camera and scale", fill=(45, 55, 61)) + return pair + + +def main(): + lower_priority() + data = json.loads((RESULTS / "audit.json").read_text(encoding="utf-8")) + anchor = data["legacy_anchor_voxels"] + route = data["route_points_voxels"] + guide = {"origin_voxels": anchor[:2], "top_voxels": anchor[2]} + cluster = { + "legacy": read_mesh(RESULTS / "cluster_legacy.vfmesh"), + "native": read_mesh(RESULTS / "cluster_native.vfmesh"), + } + detail = { + "legacy": read_mesh(RESULTS / "detail_legacy.vfmesh"), + "native": read_mesh(RESULTS / "detail_native.vfmesh"), + } + a = anchor + scenes = ( + ("overview", "Cluster overview", cluster, (a[0], a[1], a[2] - 28.0), (8.0, -10.0, 7.0), None, None), + ("underside", "Island underside", detail, (a[0], a[1], a[2] - 25.0), (8.0, -10.0, -16.0), None, None), + ("edge_cliff", "Lobed island edge / cliff", detail, (a[0], a[1] + 35.0, a[2] - 8.0), (0.0, 12.0, 2.0), None, None), + ("tile_lod_seam", "Tile / LOD reference planes", cluster, (a[0], a[1], a[2] - 28.0), (8.0, -10.0, 7.0), None, guide), + ("route", "Planned route over final field", cluster, (a[0], a[1], a[2] - 26.0), (8.0, -10.0, 7.0), route, None), + ) + pairs = [] + for key, title, meshes, target, camera, path, grid in scenes: + left = render(meshes["legacy"], (meshes["legacy"], meshes["native"]), "legacy", + target, camera, title, "Legacy bridge | exact owner field", route=None, guide=grid) + route_caption = "Core native | planner route" if path else "Core native | matched field view" + right = render(meshes["native"], (meshes["legacy"], meshes["native"]), "native", + target, camera, title, route_caption, route=path, guide=grid) + pair = titled_pair(left, right, title) + out = CAPTURES / f"{key}_matched.jpg" + pair.save(out, quality=82, optimize=True) + pairs.append(pair) + print(f"capture={out.name} bytes={out.stat().st_size} size={pair.size[0]}x{pair.size[1]}") + + sheet = Image.new("RGB", (WIDTH * 2, (HEIGHT + 32) * len(pairs)), (218, 225, 228)) + for row, pair in enumerate(pairs): + sheet.paste(pair, (0, row * (HEIGHT + 32))) + sheet_path = CAPTURES / "contact_sheet.jpg" + sheet.save(sheet_path, quality=76, optimize=True) + oversized = [(path.name, path.stat().st_size) for path in sorted(CAPTURES.glob("*.jpg")) + if path.stat().st_size >= 300_000] + for path in sorted(CAPTURES.glob("*.jpg")): + print(f"capture_size={path.name} bytes={path.stat().st_size}") + if oversized: + raise RuntimeError(f"captures exceed the 300 KB limit: {oversized}") + print(f"contact_sheet={sheet_path.name} bytes={sheet_path.stat().st_size} views=5 matched_pairs=5") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Round1/results/audit.json b/Standalone/Research/WorldAssemblerM3/Round1/results/audit.json new file mode 100644 index 0000000..2d21f4b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round1/results/audit.json @@ -0,0 +1,21 @@ +{ + "recipe_hash":"111825edfebea17f", + "plan_hash":"65675554b7aa947e", + "manifest_hash":"03a4685554edce1a", + "unchanged_existing_hashes":{"m1_round1_manifest":"c87ae279b0e26f53","surfaceworld_round4_manifest":"8933693d9560ba9f"}, + "plan_islands":6, + "plan_connectors":5, + "route_points":117, + "legacy_anchor_voxels":[48,16,-1904.03515625], + "legacy_anchor_plateau_samples":25, + "recipe_parameters":{"seed":0,"density_q16":32768,"spacing_voxels":112,"radius_voxels":[24,48],"thickness_ratio_q16":39322,"vertical_jitter_q16":39322,"top_flatten_q16":39322,"contour_warp_voxels":12.6015625,"roughness_voxels":3,"blend_voxels":6}, + "stable_island_ids":["e6e461e18b3a9c87","2bc1820baba047f6","adc259cbe2a395e9","62a5f29ab581c2b0","0412f78b71b39ceb","7855fb022cc561c9"], + "stable_connector_ids":["eaa5dd310e846520","034023599b466c51","08e698cf39fd6c22","3794e6d8e5033e8b","4962fa885545fb26"], + "bounds":{"passed":true,"negative_control_detected":true,"boxes":517,"samples":13932,"unknown":108,"missed":0,"island_boxes":6,"connector_boxes":5,"void_boxes_proved_air":4}, + "route":{"passed":true,"segments":116,"centers":1044,"capsule_samples":283968,"blocked":0,"support_samples":6344,"unsupported":0,"grade_failures":0,"minimum_clearance_subvoxels":40}, + "raised_route_negative_control":{"detected":true,"passed":false,"unsupported":1044,"blocked":0}, + "determinism":{"W1":"bf3ce7da876431e2","W8":"bf3ce7da876431e2","W16":"bf3ce7da876431e2","plan_repeat":true,"hash_samples":32768}, + "timing":{"same_queries":32768,"legacy_ms":63.6167,"native_ms":39.3082,"native_over_legacy":0.617891214099,"sign_mismatches":823,"sign_mismatch_percent":2.51159667969,"legacy_solid_samples":90,"native_solid_samples":885,"legacy_only_solid_samples":14,"native_only_solid_samples":809}, + "mesh":[{"name":"cluster_legacy","grid_samples":422625,"triangles":4316,"bytes":207176},{"name":"cluster_native","grid_samples":422625,"triangles":35068,"bytes":1683272},{"name":"detail_legacy","grid_samples":126750,"triangles":15972,"bytes":766664},{"name":"detail_native","grid_samples":126750,"triangles":30044,"bytes":1442120}], + "route_points_voxels":[[-176,10.43359375,-1904.30859375],[-172.13671875,10.41015625,-1904.40625],[-168.27734375,10.38671875,-1904.70703125],[-164.4140625,10.36328125,-1905.23828125],[-160.55078125,10.33984375,-1904.484375],[-156.69140625,10.31640625,-1904.625],[-152.828125,10.29296875,-1904.73828125],[-148.96484375,10.26953125,-1904.71875],[-145.1015625,10.24609375,-1904.609375],[-141.2421875,10.22265625,-1904.3203125],[-137.37890625,10.19921875,-1904.1796875],[-133.515625,10.17578125,-1903.96875],[-129.65625,10.15234375,-1903.71484375],[-125.79296875,10.12890625,-1903.4609375],[-121.9296875,10.10546875,-1903.2109375],[-118.0703125,10.078125,-1902.95703125],[-114.20703125,10.0546875,-1902.703125],[-110.34375,10.03125,-1902.44921875],[-106.484375,10.0078125,-1902.19921875],[-102.62109375,9.984375,-1901.9453125],[-98.7578125,9.9609375,-1901.69140625],[-94.8984375,9.9375,-1901.4375],[-91.03515625,9.9140625,-1900.71484375],[-87.171875,9.890625,-1899.73046875],[-83.30859375,9.8671875,-1898.74609375],[-79.44921875,9.84375,-1897.765625],[-75.5859375,9.8203125,-1896.78125],[-71.72265625,9.796875,-1896.0390625],[-67.86328125,9.7734375,-1895.84375],[-64,9.75,-1895.9453125],[-60.13671875,9.96484375,-1896.41015625],[-56.27734375,10.1796875,-1897.2265625],[-52.4140625,10.3984375,-1898.2109375],[-48.55078125,10.61328125,-1898.98046875],[-44.69140625,10.828125,-1899.39453125],[-40.828125,11.04296875,-1900.06640625],[-36.96484375,11.2578125,-1900.56640625],[-33.1015625,11.47265625,-1900.84765625],[-29.2421875,11.69140625,-1901.04296875],[-25.37890625,11.90625,-1901.1328125],[-21.515625,12.12109375,-1901.2265625],[-17.65625,12.3359375,-1901.3203125],[-13.79296875,12.55078125,-1901.41015625],[-9.9296875,12.765625,-1901.50390625],[-6.0703125,12.984375,-1901.59375],[-2.20703125,13.19921875,-1901.6875],[1.65625,13.4140625,-1901.78125],[5.515625,13.62890625,-1901.87109375],[9.37890625,13.84375,-1901.96484375],[13.2421875,14.05859375,-1902.05859375],[17.1015625,14.27734375,-1902.00390625],[20.96484375,14.4921875,-1901.01953125],[24.828125,14.70703125,-1900.03515625],[28.69140625,14.921875,-1899.30078125],[32.55078125,15.13671875,-1898.91015625],[36.4140625,15.3515625,-1898.82421875],[40.27734375,15.5703125,-1898.9375],[44.13671875,15.78515625,-1899.29296875],[48,16,-1899.83203125],[51.86328125,16.06640625,-1900.5234375],[55.72265625,16.1328125,-1901.33984375],[59.5859375,16.19921875,-1902.2265625],[63.44921875,16.26171875,-1903.13671875],[67.30859375,16.328125,-1903.8828125],[71.171875,16.39453125,-1902.8984375],[75.03515625,16.4609375,-1902.83984375],[78.8984375,16.52734375,-1902.6796875],[82.7578125,16.59375,-1902.23046875],[86.62109375,16.66015625,-1901.98828125],[90.484375,16.72265625,-1901.52734375],[94.34375,16.7890625,-1901.0546875],[98.20703125,16.85546875,-1900.58203125],[102.0703125,16.921875,-1900.11328125],[105.9296875,16.98828125,-1899.640625],[109.79296875,17.0546875,-1899.171875],[113.65625,17.12109375,-1898.69921875],[117.515625,17.1875,-1898.23046875],[121.37890625,17.25,-1897.7578125],[125.2421875,17.31640625,-1897.28515625],[129.1015625,17.3828125,-1896.81640625],[132.96484375,17.44921875,-1896.34375],[136.828125,17.515625,-1896.67578125],[140.69140625,17.58203125,-1896.12890625],[144.55078125,17.6484375,-1895.6875],[148.4140625,17.7109375,-1895.375],[152.27734375,17.77734375,-1895.08984375],[156.13671875,17.84375,-1894.8046875],[160,17.91015625,-1894.4453125],[163.86328125,17.640625,-1894],[167.72265625,17.3671875,-1893.47265625],[171.5859375,17.09765625,-1892.9921875],[175.44921875,16.82421875,-1892.71875],[179.30859375,16.5546875,-1892.65625],[183.171875,16.28125,-1892.6875],[187.03515625,16.01171875,-1893.26171875],[190.8984375,15.73828125,-1894.19140625],[194.7578125,15.46875,-1895.17578125],[198.62109375,15.1953125,-1896.16015625],[202.484375,14.92578125,-1897.1484375],[206.34375,14.65234375,-1898.1328125],[210.20703125,14.3828125,-1899.1171875],[214.0703125,14.109375,-1900.10546875],[217.9296875,13.83984375,-1901.08984375],[221.79296875,13.56640625,-1902.07421875],[225.65625,13.296875,-1902.78125],[229.515625,13.0234375,-1903.4140625],[233.37890625,12.75390625,-1904.046875],[237.2421875,12.48046875,-1904.68359375],[241.1015625,12.2109375,-1905.31640625],[244.96484375,11.9375,-1905.94921875],[248.828125,11.66796875,-1905.16796875],[252.69140625,11.39453125,-1904.4296875],[256.55078125,11.125,-1904.078125],[260.4140625,10.8515625,-1903.9296875],[264.27734375,10.58203125,-1903.92578125],[268.13671875,10.30859375,-1903.97265625],[272,10.0390625,-1903.96484375]] +} diff --git a/Standalone/Research/WorldAssemblerM3/Round1/results/captures/contact_sheet.jpg b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/contact_sheet.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7cdc81d62137c0511b91a3ead55d9e2658b3c771 GIT binary patch literal 68601 zcmeFZ1yoyYn>HEjJ4K5Gm*5m=3DRJ}DFk!VinJ7} zhxulG?>q8-Xa1Qv|2pfOf0DKKy0iCw{MP5W?)+Z*y$K*zS5{L7prHW(X!l>h?^S>T z00$Eb8w(Q$8w(p32L~6Q3?Cm451)#Jl#mQWMMpOB0JMLq`Tmg*$$cZ}Xy};em_Q%~@SpmHMuJWXWa7sllhd_f zzVJ$J=$(ETQNSXwg()bnFQjMtdj)_Gye~xpBmu|(7W{SkB8;H)fk^=QxB2yZ;>h*-%cRK^8nUPUn&|Kjra^7gKxn-RHTYUth3>*a;mJWo!A z+pX+VN`l9el8al~8Le}YW?N65<5cc1J~f(DF}PzvB2|=F#5Iq6v~h;TBoYSqGZ*HD z?aD%CB>At1K~RX9#m;9Tvd`ShZEtu~$mGmNwgn)9T|6! z%g;E(5C|HsN54#c$pq3qpOJ_dwelD=KA|LFS0knHpk~^j3YUMa^~C)sZ?E{PG6Sa1VIu$-gxnxC9 zrl?W{yaZ&Amh?E74)-d`!F<5WH-nR@IjTd8jA#tT!7^k%Istw6Muf4bXGzNn{bV>=IS6rtmo zWUf62>AL$_3Kk%M$}at{_#16+IbVZNLw@ZoUB){ke;d#P>z}hSV`^XG{55|Ac-R*n zvu|t6cb9%}K776?XfevD09rSg%AkK+6Y$ej{*wa{D3HmZ0q%ZwQP?i%D02f>nJL+ zge5pOJ9`a=1i)*@CN%!e3mVD;es)yiK1=A4^&dgB7Du(U`RxW84urz0b=J00(wr5m*WS&kg}FQro6=2{HJDlJE@t z8{ly8&#jnxgi!p5#n@G|)Y513(A~HmXW6oraMxJ-YVRtpi&U~)6DwRh?sQaO#e&aA z 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 z�%}G_#U1y$`8;<^ahlE8!9UVj0#FW_c9qq|!+WtcKWpwmO`^j*D*`F0;iiT>{kE zfAZ(PoHB_axahw!aPyF|pMYV+=1oPHZ1fMk7T+i3jj|~KMdL02YqSa}4?5d?`VGT+ z!z0t)Gv5%B=jFsv@yh*Q1Igfuvx}40NPtr8-jl&H^L7=Z_N?Eu@+)2x{@{T2QGOgN zQyW?&z^W^W2GZErrS+1uUKVsL9F_8~{{Qm474BBLKs5jwDk4(;>Az`+Xfmb<=48fyCe!Fo+DQD0* zxmp3gklb2YnF$SYCX4rm$r+^Pry^Y}7|wWs)Z}Nv;ArrslCb$rNx2pH zn!Wvu86WG3JkVBVo!Y8r{7G}Sp#o{tzrrLM)yUoARdeR6w%Sn-b zS?C>GwG@nB&>wP{9HFVuqtQMJfJiJWPS&^5oJ5)>z%Mi;rfs0b-pG=8wlt|YF??#r zv}~x@SAqywlmZSd=99bcGbyp%SErx=Ur3#s$dWAY6h@DUmiRKS!;zvRaOKZ}s18U% z5?G!Z&0{mNaPMkA_WtDb9+tv<;}J=D%x{;9KX@2i#RVY+ zz@oi=UvCqHF$r?{*-xTse)9A|6Lg<+?{{bMq|b#!_=Sgd<{WM>M(y~`R>Z|9Iv3UI zi=YBg-nEIMWlW$4K3~bfwkwO*{9Ms$V@n!+9iEij*=#fu3~+;Z&}xmy@(rQVEEK! zGxz$h&9^ z@_SxT-6<)^4JWZ{ke}>c>`c7BYN@~jtZ62RYL6z(c0ep#GpFOjx#`nBb+oW^i7%)r zfg+VrG9m|P_$ctU_XDkr{?+1vWYVWs>cpzAxruJFJ;8yu@rxNzTt`$he3yXD*m&RN zJ+px9dYbif9xic)%J~x$8i9gh5BpsYCu+hF@#cU~@w5o{6`b4C4{;^cd&^OIoSczY zF+D8o`k(BM+20UgD^!2dJNa&Z@?W(7ImYi_2}K2B_eSJ~+Ns*93hzc%=TDmmIb;iw zRc=rq!FgZ5Icq)hjGLJwqg9wHBbz0AZQQ_vEz|xQxTz`^TU$^HtyfF);defyagpn* zIk5xnfG@a?_v+*^wO?hK~>wXfg!A$we5Y3?^b%9-3LlyA@@nWd)*= zWVb@d+L&fhS}b5@uFvziPhI=mmY27Z=;*9U|AD__{2OAj_Id5Xx4KI}oxR`vZk9ip zN!|4*k*(YKoZmTxSsed$?Crlu{!gUgzvrg^5B!(^P2v8N_nv>+knRsZk(~>C{_kAA z>_5rwfPYT^KmISJ{{MGe{eyB|0mo>?EtdGC{2AB$%=amxu$?b~cTuE-Ti0AXnOBO6 z{~8ufIw+KQoUNW)=Dk7n&=r5`XxMoG50b>&&8i0gEcS)o={?dn&l!6*lkYer?fI(6 zQJ=2>#_S_gIJMR{z4D9x5G&;;Cz77Fo2}`RbxQ@W-;@!dr^f4ZUu3>lQf{WK<@Qx_ zd;J)Wl>WZ2$!M8SCYY1AK>Ly0#84<^E@$ox&0aG-)v`+8CcmEjLDlF^;)qCq|IM0) z_dXEjIB3T8zvzHp5So zQ_UD$grr)sV+TXlB&6EKW9Xt=wM4v8rOw7{Yv+<~d6gz?+9t}5NhOxg4w!Q}dOM|x zeMBx^`S`my18ic0%gWWUmX*p5$i%Ija2stVhY`{;Ww}8?Xj|1a_0;Z%C~23p!eI%P zO$AkP`ZMZ|v27?XmyI3kdoJ0E<|dSuSu9Fh_nD#Lk1mrG-ki!HdvQj$&g}9Fj5PW_ z`d}L0NgjZ|ZMfX^)GD`v;Ur;PS>G`!cnoiOB3D)s1+8)uQEH39-~dQvp5Z_-E|Hgh zDq~}n-iUrNe2#GSwG9v)2$YPsgwCZ1XYH!;NLA#FBCyhG1usB}#f4F2aXYE|Dkc~V z0EdzXq);wG&ew&Q^Y}XC8hO2y3?@zQBvr)pJ$OCwcv36`#quVNJF>b<7G)qOky#?? zm7g7ntEjtv@0Cx|d`DAevQ9<2`1F2TEjxBozNNlqDo+sqzQU#cSfK?OZrHvSN-0(M zNdc=QA`QGE;y!2@FBUpJf1ZviLySp6XIZ9+?gAEP7vC1)H<0M)>r{p<_<<96LALL} z+YwRA%%Vpd#4}l``>5}8L3tL-U~3^!)RW_LLqq!!7UK+i4+EAyw+HBgjI`49xfDOU z)huTi4CG}iqh(4hT5jTiI8z!})t+|94vB0uNez2FH(iDDcJ`3d^5O}8shug%*#WIE z3?2XA%W7Mv)UbP8L&VM; zK;VehT>n{$R~OyV4O=GM#Ppd!E8_8;ly8uD-grH!YRUmhlqAtLl^!e&$y%(^i}e?4 z3QD_^EN|I#KtWG#FyDc?E>{%8xWp{Hg^>(pb|zxQ*i)gU@PJDI*@cXq3I98Ii->p$ zX`#w88E>Jxe#@!IIU>-1#Ly)DQ)z6aQfWsiX=vmYG_2(i0|}INP0!yA;HKY>8FUD1 z3|!DRmSM>_?%wnb*bFJz*#)Km|SpRnujWy*AABxt*S5Df_zPxX4OU9k%kgd?HN#2c&}6VV`J6D9Ze5p z#9$b;7*jgbe67{ebJud!ynh&*}}t3?`__Nq$4S9L5TG> zSH&QIJ5R7IvS0$2F#~U`{C?UWeZ2d;_GW2w&2IGC{%s~Ns23c#1tBGfuu;x(D1E9= zmU)*q@1-V)oo{xS{$iAgyp07uTn(hD=0M>Pc~uJXEvgI^Mj(6^g~nHiDf$|HMt9{TL#u!?v#?@UP9rn zxVdRWc&4N&pm^r;JGKr(1ir1(4GKbrvuLtfrWXfRCPnV`6P_8o_LA7y`sUN;;V8(@ zb&FQ@@FN3aflPnAbhuwG{NA0s@IVi#isU{}VRY9`QfraLMOSchOOLFei|13f_>C7^oDoKxbsd&ZZ_4B`a&h4*P3I+mfr zj;MD%{w_63uw*8qX}sX!I6KAybx380~_W@D*-KUb( z6N)-S{o*$q`BOsGSmvgjz7ue#0S`75-r>#6s5~n)ZG#i;YskHOT=Z#nnpL#EYO7C$ zVgKq3*Ki=bQ%qh$3njZ=qi#c_f>d+alU4` zj#a$xdF?G=srdIf25LX&hnc;sM@A@AL>|QiDuzj2m)I~-7Z{q!41AHln0U}NJ*LuC zrkrpKEDHuh8ZOXCP{#E<_hS906(=tog<0R?+IuwN1bCM7cQO%<52y+HAMg-TNl{!k z^W)QOY#7@YA-DTs#0?_w@_pj&cDU6t-NcICnI2&4a|IZJ6gDQ>?^xBB-NkP$L-xgE zXpI#!Z4pdc6}fO1&mZIJ-yNgsI<#zOE#GM|C$4ZI%E-C6wtKko)IBS-WSz8{xnD%) z^Q}?d%(g2bmgJV$UG|lVmj3xpaD)sH<4R+mGyHm{_+w?B85IaP(XggmvoEdO(3KBQ zQBUzC7r?Wu&5B2RO%CX<8eM?8P+adLR>6!>O0yg6*r{jEJQ=S@eEA5qP}u(GpU_Jts!SS2!!6k6qUI-Seu{{e?Q(P#hw literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Round1/results/captures/overview_matched.jpg b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/overview_matched.jpg new file mode 100644 index 0000000000000000000000000000000000000000..808211f9453d58977e130529d9767ed38fded2de GIT binary patch literal 12798 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Round1/results/captures/route_matched.jpg b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/route_matched.jpg new file mode 100644 index 0000000000000000000000000000000000000000..70380cdace03fac7d45e173ba3f0b30c724d452a GIT binary patch literal 14572 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 C>i=2* literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Round1/results/captures/tile_lod_seam_matched.jpg b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/tile_lod_seam_matched.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0d8669a62c30f437b5884ff9c8b22cfe68420f98 GIT binary patch literal 18890 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 NWhod9qw2-X{{Xwcx2^yH literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Round1/results/captures/underside_matched.jpg b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/underside_matched.jpg new file mode 100644 index 0000000000000000000000000000000000000000..de122c2c41ae2f5b0ceefab06f28efd4d753d221 GIT binary patch literal 17282 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 literal 0 HcmV?d00001