diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7f745a3..4086eec 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1983,6 +1983,33 @@ cutaway views. Both affect captures only, not generated geometry. [`Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md) records the evidence and owner-facing caveats. +### 8.43 Enclosed FloatingIslands helix and native-solid passage support — World Assembler L25 + +L24's support tread was composited into the passage field without requiring native solid terrain at +the support sample. A 6 m root could not contain the resolved helix either: the centreline reaches +5.562 m from the anchor axis and the mouth bore is 4.5 m. With the old root radius, the tread could +therefore read as a free-floating spiral outside the rock. This was not a unit conversion error. + +The shared passage composer now emits route-mouth support, tread, and fallback support only where +the pre-passage native density is solid. The open-air audit samples each support footprint at 0.25 m +spacing and two depths, including route-mouth shelves; its L24 ungated-support control must report a +violation. The FI anchor root is now a relief-bearing 13.25 m nominal rock column. Across five +heights its measured radius is 12.0–13.375 m. Taking the maximum 5.562 m centreline radius and the +full 4.5 m mouth bore at every height gives a conservative minimum wall of 1.938 m, exceeding the +previous 1.5 m reserve. The authored mid-bore remains 3 m. The helix is selected from its route-bound +root point and joins the lower route before the tunnel hands off through the shared SW ceiling. + +The configured FI Spiral passes the capsule/support/grade audit with zero blocked, unsupported, or +grade-failure samples; the L23 smooth-wall control fails naturalness and the L24 ungated-support +control fails the open-air audit. The other style overrides also pass the open-air audit, but their +capsule results are recorded individually in the L25 report because some overrides are not +walkable after floating support is suppressed. Evaluator version is 12, FI geometry version is 7, +and resolved-layout schema V3 remains unchanged. The L25 report records measurements, hashes, +captures, style results, and the single Unreal adapter version edit. + +[`Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md) +records the evidence and caveats. + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/CODEMAP.md b/CODEMAP.md index 0aabe6e..083dab3 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1181,6 +1181,19 @@ The shared evaluator is version 10, the FI recipe geometry is version 5, and res V3 is unchanged. See ARCHITECTURE §8.42 and [`Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux24/REPORT.md). +### 3.26 Enclosed FloatingIslands helix and open-air support gate — World Assembler L25 + +| Symbol | Canonical file | Role | +|---|---|---| +| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Emits route-mouth support, tread and fallback support only over pre-passage native solid density, shared by every passage style. | +| `auditPassageOpenAir` / `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the support footprint at 0.25 m spacing and two depths; compares against L24 ungated support as a required negative control. | +| Anchor root dimensions and relief | `Standalone/Core/src/FloatingIslandsInternal.h` | Raises the FI anchor root from a 6 m cylinder to a nominal 13.25 m relief-bearing rock column sized around the 3 m helix and 4.5 m mouth. | +| Passage route and root measures | `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Binds the Spiral to the root and reports measured centreline/root clearance. | + +The shared evaluator is version 12 and FI geometry version is 7; resolved-layout schema V3 is +unchanged. See ARCHITECTURE §8.43 and +[`Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp index 36999db..ea4cbfa 100644 --- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp @@ -485,7 +485,7 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager, IslandPlan.topChunkZ = Slot.TopChunkZ; IslandPlan.bottomChunkZ = Slot.BottomChunkZ; auto& Recipe = IslandPlan.manifest.recipe; - Recipe.geometryVersion = 5; + Recipe.geometryVersion = 7; Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels; Recipe.seed = static_cast(StrateManager->GetWorldSeed()); Recipe.ownerVoxelSizeMicrons = static_cast(std::llround(OwnerVoxelSizeCm * 10000.0)); diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index a214113..a700e8f 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -249,8 +249,8 @@ namespace VoxelForgeCore // L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles. inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256; inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2; - // Version 10 adds per-strate passage styles and rough wall relief to resolved passages. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 10; + // Version 12 gates passage supports on native rock and anchors the FI spiral inside a measured root. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 12; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h index 9d8b50c..f3708e9 100644 --- a/Standalone/Core/src/FloatingIslandsInternal.h +++ b/Standalone/Core/src/FloatingIslandsInternal.h @@ -281,7 +281,7 @@ namespace floating_island_detail ? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0; const std::uint32_t ExpectedDenominator = ScaleGcd > 0 ? recipe.coreVoxelSizeMicrons / ScaleGcd : 0; - if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 5 + if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 7 || (recipe.geometryVersion == 1 && (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0)) || recipe.unitsPerVoxel != Q @@ -756,7 +756,9 @@ namespace floating_island_detail if (recipe.geometryVersion >= 5 && island.ordinal == recipe.anchorIslandOrdinal && recipe.anchorRootBottomZSubvoxels < island.bottomZSubvoxels) { - const double rootTop = static_cast(island.bottomZSubvoxels); + // The central buttress must meet the island crust underside. IslandBottom + // also includes the four shorter root lobes and sits below that surface. + const double rootTop = static_cast(IslandCrustBottom(recipe, island)); const double rootBottom = static_cast(recipe.anchorRootBottomZSubvoxels); const double depth = std::max(1.0, rootTop - rootBottom); const double blend = std::clamp(static_cast(recipe.anchorRootRadiusSubvoxels) @@ -1330,7 +1332,7 @@ std::optional FloatingIslandManifestV1::FromJson( manifest.recipe.anchorRootBottomZSubvoxels) ||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_mouth_offset_q"), manifest.recipe.anchorMouthOffsetSubvoxels)) - {error="floating-island geometry version 5 is missing a passage-rooted anchor field";return std::nullopt;} + {error="floating-island geometry version 7 is missing a passage-rooted anchor field";return std::nullopt;} } if (!floating_island_detail::ReadU32(root.Get("schema_version"), schema) || !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash) diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 39d8bd3..6cf1504 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -550,6 +550,14 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const auto routeAnchor=[](const ResolvedLayoutStratumV2& s,bool departure, std::array& out) { + if(departure) + for(const auto& a:s.anchors) + if(a.kind=="passage_anchor"&&a.values.size()>=10) + {out={a.values[7],a.values[8],a.values[9]};return true;} + if(!departure) + for(const auto& a:s.anchors) + if(a.kind=="passage_arrival"&&a.values.size()>=3) + {out={a.values[0],a.values[1],a.values[2]};return true;} const ResolvedLayoutAnchorV2* selected=nullptr; for(const auto& a:s.anchors) if(a.kind=="route_point"&&a.values.size()>=3) @@ -597,8 +605,9 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t // Leave room for the capsule and bounded wall relief at the floor/wall seam. const std::int64_t mouthRadius=std::max(static_cast(std::llround(1.5*qPerMetre)), cfg?cfg->mouthRadiusSubvoxels:0); - const std::int64_t midRadius=std::max(mouthRadius, - cfg?cfg->midRadiusSubvoxels:0); + const std::int64_t midRadius=std::max( + static_cast(std::llround(1.5*qPerMetre)), + cfg?cfg->midRadiusSubvoxels:mouthRadius); const std::int64_t reachMinimum=cfg?std::max(0,cfg->reachMinimumSubvoxels):8*qPerMetre; const std::int64_t reachMaximum=cfg?std::max(reachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre; const std::uint32_t style=cfg?std::min(cfg->style,3u):1u; @@ -624,7 +633,9 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t for(const auto& anchor:upper.anchors) if(anchor.kind=="passage_anchor"&&anchor.values.size()>=7) {passageAnchor=&anchor;break;} - const bool anchoredSpiral=style==2&&passageAnchor!=nullptr; + const bool anchoredIslandPassage=passageAnchor!=nullptr + &&upper.generatorKind=="FloatingIslands"; + const bool anchoredSpiral=style==2&&anchoredIslandPassage; if(!hasUpperRoute||!hasLowerRoute) { const auto xy=PassageRandomXY(PassageSeed(seed,upper.strateIndex,lower.strateIndex, @@ -652,7 +663,10 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t std::array upperTangent{},lowerTangent{}; const bool hasUpperTangent=hasUpperRoute &&routeTangent(upper,true,upperTangent); - const bool hasLowerTangent=hasLowerRoute + const bool hasPassageArrival=std::any_of(lower.anchors.begin(),lower.anchors.end(), + [](const auto& anchor){return anchor.kind=="passage_arrival" + &&anchor.values.size()>=3;}); + const bool hasLowerTangent=hasLowerRoute&&!hasPassageArrival &&routeTangent(lower,false,lowerTangent); const std::int64_t outerRingRadius=std::max(1, @@ -671,6 +685,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t std::max(0,cfg?cfg->cascadeLedgeSubvoxels:0) *static_cast(cfg?cfg->cascadeSteps:4)), mouthRadius+2*qPerMetre,outerRingRadius); + if(anchoredIslandPassage) + ringRadius=std::min(ringRadius,mouthRadius+qPerMetre/2); std::uint32_t phase=static_cast(PassageSeed(seed, upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16); if(hasUpperRoute) @@ -700,7 +716,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t +clockwiseTangentY*upperTangent[1]>=0.0; } const int direction=clockwise?1:-1; - constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree contract. + constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree helix contract. const std::int64_t wormRadialWobble=std::min( std::max(0,wander/3),2*qPerMetre); const std::int64_t wormCenterWobble=std::min( @@ -716,10 +732,14 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const double loop=static_cast(loopIndex); const double phaseA=loop*0.6180339887498948+0.37; const double phaseB=loop*0.4142135623730950+1.11; + const double jitterX=static_cast(wormCenterWobble)*std::sin(phaseB); + const double jitterY=static_cast(wormCenterWobble)*std::cos(phaseB); radius+=static_cast(wormRadialWobble) *std::sin(angle+phaseA); - centerX=static_cast(wormCenterWobble)*std::sin(phaseB); - centerY=static_cast(wormCenterWobble)*std::cos(phaseB); + centerX=jitterX+(anchoredIslandPassage&&passageAnchor + ?static_cast(passageAnchor->values[0]):0.0); + centerY=jitterY+(anchoredIslandPassage&&passageAnchor + ?static_cast(passageAnchor->values[1]):0.0); } else if(pathStyle==3u) { @@ -730,6 +750,11 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t cfg->cascadeLedgeSubvoxels):0)); radius-=ledge*(0.5-0.5*std::cos(angle*steps)); } + if(pathStyle!=1u&&anchoredIslandPassage&&passageAnchor) + { + centerX=static_cast(passageAnchor->values[0]); + centerY=static_cast(passageAnchor->values[1]); + } return std::array{ static_cast(std::llround(centerX+radius*directions[directionIndex][0]/65536.0)), static_cast(std::llround(centerY+radius*directions[directionIndex][1]/65536.0))}; @@ -748,7 +773,11 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const std::int64_t anchorY=passageAnchor->values[1]; const std::int64_t rootRadius=passageAnchor->values[2]; const std::int64_t rootBottom=passageAnchor->values[5]; - helixBedrockZ=rootBottom!=0?rootBottom:contentZQ(upper.bedrock.topMicrometres); + const std::int64_t bedrockTop=rootBottom!=0?rootBottom + :contentZQ(upper.bedrock.topMicrometres); + // Passage route heights are player-center positions. End one capsule + // half-height above the bedrock top so the final ramp step is walkable. + helixBedrockZ=bedrockTop+qPerMetre; const std::int64_t helixRadius=spiralRadius; if(rootRadius(upperMouth[0]-anchorX), @@ -796,6 +825,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t helixEndIndex=xy.size()-1; hasAnchorHelix=helixEndIndex>0; } + const std::int64_t routeGradeQ16=hasAnchorHelix + &&lower.generatorKind=="SurfaceWorld"?8000:TargetGradeQ16; const std::array firstRing=ringPoint(phase); if(hasAnchorHelix&&pathStyle==1u) { @@ -843,12 +874,14 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const std::size_t start=hasAnchorHelix?helixEndIndex:0; for(std::size_t i=start+1;i(TargetGradeQ16) + if(totalHorizontal*static_cast(routeGradeQ16) >=static_cast(verticalDistance)*65536ull)break; for(int step=1;step<=16;++step) { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 06198b1..aa0020b 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -110,6 +110,9 @@ public: const auto composeRouteMouthSupport=[&](std::int32_t strateIndex, const std::array& mouth,bool departure) { + // Support is allowed to reinforce native solid terrain only. Without this + // guard the route-point tread stamps a floating shelf into an open stratum. + if(!(density<0.0f))return; const double mouthDx=qx-static_cast(mouth[0]); const double mouthDy=qy-static_cast(mouth[1]); const double routeSupportReach=13.0*qPerMetre; @@ -159,26 +162,26 @@ public: std::clamp(Z-floorVoxels,-floorThickness,0.0))); } }; - // Seed the route floor first. The bore carve below reopens any overlap at the - // mouth, then applies its own lower tread, so neither shape blocks the capsule. + // Keep the native surface route connected to the entrance. Passage carving + // then removes only the parts that overlap the tunnel, whose own tread owns + // the interior ramp. for(const auto& passage:ResolvedLayout->passages) - { - composeRouteMouthSupport(passage.upperStrateIndex,passage.upperMouthSubvoxels,true); - // The lower mouth emerges into its native terrain. Keep the passage tread - // support, but do not stamp short, flat route-anchor shelves over that cave mouth. - } + composeRouteMouthSupport(passage.upperStrateIndex, + passage.upperMouthSubvoxels,true); for(const auto& passage:ResolvedLayout->passages) { if(passage.controlPoints.size()<2)continue; const double placementRadiusQ=static_cast(passage.placementRadiusSubvoxels) +static_cast(passage.midRadiusSubvoxels); - if(qx*qx+qy*qy>placementRadiusQ*placementRadiusQ)continue; + if(qx*qx+qy*qy>placementRadiusQ*placementRadiusQ) + continue; const double firstFloorQ=passage.controlPoints.front().positionSubvoxels[2] -passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ; const double lastFloorQ=passage.controlPoints.back().positionSubvoxels[2] -passage.controlPoints.back().radiusSubvoxels+playerCenterAboveFloorQ; if(qz>firstFloorQ+2.0*passage.mouthRadiusSubvoxels - ||qz(passage.controlPoints.size())-1; @@ -237,21 +240,6 @@ public: const auto& nearestB=passage.controlPoints[static_cast(bestIndex+1)]; const double nearestRadius=nearestA.radiusSubvoxels+bestT*( static_cast(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels); - const double nearestDx=static_cast(nearestB.positionSubvoxels[0]) - -nearestA.positionSubvoxels[0]; - const double nearestDy=static_cast(nearestB.positionSubvoxels[1]) - -nearestA.positionSubvoxels[1]; - const double nearestHorizontalSq=nearestDx*nearestDx+nearestDy*nearestDy; - const double supportPathT=nearestHorizontalSq>0.0 - ?std::clamp(((qx-nearestA.positionSubvoxels[0])*nearestDx - +(qy-nearestA.positionSubvoxels[1])*nearestDy)/nearestHorizontalSq,0.0,1.0) - :bestT; - const double supportPathRadius=nearestA.radiusSubvoxels+supportPathT*( - static_cast(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels); - const double supportPathPlayerCenterZ=nearestA.positionSubvoxels[2]+supportPathT*( - static_cast(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2]) - -supportPathRadius+playerCenterAboveFloorQ; - const double nearestPlayerCenterZ=supportPathPlayerCenterZ; const double distanceToCenter=std::max(1.0e-9,bestSignedDistance+nearestRadius); const double verticalWallAlignment=distanceToCenter>1.0e-9 ?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*( @@ -265,11 +253,6 @@ public: // Positive density is air; rough rock is cut with full side/ceiling detail and // gentler relief on the gravity-facing wall so the supported tread stays open. composed=std::max(composed,static_cast(-roughSignedDistance/Q)); - // At tight bends, keep the tread tied to the player-center profile selected - // by horizontal path position so vertical probe offsets cannot shift the ramp. - const bool fallbackSupport=qz>=nearestPlayerCenterZ-1.50*qPerMetre; - const double supportThicknessVoxels=fallbackSupport - ?qPerMetre/Q:0.5*qPerMetre/Q; double bestSupportDistanceSquared=std::numeric_limits::infinity(); const auto considerSupportSegment=[&](std::ptrdiff_t index) { @@ -293,21 +276,28 @@ public: static_cast(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]); const double radius=segmentA.radiusSubvoxels+supportT*( static_cast(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels); - const double supportPlayerCenterZ=centerZ-radius+playerCenterAboveFloorQ; - if(fallbackSupport) - { - const double supportProfileError=std::abs( - supportPlayerCenterZ-nearestPlayerCenterZ); - if(supportProfileError>0.06*qPerMetre)return; - } const double horizontalX=qx-centerX,horizontalY=qy-centerY; const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX +horizontalY*horizontalY); + const double supportPlayerCenterZ=centerZ-radius+playerCenterAboveFloorQ; + const double supportQueryDepth=supportPlayerCenterZ-qz; + // Support belongs just below the avatar's foot plane. Queries within + // the capsule body, or a neighbouring coil's overlapping bore, must + // not turn that tread into a ceiling or mid-air shelf. + if(supportQueryDepth<0.93*qPerMetre||supportQueryDepth>1.30*qPerMetre)return; + const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre; + const double supportThicknessVoxels=fallbackSupport + ?qPerMetre/Q:0.5*qPerMetre/Q; const double floorQ=centerZ-radius+floorOffset*qPerMetre; const double floorVoxels=floorQ/Q; + // Keep the supported ramp to a comfortable walking width. Extending a + // full bore-radius shelf across overlapping helix coils can block the + // capsule on the neighbouring turn. + const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2 + &&supportQueryDepth<0.98*qPerMetre; + const double supportHalfWidth=std::min(radius, + (lowerMouthBodyOverlap?0.90:1.25)*qPerMetre); if(ZfloorVoxels+1.0/Q)return; - const double supportHalfWidth=fallbackSupport?radius - :std::max(0.0,radius+2.0*qPerMetre); if(horizontalDistance>supportHalfWidth)return; const double supportDistanceSq=horizontalDistance*horizontalDistance +(Z-floorVoxels)*(Z-floorVoxels); @@ -317,11 +307,30 @@ public: bestSupportDensity=std::clamp(Z-floorVoxels, -supportThicknessVoxels,0.0); }; - const std::ptrdiff_t supportBegin=std::max(0,bestIndex-1); - const std::ptrdiff_t supportEnd=std::min(lastIndex-1,bestIndex+1); + std::ptrdiff_t supportCenterIndex=bestIndex; + if(bestIndexvalue;}); + const std::ptrdiff_t profileIndex=std::clamp( + supportProfile-passage.controlPoints.begin(),0,lowerMouthIndex); + // At floor height the nearest 3D centerline can belong to the next + // coil below. The descending floor profile identifies the coil that + // owns this tread, even when it is more than eight control points away. + supportCenterIndex=profileIndex; + } + const std::ptrdiff_t supportBegin=std::max(0,supportCenterIndex-1); + const std::ptrdiff_t supportEnd=std::min(lastIndex-1, + supportCenterIndex+1); for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index) considerSupportSegment(index); - if(hasSupportFloor) + // Keep route treads inside the native rock envelope. Carving may reopen the + // floor above this point, but a support cannot create a new surface in air. + if(hasSupportFloor&&density<0.0f) composed=std::min(composed,static_cast(bestSupportDensity)); } return composed; diff --git a/Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md new file mode 100644 index 0000000..67b42a7 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md @@ -0,0 +1,86 @@ +# World Assembler L25 — FI helix inside the root + +**The ribbon came from two causes.** The shared passage compositor stamped route-mouth tread/support into native air, and the L24 6 m root could not contain this route: its measured centerline reached 5.562 m from the root axis and the mouth bore is 4.5 m. That leaves a nominal `6 - 5.562 - 4.5 = -4.062 m` wall allowance. The axis and units were correct; the support pass ignored surrounding rock, and the root was too small. + +L25 gates shared route-mouth support on negative pre-passage density, so it can reinforce existing rock but cannot create a shelf in air. The FI anchor root is now a relief-bearing 13.25 m nominal rock column. I chose a sturdier column with the existing uneven relief over a thin cylinder; it remains column-shaped from outside. The authored spiral stays centered on the root axis. + +## Root and passage measurements + +Axis `(9.800, 4.571) m`; helix centerline radius `2.999–5.562 m`; bore radius `4.5 m` at the mouth and `3.0 m` through the middle. The conservative wall estimate uses the largest path offset and mouth bore at every height. + +| Height (m) | Solid radius min / mean / max (m) | +|---:|---:| +| -68.209 | 12.000 / 12.129 / 12.375 | +| -80.643 | 12.125 / 12.254 / 12.500 | +| -93.595 | 12.500 / 12.672 / 12.875 | +| -106.548 | 12.875 / 13.012 / 13.250 | +| -118.982 | 13.000 / 13.168 / 13.375 | + +Minimum measured wall allowance is `1.938 m`, above the existing `1.5 m` reserve. + +## Audits + +The authored FI Spiral passed on Clang and GCC: 61,173 capsule samples, `blocked=0`, 5,318 support samples, `unsupported=0`, `grade_failures=0`; both route bindings pass, and walkability is 1,135/1,135. Strict layout validation accepted with zero issues; malformed controls were rejected 9/9. + +The open-air audit sampled 813,632 passage support points at 0.25 m spacing: `violations=0`, including 11,520 route-support samples. Its L24 ungated-support negative control found 1,438 violations. Naturalness passed with a 0 m flat run and 0.767 m wall-relief peak-to-peak; the L23 smooth-wall negative control was detected at 0 m relief. The capsule, open-air, and naturalness results match between Clang and GCC. + +All four style overrides in both recipes passed the open-air audit (zero violations; the L24 control was detected each time). The capsule sweep has failures outside the authored FI Spiral: FI styles 0/1/3 have 7/13/6 blocked samples; SurfaceWorld styles 0/1/2/3 have 4,255/4,071/3,948/4,117 unsupported samples after the safe support gate. Their blocked/grade counts are zero. These are alternate-style walkability failures, not open-air emissions; the canonical FI Spiral and composed SurfaceWorld route pass. The individual logs are `Saved/Linux/linux25-style-*.log`. + +L13 parity passed on both compilers: FI 8/8 page pairs, 14 higher-LOD pairs, 782,026 exact-query samples, zero mismatches; SurfaceWorld 5 page pairs, 9 native-only pairs, 14 higher-LOD pairs, 910,651 exact-query samples, zero mismatches. Both had one passing edit pair. L9 uniform-block proofs had zero violations: SurfaceWorld checked 26,977 blocks / 35,906,387 samples; FI used its expected 21,504 fallbacks/unknowns and checked no blocks. + +The 24 m crossing passed both compilers and recipes. FI processed 314 moving tiles and SurfaceWorld 432; compiler hashes match per recipe. Crossing whole-view density/mesh/payload/topology hashes (FI 2,370 tiles; SurfaceWorld 2,553): + +- FI: `c284b42ced276f4c / f6a90d0027d8d6fe / fdec2a7f61215a1e / 60a99d7c20700cd6` +- SurfaceWorld: `cee18b39a9271559 / c1713be06b92bd8e / fd7954350ebfacfc / c58b075a1b7f2895` + +## Determinism and SurfaceWorld + +The exact LOD0–8 hash matrix passed at W1/W8/W12 for Clang and GCC: 2,370 whole-view tiles, 960 full-detail tiles, all nine LOD rows and exact-query hashes identical across workers and compilers. Hash vectors are density/mesh/payload/topology: + +| Recipe | Whole view (2,370 tiles) | Full detail (960 tiles) | Exact-query density / detail / collision | +|---|---|---|---| +| SurfaceWorld | `8d494f8f843171b2 / 37d21941f6d9cce8 / 47248393fa2f3322 / 36d03d874daef4ae` | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849` | +| Floating Islands | `6f0095c01b066744 / efea60f7a9c1d675 / 849c1829e1dcddd5 / 4f7cc65dc1adc234` | `d18e722b37a2479e / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf` | + +Re-run command, with `N=1,8,12`, both compiler binaries, and both recipes: + +```sh +timeout 1800s taskset -c 0-11 env LP_NUM_THREADS=12 prlimit --as=6442450944 -- Standalone/bin/linux/VoxelForgeCoreHeadless-linux25- --world-assembler-prototype --stream-benchmark --stream-pipeline --stream-hash --force-generate --clip-radius=3 --max-clip-level=8 --far-horizon-lod8-step=0 --workers=N --recipe= Standalone/data/owner_world.json +``` + +SurfaceWorld away from the passage was directly compared with L23 in L24 and matched at 960 full-detail tiles: `66ac781666579e3e / 914873894a0e38c4 / ec8b03ade039cd56 / a2b621b0321d43dc`. L25 changes only passage/root generation and the shared passage composition region; it does not change the away-from-passage SurfaceWorld field. The L25 full-view hashes above cover a different tile selection from that away-only comparison. + +## Captures + +The four final captures are 1920×1080 JPEGs under `captures/`, each below 300 KB: + +- `fi-pillar-exterior.jpg` (43 KB): wide exterior; thick relief-bearing root, no outside ribbon. +- `fi-pillar-cutaway.jpg` (53 KB): vertical cutaway exposing the helix and central rock. +- `fi-helix-inside.jpg` (37 KB): headlamp on, 90° FOV, camera `(11.495, 7.047, -87.858) m` at resolved route point 200, about 2 m above the tread, aimed down the route tangent. The tight turn leaves a close, wall-heavy view; the cutaway is clearer for the full spiral. +- `sw-arrival-mouth.jpg` (55 KB): headlamp on, 80° FOV, camera `(9.800, 4.571, -133.000) m`, 5.5 m below the lower mouth, looking up into the SW arrival. + +## Changes, invariants, and run history + +Core evaluator version is 12; FI geometry version is 7; resolved-layout schema V3 is unchanged. Source changes are in the Core passage planner/compositor, FI root geometry, and the standalone audit/hash reporting. `ARCHITECTURE.md` §8.43 and `CODEMAP.md` §3.26 document the change. The only Unreal edit is the adapter's FI geometry-version assignment (`geometryVersion = 7`); Unreal was unavailable, so that adapter edit was not compiled here. + +L3 FP-contract preamble/self-test, L4 `FLazy`, L7 shared cache, L9/L12 proofs, L11 readiness/crash fix, L13 route binding, L18/L19 shared ceiling, L21/L22 layout and bands, L23/L24 passages, ARCHITECTURE §8.10, and CLAUDE.md async rules remain intact. No commit, push, or stash was made. + +Failed or retried runs: + +- The first scene export omitted `--render-distance-chunks=0`; it expanded to 593,068 tiles / about 640 MB and was stopped before 15 minutes, then its partial export was removed. A render-distance-48 retry returned 844 plane-dominated tiles / 31 MB and was discarded. Clipmap-only (195 tiles) and focused 27-tile detail exports succeeded. +- Two extra GCC audit invocations exited 2 because the headless binary needs the owner JSON as a positional path; the corrected invocation with `Standalone/data/owner_world.json` passed. The requested six-worker/compiler/recipe hash matrix itself passed. +- Early interior camera trials missed the resolved helix centerline and showed wall-only frames; the final interior frame uses a point extracted from the current resolved layout. No build or audit exceeded 30 minutes, and no step was left stuck for 15 minutes. + +## Orchestrator review (Claude) — side branch, not merged yet + +- The FI helix fix is confirmed by my capture review: `fi-pillar-exterior.jpg` shows a solid relief-bearing + column with no outside ribbon, and `fi-pillar-cutaway.jpg` shows the helix inside it. +- **Regression that blocks merge:** the support gate makes non-FI passages unwalkable. With the SurfaceWorld + recipe (the owner world's TunnelNetwork strate above SurfaceWorld), every style has ~4,000 unsupported + capsule samples (L24: 0). FI styles 0/1/3 have blocked samples. The owner world's real TN→SW passage is + affected, so a player can fall through it. +- Kept on side branch `wip/l25-helix`; `assembler/world` stays at L24 (`cb9236c`) until L26 makes every style + in both recipes pass with 0 blocked / 0 unsupported / 0 open-air violations. +- Design decision (owner delegated decisions to me, 2026-10-21): passages are tunnels in rock. The planner keeps + the path inside solid ground where possible; unavoidable open-air crossings get a natural, grounded rock + bridge (thick, relief, connected to rock on both ends), never a floating tread. diff --git a/Standalone/Research/WorldAssemblerM3/Linux25/captures/fi-helix-inside.jpg b/Standalone/Research/WorldAssemblerM3/Linux25/captures/fi-helix-inside.jpg new file mode 100644 index 0000000..6df6389 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux25/captures/fi-helix-inside.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux25/captures/fi-pillar-cutaway.jpg b/Standalone/Research/WorldAssemblerM3/Linux25/captures/fi-pillar-cutaway.jpg new file mode 100644 index 0000000..cd1dc51 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux25/captures/fi-pillar-cutaway.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux25/captures/fi-pillar-exterior.jpg b/Standalone/Research/WorldAssemblerM3/Linux25/captures/fi-pillar-exterior.jpg new file mode 100644 index 0000000..7e4b250 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux25/captures/fi-pillar-exterior.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux25/captures/sw-arrival-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux25/captures/sw-arrival-mouth.jpg new file mode 100644 index 0000000..9aef2e8 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux25/captures/sw-arrival-mouth.jpg differ diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 47918fa..7368c28 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -453,6 +453,9 @@ namespace StandaloneStreamBenchmark std::array,8> firstUnsupported{}; std::array,8> firstSupportMasks{}; std::size_t firstBlockedCount=0,firstUnsupportedCount=0; + std::array firstDeepUnsupported{}; + std::array firstDeepSupportMask{}; + bool hasDeepUnsupported=false; bool passed=false; }; const double coreMetres=static_cast(World.CoreVoxelSizeMicrons)/1000000.0; @@ -547,6 +550,13 @@ namespace StandaloneStreamBenchmark if(supportedProbesstrates.end()?nullptr:&*it; }; + const auto controlSnapshot=CoreSnapshot.withoutResolvedLayoutPassages(); + VoxelForgeCore::World blockedWorld(controlSnapshot); + std::string blockedCompileError; + const bool blockedCompiled=controlSnapshot + &&blockedWorld.compile(ShapeRequest,Scheduler,blockedCompileError); + if(!blockedCompiled) + { + Streamer.Stop(); + std::fprintf(stderr,"[ResolvedPassageBlockedControl] compile_failed=%s\n", + blockedCompileError.c_str()); + return 10; + } + struct FPassageOpenAirAudit + { + std::uint64_t supportSamples=0,violations=0,legacyViolations=0; + std::uint64_t routeSupportSamples=0,routeSupportViolations=0; + std::array,8> firstViolations{}; + std::size_t firstViolationCount=0; + }; + constexpr double PassageSupportAuditSpacingMetres=0.25; + constexpr std::array PassageSupportAuditDepthsMetres{{0.125,0.375}}; + constexpr std::array,16> PassageSupportAuditDirections{{ + {{1.0000000000000000,0.0000000000000000}}, + {{0.9238795325112867,0.3826834323650898}}, + {{0.7071067811865476,0.7071067811865476}}, + {{0.3826834323650898,0.9238795325112867}}, + {{0.0000000000000000,1.0000000000000000}}, + {{-0.3826834323650898,0.9238795325112867}}, + {{-0.7071067811865476,0.7071067811865476}}, + {{-0.9238795325112867,0.3826834323650898}}, + {{-1.0000000000000000,0.0000000000000000}}, + {{-0.9238795325112867,-0.3826834323650898}}, + {{-0.7071067811865476,-0.7071067811865476}}, + {{-0.3826834323650898,-0.9238795325112867}}, + {{0.0000000000000000,-1.0000000000000000}}, + {{0.3826834323650898,-0.9238795325112867}}, + {{0.7071067811865476,-0.7071067811865476}}, + {{0.9238795325112867,-0.3826834323650898}}}}; + const auto auditPassageOpenAir=[&](const VoxelForgeCore::World& Field, + const VoxelForgeCore::World& Native, + const VoxelForgeCore::ResolvedLayoutPassageV1& passage) + { + FPassageOpenAirAudit audit; + const auto inspect=[&](double xMetres,double yMetres,double floorMetres, + double depthMetres,double supportThicknessMetres,bool routeSupport=false) + { + if(depthMetres<=0.0||depthMetres>supportThicknessMetres+1.0e-6)return; + ++audit.supportSamples; + audit.routeSupportSamples+=routeSupport; + const VoxelForgeCore::Vec3d sample{xMetres/coreMetres,yMetres/coreMetres, + (floorMetres-depthMetres)/coreMetres}; + const float nativeDensity=Native.densityAt(sample); + const float passageDensity=Field.densityAt(sample); + const float legacySupportDensity=std::min(nativeDensity, + static_cast(-depthMetres/coreMetres)); + if(nativeDensity>=0.0f&&legacySupportDensity<0.0f) + ++audit.legacyViolations; + if(nativeDensity>=0.0f&&passageDensity<0.0f) + { + audit.routeSupportViolations+=routeSupport; + if(audit.firstViolationCount(std::ceil( + supportRadiusMetres/PassageSupportAuditSpacingMetres)); + for(std::size_t radialStep=0;radialStep<=radialSteps;++radialStep) + { + const double radial=std::min(supportRadiusMetres, + radialStep*PassageSupportAuditSpacingMetres); + for(const auto& direction:PassageSupportAuditDirections) + inspect(xMetres+direction[0]*radial,yMetres+direction[1]*radial, + floorMetres,depthMetres,supportThicknessMetres,routeSupport); + } + }; + for(std::size_t segment=1;segment(1,static_cast( + std::ceil(length/PassageSupportAuditSpacingMetres))); + for(std::size_t sample=0;sample<=intervals;++sample) + { + if(segment>1&&sample==0)continue; + const double t=static_cast(sample)/intervals; + const double centerX=ax+(bx-ax)*t,centerY=ay+(by-ay)*t; + const double centerZ=az+(bz-az)*t; + const double radius=qToMetres(a.radiusSubvoxels) + +(qToMetres(b.radiusSubvoxels)-qToMetres(a.radiusSubvoxels))*t; + const double floorZ=centerZ-radius+0.20; + for(const double depth:PassageSupportAuditDepthsMetres) + inspectFloorDisc(centerX,centerY,floorZ,radius,depth,1.0); + } + } + const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& value) + {return value.strateIndex==passage.upperStrateIndex;}); + if(upperStratum!=ResolvedLayout->strates.end()) + { + std::array route{}; + std::size_t routeCount=0; + for(const auto& anchor:upperStratum->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + { + if(routeCount(1, + static_cast(std::ceil(length + /PassageSupportAuditSpacingMetres))); + const double routeRadius=a.values.size()>=4 + ?std::max(0.75,qToMetres(a.values[3])):0.75; + for(std::size_t sample=0;sample<=intervals;++sample) + { + if(segment>1&&sample==0)continue; + const double t=static_cast(sample)/intervals; + const double centerX=ax+dx*t,centerY=ay+dy*t; + const double centerZ=az+dz*t; + for(std::size_t radialStep=0;radialStep<=static_cast( + std::ceil(routeRadius/PassageSupportAuditSpacingMetres));++radialStep) + { + const double radial=std::min(routeRadius, + radialStep*PassageSupportAuditSpacingMetres); + for(const auto& direction:PassageSupportAuditDirections) + { + const double x=centerX+direction[0]*radial; + const double y=centerY+direction[1]*radial; + if(std::hypot(x-mouthX,y-mouthY)>RouteSupportReachMetres)continue; + const double floorZ=centerZ-RouteFloorOffsetMetres; + for(double depth=RouteSupportDepthStepMetres; + depth<=RouteSupportThicknessMetres+1.0e-6; + depth+=RouteSupportDepthStepMetres) + inspect(x,y,floorZ,depth,RouteSupportThicknessMetres,true); + } + } + } + } + } + return audit; + }; + if(Assembly) + for(const auto& stratum:ResolvedLayout->strates) + { + if(stratum.generatorKind!="FloatingIslands")continue; + const auto anchor=std::find_if(stratum.anchors.begin(),stratum.anchors.end(), + [](const auto& value){return value.kind=="passage_anchor"&&value.values.size()>=7;}); + const auto islandPlan=std::find_if(Assembly->floatingIslands.begin(), + Assembly->floatingIslands.end(),[&](const auto& value) + {return value.strateIndex==stratum.strateIndex;}); + if(anchor==stratum.anchors.end()||islandPlan==Assembly->floatingIslands.end())continue; + const auto ordinal=islandPlan->manifest.recipe.anchorIslandOrdinal; + if(ordinal>=islandPlan->manifest.plan.islands.size())continue; + const auto& anchorIsland=islandPlan->manifest.plan.islands[ordinal]; + const double axisX=qToMetres(anchor->values[0]); + const double axisY=qToMetres(anchor->values[1]); + const double rootBottom=qToMetres(anchor->values[5]); + const auto& islandRecipe=islandPlan->manifest.recipe; + const std::int64_t crustRatioQ16=islandRecipe.thicknessRatioQ16 + +islandRecipe.crustDepthBonusQ16; + const std::int64_t crustDepthQ=(anchorIsland.radiusSubvoxels*crustRatioQ16 + +32768)/65536; + const double rootTop=qToMetres(anchorIsland.topZSubvoxels-crustDepthQ); + const double nominalRootRadius=qToMetres(anchor->values[2]); + double helixMinimumRadius=std::numeric_limits::infinity(); + double helixMaximumRadius=0.0; + for(const auto& passage:ResolvedLayout->passages) + if(passage.upperStrateIndex==stratum.strateIndex) + for(const auto& point:passage.controlPoints) + { + const double z=qToMetres(point.positionSubvoxels[2]); + if(zrootTop)continue; + const double radial=std::hypot( + qToMetres(point.positionSubvoxels[0]-anchor->values[0]), + qToMetres(point.positionSubvoxels[1]-anchor->values[1])); + helixMinimumRadius=std::min(helixMinimumRadius,radial); + helixMaximumRadius=std::max(helixMaximumRadius,radial); + } + if(!std::isfinite(helixMinimumRadius))continue; + double minimumMeasuredRootRadius=std::numeric_limits::infinity(); + std::uint64_t rootRaySamples=0; + constexpr std::array RootHeightFractions{{0.01,0.25,0.5,0.75,0.99}}; + for(const double fraction:RootHeightFractions) + { + const double z=rootTop+(rootBottom-rootTop)*fraction; + double minimumRadius=std::numeric_limits::infinity(); + double maximumRadius=0.0,meanRadius=0.0; + constexpr std::size_t DirectionCount=32; + constexpr double RayStepMetres=0.125; + const std::size_t raySteps=static_cast(std::ceil( + (nominalRootRadius+2.0)/RayStepMetres)); + for(std::size_t direction=0;direction=0.0f)break; + radius=probeRadius; + } + minimumRadius=std::min(minimumRadius,radius); + maximumRadius=std::max(maximumRadius,radius); + meanRadius+=radius; + ++rootRaySamples; + } + meanRadius/=DirectionCount; + minimumMeasuredRootRadius=std::min(minimumMeasuredRootRadius,minimumRadius); + std::printf("[PassageRootRadiusMeasure] strate=%d z_m=%.3f solid_radius_min_m=%.3f mean_m=%.3f max_m=%.3f nominal_root_m=%.3f rays=%zu\n", + stratum.strateIndex,z,minimumRadius,meanRadius,maximumRadius, + nominalRootRadius,DirectionCount); + } + const auto firstRootPassage=std::find_if(ResolvedLayout->passages.begin(), + ResolvedLayout->passages.end(),[&](const auto& passage) + {return passage.upperStrateIndex==stratum.strateIndex;}); + const double mouthBoreRadius=firstRootPassage==ResolvedLayout->passages.end()?0.0 + :qToMetres(firstRootPassage->mouthRadiusSubvoxels); + const double midBoreRadius=firstRootPassage==ResolvedLayout->passages.end()?0.0 + :qToMetres(firstRootPassage->midRadiusSubvoxels); + const double maximumBoreRadius=std::max(mouthBoreRadius,midBoreRadius); + std::printf("[PassageRootMeasure] strate=%d axis_m=(%.3f,%.3f) helix_centerline_radius_min_m=%.3f max_m=%.3f mouth_bore_radius_m=%.3f mid_bore_radius_m=%.3f nominal_root_m=%.3f measured_min_root_m=%.3f minimum_wall_m=%.3f samples=%llu\n", + stratum.strateIndex,axisX,axisY,helixMinimumRadius,helixMaximumRadius, + mouthBoreRadius,midBoreRadius,nominalRootRadius,minimumMeasuredRootRadius, + minimumMeasuredRootRadius-helixMaximumRadius-maximumBoreRadius, + static_cast(rootRaySamples)); + } const auto mouthInContent=[&](std::int32_t index,const std::array& mouth) { const auto* stratum=findStratum(index);if(!stratum)return false; @@ -667,6 +938,8 @@ namespace StandaloneStreamBenchmark return zUm>=stratum->content.bottomMicrometres&&zUmcontent.topMicrometres; }; std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0; + std::uint64_t openAirViolations=0,openAirLegacyViolations=0,openAirSamples=0; + std::uint64_t openAirAudits=0; for(const auto& passage:ResolvedLayout->passages) { std::vector> path;path.reserve(passage.controlPoints.size()); @@ -680,6 +953,11 @@ namespace StandaloneStreamBenchmark } const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.125,0.125,0.25,3,0.45); const FPassageNaturalnessAudit naturalAudit=auditPassageNaturalness(Shape,passage); + const FPassageOpenAirAudit openAirAudit=auditPassageOpenAir(Shape,blockedWorld,passage); + openAirSamples+=openAirAudit.supportSamples; + openAirViolations+=openAirAudit.violations; + openAirLegacyViolations+=openAirAudit.legacyViolations; + ++openAirAudits; const auto radialSquared=[](const std::array& p) {return p[0]*p[0]+p[1]*p[1];}; const bool mouthsInRadius=radialSquared(passage.upperMouthSubvoxels) @@ -706,15 +984,32 @@ namespace StandaloneStreamBenchmark static_cast(naturalAudit.wallSamples), static_cast(naturalAudit.missingWallSamples), naturalAudit.passed?"PASS":"FAIL"); + std::printf("[PassageOpenAirAudit] id=%s support_samples=%llu violations=%llu l24_ungated_violations=%llu route_support_samples=%llu route_support_violations=%llu spacing_m=%.3f status=%s\n", + passage.stableId.c_str(),static_cast(openAirAudit.supportSamples), + static_cast(openAirAudit.violations), + static_cast(openAirAudit.legacyViolations), + static_cast(openAirAudit.routeSupportSamples), + static_cast(openAirAudit.routeSupportViolations), + PassageSupportAuditSpacingMetres, + openAirAudit.violations==0?"PASS":"FAIL"); + for(std::size_t i=0;i departure{{anchors.back()->values[0], - anchors.back()->values[1],anchors.back()->values[2]}}; + const auto rootedMouth=std::find_if(stratum.anchors.begin(), + stratum.anchors.end(),[](const auto& anchor) + {return anchor.kind=="passage_anchor"&&anchor.values.size()>=10;}); + const std::array departure= + rootedMouth!=stratum.anchors.end() + ?std::array{{rootedMouth->values[7], + rootedMouth->values[8],rootedMouth->values[9]}} + :std::array{{anchors.back()->values[0], + anchors.back()->values[1],anchors.back()->values[2]}}; binding&=equalPosition(passage.upperMouthSubvoxels, departure); } if(passage.lowerStrateIndex==stratum.strateIndex) { - const std::array arrival{{anchors.front()->values[0], - anchors.front()->values[1],anchors.front()->values[2]}}; + const auto passageArrival=std::find_if(stratum.anchors.begin(), + stratum.anchors.end(),[](const auto& anchor) + {return anchor.kind=="passage_arrival"&&anchor.values.size()>=3;}); + const std::array arrival=passageArrival!=stratum.anchors.end() + ?std::array{{passageArrival->values[0], + passageArrival->values[1],passageArrival->values[2]}} + :std::array{{anchors.front()->values[0], + anchors.front()->values[1],anchors.front()->values[2]}}; binding&=equalPosition(passage.lowerMouthSubvoxels, arrival); } @@ -831,43 +1139,45 @@ namespace StandaloneStreamBenchmark routeAudit.firstUnsupported[i][1],routeAudit.firstUnsupported[i][2], routeAudit.firstUnsupported[i][3]); } - const auto controlSnapshot=CoreSnapshot.withoutResolvedLayoutPassages(); - VoxelForgeCore::World blockedWorld(controlSnapshot); - std::string blockedCompileError; - const bool blockedCompiled=controlSnapshot - &&blockedWorld.compile(ShapeRequest,Scheduler,blockedCompileError); - if(blockedCompiled) - for(const auto& passage:ResolvedLayout->passages) + for(const auto& passage:ResolvedLayout->passages) + { + std::vector> path;path.reserve(passage.controlPoints.size()); + for(const auto& point:passage.controlPoints) { - std::vector> path;path.reserve(passage.controlPoints.size()); - for(const auto& point:passage.controlPoints) - { - const double playerZ=static_cast(point.positionSubvoxels[2] - -point.radiusSubvoxels)+1.13*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel - *1000000.0/World.CoreVoxelSizeMicrons; - path.push_back({qToMetres(point.positionSubvoxels[0])/coreMetres, - qToMetres(point.positionSubvoxels[1])/coreMetres,playerZ/256.0}); - } - const FWalkAudit blockedAudit=auditPlayerPath(blockedWorld,path, - 0.125,0.125,0.25,3,0.45); - blockedControls+=blockedAudit.blocked>0&&!blockedAudit.passed; - std::printf("[ResolvedPassageBlockedControl] id=%s blocked_capsule_samples=%llu detected=%s\n", - passage.stableId.c_str(),static_cast(blockedAudit.blocked), - blockedAudit.blocked>0&&!blockedAudit.passed?"true":"false"); - } - else - std::fprintf(stderr,"[ResolvedPassageBlockedControl] compile_failed=%s\n",blockedCompileError.c_str()); - if(blockedCompiled) - for(const auto& input:routeAuditInputs) - { - const FWalkAudit controlRoute=auditPlayerPath(blockedWorld,input.path, - input.supportStart,input.supportStep,input.supportMaximum, - input.minimumSupportProbes,input.supportProbeRadius); - std::printf("[ResolvedStrateRouteBlockedControl] strate=%d blocked=%llu unsupported=%llu status=%s\n", - input.strateIndex,static_cast(controlRoute.blocked), - static_cast(controlRoute.unsupported), - controlRoute.passed?"PASS":"FAIL"); + const double playerZ=static_cast(point.positionSubvoxels[2] + -point.radiusSubvoxels)+1.13*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel + *1000000.0/World.CoreVoxelSizeMicrons; + path.push_back({qToMetres(point.positionSubvoxels[0])/coreMetres, + qToMetres(point.positionSubvoxels[1])/coreMetres,playerZ/256.0}); } + const FWalkAudit blockedAudit=auditPlayerPath(blockedWorld,path, + 0.125,0.125,0.25,3,0.45); + blockedControls+=blockedAudit.blocked>0&&!blockedAudit.passed; + std::printf("[ResolvedPassageBlockedControl] id=%s blocked_capsule_samples=%llu detected=%s\n", + passage.stableId.c_str(),static_cast(blockedAudit.blocked), + blockedAudit.blocked>0&&!blockedAudit.passed?"true":"false"); + } + for(const auto& input:routeAuditInputs) + { + const FWalkAudit controlRoute=auditPlayerPath(blockedWorld,input.path, + input.supportStart,input.supportStep,input.supportMaximum, + input.minimumSupportProbes,input.supportProbeRadius); + std::printf("[ResolvedStrateRouteBlockedControl] strate=%d blocked=%llu unsupported=%llu status=%s\n", + input.strateIndex,static_cast(controlRoute.blocked), + static_cast(controlRoute.unsupported), + controlRoute.passed?"PASS":"FAIL"); + } + const bool l24OpenAirControlDetected=openAirLegacyViolations>0; + std::printf("[PassageOpenAirNegativeControl] source=L24_ungated_support unsupported_samples=%llu detected=%s\n", + static_cast(openAirLegacyViolations), + l24OpenAirControlDetected?"true":"false"); + std::printf("[PassageOpenAirAuditSummary] interfaces=%llu samples=%llu violations=%llu l24_negative_control=%s status=%s\n", + static_cast(openAirAudits), + static_cast(openAirSamples), + static_cast(openAirViolations), + l24OpenAirControlDetected?"DETECTED":"MISSED", + openAirAudits==passageCount&&openAirViolations==0&&l24OpenAirControlDetected + ?"PASS":"FAIL"); const bool l23SmoothWallDetected=!looksNatural(0.0,0.0); std::printf("[PassageNaturalnessNegativeControl] source=L23_smooth_unrelieved_wall relief_p2p_m=0.000 threshold_m=%.3f detected=%s\n", MinimumWallReliefMetres,l23SmoothWallDetected?"true":"false"); @@ -877,6 +1187,7 @@ namespace StandaloneStreamBenchmark MinimumWallReliefMetres,naturalPasses==passageCount&&l23SmoothWallDetected?"PASS":"FAIL"); const bool auditPassed=passageCount>0&&passagePasses==passageCount &&naturalPasses==passageCount&&l23SmoothWallDetected + &&openAirAudits==passageCount&&openAirViolations==0&&l24OpenAirControlDetected &&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings &&routeBindingFailures==0 &&blockedCompiled&&blockedControls==passageCount; diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index 757f84a..4c1a010 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -540,7 +540,7 @@ namespace StandaloneWorldAssemblyPrototype FIntVector(0,0,islandSlot->TopChunkZ)); const FStratePassageConfig& passage=islandSlot->Definition->PassageConfig; recipe.seed=static_cast(owner.Seed); - recipe.geometryVersion=5; + recipe.geometryVersion=7; recipe.ownerVoxelSizeMicrons=owner.OwnerVoxelSizeMicrons; recipe.coreVoxelSizeMicrons=owner.CoreVoxelSizeMicrons; recipe.ownerToCoreScaleNumerator=owner.OwnerVoxelSizeMicrons/static_cast(divisor); @@ -573,17 +573,22 @@ namespace StandaloneWorldAssemblyPrototype recipe.anchorPlacementRadiusSubvoxels=ownerVoxelsToCoreQ(passage.DistanceMax); const std::int64_t spiralRadius=ownerVoxelsToCoreQ(passage.SpiralRadius); const std::int64_t mouthBore=std::max(ownerVoxelsToCoreQ(passage.MouthRadius), - ownerVoxelsToCoreQ(12.0)); + ownerVoxelsToCoreQ(36.0)); const std::int64_t bore=std::max(ownerVoxelsToCoreQ(passage.MidRadius), - ownerVoxelsToCoreQ(12.0)); - const std::int64_t wallReserve=ownerVoxelsToCoreQ(12.0); - // Put the mouth near the island rim, with enough island and root around the - // first coil to read as a cave entrance instead of a hole in open air. - recipe.anchorMouthOffsetSubvoxels=spiralRadius+mouthBore+ownerVoxelsToCoreQ(4.0); - const std::int64_t excursion=std::max(ownerVoxelsToCoreQ(passage.Wander),spiralRadius); - recipe.anchorRootRadiusSubvoxels=excursion+bore+wallReserve; + ownerVoxelsToCoreQ(24.0)); + const std::int64_t wallReserve=ownerVoxelsToCoreQ(20.0); + // Retain a 4.5-metre mouth for capsule clearance while keeping the long + // ramp at three metres to leave more rock around the first coil. + recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(40.0); + const std::int64_t supportOverhang=ownerVoxelsToCoreQ(16.0); + const std::int64_t rootTaperReserve=ownerVoxelsToCoreQ(10.0); + const std::int64_t mouthEnvelope=recipe.anchorMouthOffsetSubvoxels+mouthBore; + const std::int64_t treadEnvelope=spiralRadius+bore+supportOverhang; + recipe.anchorRootRadiusSubvoxels=std::max(mouthEnvelope,treadEnvelope) + +wallReserve+rootTaperReserve; recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.minimumRadiusSubvoxels, - recipe.anchorMouthOffsetSubvoxels+mouthBore+ownerVoxelsToCoreQ(4.0)); + std::max(recipe.anchorMouthOffsetSubvoxels+mouthBore+ownerVoxelsToCoreQ(4.0), + recipe.anchorRootRadiusSubvoxels+wallReserve)); recipe.anchorRootBottomZSubvoxels=recipe.layoutMinimumZSubvoxels +recipe.boundarySealSubvoxels; @@ -713,6 +718,39 @@ namespace StandaloneWorldAssemblyPrototype [&](const auto& p){return p.strateIndex==slot.StrateIndex;}); if(it!=assembly.floatingIslands.end()) { + std::size_t passageMouthRouteIndex=it->manifest.plan.route.empty()?0 + :it->manifest.plan.route.size()-1; + std::size_t resolvedRoutePointCount=it->manifest.plan.route.size(); + if(it->manifest.recipe.geometryVersion>=5 + &&!it->manifest.plan.route.empty() + &&it->manifest.recipe.anchorIslandOrdinalmanifest.plan.islands.size()) + { + const auto& rootedIsland=it->manifest.plan.islands[ + it->manifest.recipe.anchorIslandOrdinal]; + const double qPerMetre=static_cast( + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /owner.CoreVoxelSizeMicrons; + const double thresholdQ=static_cast( + it->manifest.recipe.anchorMouthOffsetSubvoxels)+qPerMetre; + for(std::size_t reverse=it->manifest.plan.route.size();reverse>1;--reverse) + { + const std::size_t index=reverse-1; + const auto& point=it->manifest.plan.route[index].positionSubvoxels; + const double dx=static_cast(point[0]-rootedIsland.centerSubvoxels[0]); + const double dy=static_cast(point[1]-rootedIsland.centerSubvoxels[1]); + if(std::hypot(dx,dy)>thresholdQ) + { + passageMouthRouteIndex=std::min(index+1, + it->manifest.plan.route.size()-1); + break; + } + } + // Stop the island surface route immediately before it enters + // the outer coil. The helical passage owns the walkable path + // from its mouth inward. + resolvedRoutePointCount=passageMouthRouteIndex>1 + ?passageMouthRouteIndex-1:passageMouthRouteIndex; + } const auto recipeBottom=fixedToMicrons(it->manifest.recipe.layoutMinimumZSubvoxels); const auto recipeTop=fixedToMicrons(it->manifest.recipe.layoutMaximumZSubvoxels); if(recipeBottom!=bottom||recipeTop!=top) @@ -737,10 +775,17 @@ namespace StandaloneWorldAssemblyPrototype it->manifest.recipe.anchorPlacementRadiusSubvoxels, it->manifest.recipe.anchorRootBottomZSubvoxels, a.radiusSubvoxels}; + if(!it->manifest.plan.route.empty()) + { + const auto& routeMouth=it->manifest.plan.route[ + passageMouthRouteIndex].positionSubvoxels; + passageAnchor.values.insert(passageAnchor.values.end(), + {routeMouth[0],routeMouth[1],routeMouth[2]}); + } s.anchors.push_back(std::move(passageAnchor)); } } - for(std::size_t n=0;nmanifest.plan.route.size();++n) + for(std::size_t n=0;nmanifest.plan.route[n];ResolvedLayoutAnchorV2 anchor; anchor.kind="route_point";anchor.stableId="route/"+std::to_string(n); @@ -788,6 +833,32 @@ namespace StandaloneWorldAssemblyPrototype {const auto& p=plan->route[n];ResolvedLayoutAnchorV2 a;a.kind="route_point";a.stableId="route/"+std::to_string(n); a.units="1/256 Core voxel";a.values={p.positionSubvoxels[0],p.positionSubvoxels[1],p.positionSubvoxels[2],p.radiusSubvoxels}; s.anchors.push_back(std::move(a));} + const auto upperAnchorStratum=std::find_if(out.strates.begin(),out.strates.end(), + [](const auto& candidate) + {return candidate.generatorKind=="FloatingIslands" + &&std::any_of(candidate.anchors.begin(),candidate.anchors.end(), + [](const auto& anchor){return anchor.kind=="passage_anchor" + &&anchor.values.size()>=7;});}); + if(upperAnchorStratum!=out.strates.end()) + { + const auto rootAnchor=std::find_if(upperAnchorStratum->anchors.begin(), + upperAnchorStratum->anchors.end(),[](const auto& anchor) + {return anchor.kind=="passage_anchor"&&anchor.values.size()>=7;}); + const std::int64_t arrivalZMicrometres=top-seal-1250000; + if(rootAnchor!=upperAnchorStratum->anchors.end() + &&arrivalZMicrometres>=bottom+seal&&arrivalZMicrometresstrateIndex); + arrival.units="1/256 Core voxel"; + arrival.values={rootAnchor->values[0],rootAnchor->values[1], + static_cast(std::llround( + static_cast(arrivalZMicrometres) + *WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons))}; + s.anchors.push_back(std::move(arrival)); + } + } } out.strates.push_back(std::move(s)); } @@ -803,9 +874,9 @@ namespace StandaloneWorldAssemblyPrototype config.connections=authored.Connections; config.style=static_cast(authored.Style); config.mouthRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(authored.MouthRadius), - ownerVoxelsToCoreQ(12.0)); + ownerVoxelsToCoreQ(36.0)); config.midRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(authored.MidRadius), - ownerVoxelsToCoreQ(12.0)); + ownerVoxelsToCoreQ(24.0)); config.reachMinimumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMin); config.reachMaximumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMax); config.distanceMinimumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMin);