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 0000000..7cdc81d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/contact_sheet.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Round1/results/captures/edge_cliff_matched.jpg b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/edge_cliff_matched.jpg new file mode 100644 index 0000000..29a3557 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/edge_cliff_matched.jpg differ 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 0000000..808211f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/overview_matched.jpg differ 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 0000000..70380cd Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/route_matched.jpg differ 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 0000000..0d8669a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/tile_lod_seam_matched.jpg differ 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 0000000..de122c2 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round1/results/captures/underside_matched.jpg differ