diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 0400ef9..581569d 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -327,6 +327,9 @@ namespace VoxelForgeCore // boundary-face bit mask (bit order: -X,+X,-Y,+Y,-Z,+Z). std::vector skirtFaceMasks; std::vector debugDensitySamples; + std::array debugDensityOriginVoxels{}; + std::array debugDensityDimensions{}; + std::int32_t debugDensityStepVoxels = 0; MeshGroups groups; TileGenerationStats diagnostics; std::uint64_t densitySampleCount = 0; @@ -512,7 +515,8 @@ namespace VoxelForgeCore // boundary-blended host rock from widening the authored passage wall. // v238 plans one resolved passage spline with explicit landing chambers. // v239 plans their floor planes as the passage's continuous floor profile. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 239; + // v240 adds level end runs and rejects passage tube overlaps and hairpins. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 240; enum class FieldChannel : std::uint32_t { @@ -1336,6 +1340,69 @@ namespace VoxelForgeCore std::int64_t clearHeightSubvoxels = 0; }; + inline std::int64_t ResolvedLayoutPassageLandingDiscRadiusSubvoxels( + std::int64_t mouthRadiusSubvoxels, + std::uint32_t coreVoxelSizeMicrometres) noexcept + { + if(coreVoxelSizeMicrometres==0)return 0; + const std::int64_t qPerMetre=static_cast(std::llround( + static_cast(WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /coreVoxelSizeMicrometres)); + return std::max(3*qPerMetre, + mouthRadiusSubvoxels+qPerMetre); + } + + inline bool IsInsideResolvedLayoutPassageLandingDiscV1( + const std::array& centerSubvoxels, + std::int64_t radiusSubvoxels,double xSubvoxels,double ySubvoxels) noexcept + { + if(radiusSubvoxels<=0)return false; + const double dx=xSubvoxels-static_cast(centerSubvoxels[0]); + const double dy=ySubvoxels-static_cast(centerSubvoxels[1]); + const double radius=static_cast(radiusSubvoxels); + return dx*dx+dy*dy<=radius*radius; + } + + inline bool IsInsideResolvedLayoutPassageLandingRimV1( + const std::array& centerSubvoxels, + std::int64_t radiusSubvoxels,std::int64_t halfWidthSubvoxels, + double xSubvoxels,double ySubvoxels) noexcept + { + if(radiusSubvoxels<=0||halfWidthSubvoxels<0)return false; + const double dx=xSubvoxels-static_cast(centerSubvoxels[0]); + const double dy=ySubvoxels-static_cast(centerSubvoxels[1]); + return std::abs(std::hypot(dx,dy)-static_cast(radiusSubvoxels)) + <=static_cast(halfWidthSubvoxels); + } + + // Shared landing-volume predicate for planners, the passage composer, and audits. + // Query coordinates use the same world subvoxel units as the resolved passage. + inline double ResolvedLayoutPassageLandingFloorAtXYSubvoxels( + const ResolvedLayoutPassageLandingChamberV1& chamber, + double xSubvoxels,double ySubvoxels) noexcept + { + return static_cast(chamber.floorCenterSubvoxels[2]) + +static_cast(chamber.floorGradientQ16[0])/65536.0 + *(xSubvoxels-static_cast(chamber.floorCenterSubvoxels[0])) + +static_cast(chamber.floorGradientQ16[1])/65536.0 + *(ySubvoxels-static_cast(chamber.floorCenterSubvoxels[1])); + } + + inline bool IsInsideResolvedLayoutPassageLandingChamberV1( + const ResolvedLayoutPassageLandingChamberV1& chamber, + double xSubvoxels,double ySubvoxels,double zSubvoxels) noexcept + { + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0)return false; + const double dx=xSubvoxels-static_cast(chamber.floorCenterSubvoxels[0]); + const double dy=ySubvoxels-static_cast(chamber.floorCenterSubvoxels[1]); + const double radius=static_cast(chamber.radiusSubvoxels); + const double floor=ResolvedLayoutPassageLandingFloorAtXYSubvoxels( + chamber,xSubvoxels,ySubvoxels); + return dx*dx+dy*dy<=radius*radius + &&zSubvoxels>=floor + &&zSubvoxels<=floor+static_cast(chamber.clearHeightSubvoxels); + } + struct ResolvedLayoutPassageV1 { std::string stableId; @@ -1348,6 +1415,8 @@ namespace VoxelForgeCore std::array lowerMouthSubvoxels{}; std::int64_t mouthRadiusSubvoxels = 0; std::int64_t midRadiusSubvoxels = 0; + // Planned flat distance from each landing, at least R+1 m. + std::int64_t endRunSubvoxels = 0; // Derived from the mouth binding; the main descent is monotone through this point. std::size_t lowerMouthControlIndex = 0; ResolvedLayoutPassageLandingChamberV1 upperLandingChamber; @@ -1380,6 +1449,47 @@ namespace VoxelForgeCore std::int64_t cascadeLedgeSubvoxels = 0; }; + struct ResolvedLayoutPassageSelfOverlapAuditV1 + { + std::uint64_t segmentPairs = 0; + std::uint64_t measuredSegmentPairs = 0; + std::uint64_t localExemptSegmentPairs = 0; + std::uint64_t overlappingSegmentPairs = 0; + std::uint64_t firstOverlapSegmentA = 0; + std::uint64_t firstOverlapSegmentB = 0; + std::uint64_t firstLandingColumnSegment = 0; + bool firstLandingColumnIsLower = false; + std::uint64_t landingColumnViolations = 0; + double maximumEndTurnDegrees = 0.0; + std::uint32_t endTurnViolations = 0; + double minimumClearanceMetres = 0.0; + bool hasOverlapLocation = false; + bool hasMeasuredCoverage = false; + bool passed = false; + }; + VFCORE_API ResolvedLayoutPassageSelfOverlapAuditV1 + AuditResolvedLayoutPassageSelfOverlapV1( + const ResolvedLayoutPassageV1& passage, + std::uint32_t coreVoxelSizeMicrometres); + + struct ResolvedLayoutPassageEndRunAuditV1 + { + double requiredEndRunMetres = 0.0; + double measuredStartMetres = 0.0; + double measuredEndMetres = 0.0; + double startMaximumFloorDeviationMetres = 0.0; + double endMaximumFloorDeviationMetres = 0.0; + double startMaximumGradeDegrees = 0.0; + double endMaximumGradeDegrees = 0.0; + bool hasMeasuredCoverage = false; + bool passed = false; + }; + VFCORE_API ResolvedLayoutPassageEndRunAuditV1 + AuditResolvedLayoutPassageEndRunsV1( + const ResolvedLayoutPassageV1& passage, + std::uint32_t coreVoxelSizeMicrometres, + std::int64_t endRunSubvoxels = 0); + struct VFCORE_API ResolvedLayoutSnapshotV3 { std::uint32_t schemaVersion = ResolvedLayoutSchemaVersionV3; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 96322ee..56e8e06 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -652,6 +652,7 @@ namespace <(WorldAssemblySubvoxelsPerVoxel) + *1000000.0/coreVoxelSizeMicrometres; const std::int64_t playerCenterAboveFloorQ=static_cast(std::llround( 1.13*WorldAssemblySubvoxelsPerVoxel*1000000.0/coreVoxelSizeMicrometres)); for(std::size_t i=0;i0) + { + const std::int64_t qPerMetre=static_cast( + static_cast(WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /std::max(1,coreVoxelSizeMicrometres)+0.5); + const std::int64_t minimumRadius=std::max(3*qPerMetre, + passage.mouthRadiusSubvoxels+qPerMetre); + if(passage.endRunSubvoxels& p) { const auto x=p[0],y=p[1],r=passage.placementRadiusSubvoxels; @@ -1373,6 +1387,26 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, add("passage_mouth_binding",passage.stableId+" has no control point bound to its lower mouth"); lowerMouthControlIndex=passage.controlPoints.size()-1; } + std::vector passageHorizontalAlong(passage.controlPoints.size(),0); + for(std::size_t i=1;i(std::abs(b[0]-a[0])); + const auto dy=static_cast(std::abs(b[1]-a[1])); + const auto square=dx*dx+dy*dy; + auto length=PassageIsqrt(square); + if(length*length(std::llround( + static_cast(WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /std::max(1,coreVoxelSizeMicrometres))); + const std::uint64_t straightEndRunAllowance=passage.endRunSubvoxels>0 + ?static_cast(passage.endRunSubvoxels + +std::max(1,endRunScale/100)):0; for(std::size_t i=0;i(dx*dx+dy*dy)); - const std::int64_t signedDz=floorProfile(i,point) - -floorProfile(i-1,previous); + const std::int64_t signedDz=(point.positionSubvoxels[2]-point.radiusSubvoxels) + -(previous.positionSubvoxels[2]-previous.radiusSubvoxels); const std::int64_t dz=std::abs(signedDz); + const double gradeDegrees=horizontal>0 + ?std::atan2(static_cast(dz),static_cast(horizontal)) + *180.0/3.14159265358979323846 + :90.0; const bool landingExtension=i>lowerMouthControlIndex; + const bool segmentTouchesEndRun=i<=lowerMouthControlIndex + &&straightEndRunAllowance>0 + &&(passageHorizontalAlong[i-1]static_cast(horizontal)*17560 - :signedDz>=0||dz*65536>static_cast(horizontal)*17560) + :signedDz>0||(signedDz==0&&!segmentTouchesEndRun) + ||dz*65536>static_cast(horizontal)*17560) ||horizontal==0) - add("passage_grade",passage.stableId+" is not strictly descending or exceeds the 15 degree grade limit"); + add("passage_grade",passage.stableId+" segment="+std::to_string(i-1) + +" floor_delta_m="+std::to_string(signedDz/qPerMetre) + +" horizontal_m="+std::to_string(horizontal/qPerMetre) + +" grade_deg="+std::to_string(gradeDegrees) + +" is not strictly descending or exceeds the 15 degree grade limit"); } } if(stamps.empty()) @@ -2064,6 +2112,7 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( p.style=static_cast(LayoutInt(v,"style")); p.placementRadiusSubvoxels=LayoutInt(v,"placement_radius_q"); p.mouthRadiusSubvoxels=LayoutInt(v,"mouth_radius_q");p.midRadiusSubvoxels=LayoutInt(v,"mid_radius_q"); + p.endRunSubvoxels=LayoutInt(v,"end_run_q"); p.floorProfileIncludesLandingChambers= LayoutBool(v,"floor_profile_includes_landing_chambers"); const auto& upper=v.Get("upper_mouth_q").AsArray(); @@ -4191,6 +4240,345 @@ ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1( return result; } +struct FPassageTubeSelfOverlapCensus +{ + std::uint64_t measuredSegmentPairs=0; + std::uint64_t localPairsSkipped=0; + std::uint64_t overlappingSegmentPairs=0; + std::uint64_t firstOverlapSegmentA=0; + std::uint64_t firstOverlapSegmentB=0; + std::uint64_t firstLandingColumnSegment=0; + bool firstLandingColumnIsLower=false; + std::uint64_t landingColumnViolations=0; + std::uint32_t endTurnViolations=0; + double maximumEndTurnDegrees=0.0; + double minimumClearanceSubvoxels=std::numeric_limits::infinity(); + bool hasOverlapLocation=false; +}; + +static FPassageTubeSelfOverlapCensus CensusPassageTubeSelfOverlaps( + const ResolvedLayoutPassageV1& passage,std::int64_t qPerMetre, + std::uint32_t coreVoxelSizeMicrometres) +{ + FPassageTubeSelfOverlapCensus result; + if(passage.controlPoints.size()<3||qPerMetre<=0)return result; + const auto point=[](const ResolvedLayoutPassagePointV1& value) + {return std::array{static_cast(value.positionSubvoxels[0]), + static_cast(value.positionSubvoxels[1]), + static_cast(value.positionSubvoxels[2])};}; + const auto endTurn=[&](std::size_t index) + { + const auto before=point(passage.controlPoints[index-1]); + const auto center=point(passage.controlPoints[index]); + const auto after=point(passage.controlPoints[index+1]); + const std::array a{center[0]-before[0],center[1]-before[1],center[2]-before[2]}; + const std::array b{after[0]-center[0],after[1]-center[1],after[2]-center[2]}; + const double la=std::sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]); + const double lb=std::sqrt(b[0]*b[0]+b[1]*b[1]+b[2]*b[2]); + if(la<=1.0e-9||lb<=1.0e-9)return 180.0; + const double cosine=std::clamp((a[0]*b[0]+a[1]*b[1]+a[2]*b[2])/(la*lb),-1.0,1.0); + return std::acos(cosine)*180.0/3.14159265358979323846; + }; + constexpr double MaximumEndTurnDegrees=60.0; + const double firstTurn=endTurn(1); + result.maximumEndTurnDegrees=firstTurn; + result.endTurnViolations+=firstTurn>MaximumEndTurnDegrees+1.0e-6?1u:0u; + if(passage.controlPoints.size()>3) + { + const double lastTurn=endTurn(passage.controlPoints.size()-2); + result.maximumEndTurnDegrees=std::max(result.maximumEndTurnDegrees,lastTurn); + result.endTurnViolations+=lastTurn>MaximumEndTurnDegrees+1.0e-6?1u:0u; + } + if(passage.controlPoints.size()<4)return result; + const auto segmentDistance=[](const std::array& p1, + const std::array& q1,const std::array& p2, + const std::array& q2) + { + constexpr double Epsilon=1.0e-12; + const std::array d1={q1[0]-p1[0],q1[1]-p1[1],q1[2]-p1[2]}; + const std::array d2={q2[0]-p2[0],q2[1]-p2[1],q2[2]-p2[2]}; + const std::array r={p1[0]-p2[0],p1[1]-p2[1],p1[2]-p2[2]}; + const double a=d1[0]*d1[0]+d1[1]*d1[1]+d1[2]*d1[2]; + const double e=d2[0]*d2[0]+d2[1]*d2[1]+d2[2]*d2[2]; + const double f=d2[0]*r[0]+d2[1]*r[1]+d2[2]*r[2]; + double s=0.0,t=0.0; + if(a<=Epsilon&&e<=Epsilon) + return std::sqrt(r[0]*r[0]+r[1]*r[1]+r[2]*r[2]); + if(a<=Epsilon)t=std::clamp(f/e,0.0,1.0); + else + { + const double c=d1[0]*r[0]+d1[1]*r[1]+d1[2]*r[2]; + if(e<=Epsilon)s=std::clamp(-c/a,0.0,1.0); + else + { + const double b=d1[0]*d2[0]+d1[1]*d2[1]+d1[2]*d2[2]; + const double denominator=a*e-b*b; + if(denominator> Epsilon)s=std::clamp((b*f-c*e)/denominator,0.0,1.0); + t=(b*s+f)/e; + if(t<0.0){t=0.0;s=std::clamp(-c/a,0.0,1.0);} + else if(t>1.0){t=1.0;s=std::clamp((b-c)/a,0.0,1.0);} + } + } + const std::array delta={r[0]+s*d1[0]-t*d2[0], + r[1]+s*d1[1]-t*d2[1],r[2]+s*d1[2]-t*d2[2]}; + return std::sqrt(delta[0]*delta[0]+delta[1]*delta[1]+delta[2]*delta[2]); + }; + std::vector along(passage.controlPoints.size(),0.0); + std::vector horizontalAlong(passage.controlPoints.size(),0.0); + for(std::size_t i=1;i(a.radiusSubvoxels), + static_cast(b.radiusSubvoxels), + static_cast(c.radiusSubvoxels), + static_cast(d.radiusSubvoxels)}) + +0.5*static_cast(qPerMetre); + if(routeGap<=localReach) + {++result.localPairsSkipped;continue;} + ++result.measuredSegmentPairs; + const double distance=segmentDistance(point(a),point(b),point(c),point(d)); + const double clearance=distance + -std::max(static_cast(a.radiusSubvoxels), + static_cast(b.radiusSubvoxels)) + -std::max(static_cast(c.radiusSubvoxels), + static_cast(d.radiusSubvoxels)); + result.minimumClearanceSubvoxels=std::min(result.minimumClearanceSubvoxels, + clearance); + if(clearance< -1.0e-6) + { + if(!result.hasOverlapLocation) + { + result.firstOverlapSegmentA=first; + result.firstOverlapSegmentB=second; + result.hasOverlapLocation=true; + } + ++result.overlappingSegmentPairs; + } + } + const double endRun=passage.endRunSubvoxels>0 + ?static_cast(passage.endRunSubvoxels) + :static_cast(std::max(3*qPerMetre, + passage.mouthRadiusSubvoxels+qPerMetre)+qPerMetre); + const std::int64_t landingDiscRadius=ResolvedLayoutPassageLandingDiscRadiusSubvoxels( + passage.mouthRadiusSubvoxels,coreVoxelSizeMicrometres); + const auto overlapsLandingColumn=[&]( + const ResolvedLayoutPassageLandingChamberV1& chamber, + const std::array& landingMouth, + const std::array& center) + { + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0)return false; + const double floor=static_cast(chamber.floorCenterSubvoxels[2]); + const double ceiling=floor+static_cast(chamber.clearHeightSubvoxels); + if(center[2]ceiling)return false; + return IsInsideResolvedLayoutPassageLandingDiscV1( + {landingMouth[0],landingMouth[1]},landingDiscRadius,center[0],center[1]); + }; + for(std::size_t segment=0;segment+1(std::ceil(spatialLength + /std::max(1.0,0.25*qPerMetre)))); + bool upperColumnViolation=false,lowerColumnViolation=false; + for(int sample=0;sample<=samples;++sample) + { + const double t=static_cast(sample)/samples; + const double routeDistance=horizontalAlong[segment]+t*horizontalLength; + if(routeDistance<=endRun||horizontalAlong.back()-routeDistance<=endRun)continue; + const std::array center={a[0]+t*(b[0]-a[0]), + a[1]+t*(b[1]-a[1]),a[2]+t*(b[2]-a[2])}; + upperColumnViolation|=overlapsLandingColumn(passage.upperLandingChamber, + passage.upperMouthSubvoxels,center); + lowerColumnViolation|=overlapsLandingColumn(passage.lowerLandingChamber, + passage.lowerMouthSubvoxels,center); + if(upperColumnViolation&&lowerColumnViolation)break; + } + if((upperColumnViolation||lowerColumnViolation) + &&result.landingColumnViolations==0) + { + result.firstLandingColumnSegment=segment; + result.firstLandingColumnIsLower=!upperColumnViolation&&lowerColumnViolation; + } + result.landingColumnViolations+=upperColumnViolation?1u:0u; + result.landingColumnViolations+=lowerColumnViolation?1u:0u; + } + return result; +} + +ResolvedLayoutPassageSelfOverlapAuditV1 AuditResolvedLayoutPassageSelfOverlapV1( + const ResolvedLayoutPassageV1& passage,std::uint32_t coreVoxelSizeMicrometres) +{ + ResolvedLayoutPassageSelfOverlapAuditV1 audit; + if(coreVoxelSizeMicrometres==0)return audit; + const std::int64_t qPerMetre=static_cast(std::llround( + static_cast(WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /coreVoxelSizeMicrometres)); + if(qPerMetre<=0)return audit; + const auto census=CensusPassageTubeSelfOverlaps(passage,qPerMetre, + coreVoxelSizeMicrometres); + audit.segmentPairs=census.measuredSegmentPairs+census.localPairsSkipped; + audit.measuredSegmentPairs=census.measuredSegmentPairs; + audit.localExemptSegmentPairs=census.localPairsSkipped; + audit.overlappingSegmentPairs=census.overlappingSegmentPairs; + audit.firstOverlapSegmentA=census.firstOverlapSegmentA; + audit.firstOverlapSegmentB=census.firstOverlapSegmentB; + audit.firstLandingColumnSegment=census.firstLandingColumnSegment; + audit.firstLandingColumnIsLower=census.firstLandingColumnIsLower; + audit.landingColumnViolations=census.landingColumnViolations; + audit.maximumEndTurnDegrees=census.maximumEndTurnDegrees; + audit.endTurnViolations=census.endTurnViolations; + audit.hasOverlapLocation=census.hasOverlapLocation; + audit.minimumClearanceMetres=std::isfinite(census.minimumClearanceSubvoxels) + ?census.minimumClearanceSubvoxels/static_cast(qPerMetre):0.0; + audit.hasMeasuredCoverage=audit.measuredSegmentPairs>0; + audit.passed=audit.hasMeasuredCoverage&&audit.overlappingSegmentPairs==0 + &&audit.endTurnViolations==0&&audit.landingColumnViolations==0; + return audit; +} + +ResolvedLayoutPassageEndRunAuditV1 AuditResolvedLayoutPassageEndRunsV1( + const ResolvedLayoutPassageV1& passage,std::uint32_t coreVoxelSizeMicrometres, + std::int64_t endRunSubvoxels) +{ + ResolvedLayoutPassageEndRunAuditV1 audit; + if(coreVoxelSizeMicrometres==0||passage.controlPoints.size()<2)return audit; + const std::int64_t qPerMetre=static_cast(std::llround( + static_cast(WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /coreVoxelSizeMicrometres)); + if(qPerMetre<=0)return audit; + const std::int64_t mouthRadius=passage.mouthRadiusSubvoxels>0 + ?passage.mouthRadiusSubvoxels:0; + const std::int64_t radius=std::max(3*qPerMetre, + mouthRadius+qPerMetre); + const std::int64_t endRun=endRunSubvoxels>0?endRunSubvoxels + :passage.endRunSubvoxels>0?passage.endRunSubvoxels:radius+qPerMetre; + audit.requiredEndRunMetres=static_cast(endRun)/qPerMetre; + std::vector controls; + std::vector floorProfile; + controls.reserve(passage.controlPoints.size()); + floorProfile.reserve(passage.controlPoints.size()); + for(const auto& point:passage.controlPoints) + { + controls.push_back({static_cast(point.positionSubvoxels[0]), + static_cast(point.positionSubvoxels[1]), + static_cast(point.positionSubvoxels[2]), + static_cast(point.radiusSubvoxels)}); + floorProfile.push_back(static_cast(point.positionSubvoxels[2] + -point.radiusSubvoxels)); + } + const std::size_t routeEnd=passage.lowerMouthControlIndex>0 + ?std::min(passage.lowerMouthControlIndex,controls.size()-1):controls.size()-1; + const auto spline=PassageSpline::BuildSegments(controls,routeEnd,0.25, + static_cast(qPerMetre),floorProfile,0.001); + struct FEndRunSample + { + double x=0.0,y=0.0,floor=0.0,distance=0.0; + }; + std::vector samples; + samples.reserve(spline.size()+1); + for(const auto& segment:spline) + { + if(samples.empty())samples.push_back({segment.a[0]/qPerMetre, + segment.a[1]/qPerMetre,segment.plannedFloorA/qPerMetre,0.0}); + const auto& previous=samples.back(); + const double x=segment.b[0]/qPerMetre,y=segment.b[1]/qPerMetre; + const double horizontal=std::hypot(x-previous.x,y-previous.y); + samples.push_back({x,y,segment.plannedFloorB/qPerMetre, + previous.distance+horizontal}); + } + if(samples.size()<2)return audit; + const double total=samples.back().distance; + const double requiredMetres=static_cast(endRun)/qPerMetre; + audit.measuredStartMetres=std::min(total,requiredMetres); + audit.measuredEndMetres=std::min(total,requiredMetres); + audit.hasMeasuredCoverage=total>=2.0*requiredMetres; + if(total1.0e-12 + ?std::clamp((distance-a.distance)/span,0.0,1.0):0.0; + return FEndRunSample{std::lerp(a.x,b.x,t),std::lerp(a.y,b.y,t), + std::lerp(a.floor,b.floor,t),distance}; + }; + const auto startEnd=sampleAt(requiredMetres); + const auto endStart=sampleAt(total-requiredMetres); + const double startFloor=samples.front().floor,endFloor=samples.back().floor; + double startDeviation=std::abs(startEnd.floor-startFloor); + double endDeviation=std::abs(endStart.floor-endFloor); + double startGrade=0.0,endGrade=0.0; + double startGradeAt=0.0,endGradeAt=0.0; + double startGradeFloorDelta=0.0,endGradeFloorDelta=0.0; + double startGradeHorizontal=0.0,endGradeHorizontal=0.0; + for(const auto& sample:samples) + { + if(sample.distance<=requiredMetres) + startDeviation=std::max(startDeviation,std::abs(sample.floor-startFloor)); + if(total-sample.distance<=requiredMetres) + endDeviation=std::max(endDeviation,std::abs(sample.floor-endFloor)); + } + for(std::size_t i=1;istartGrade) + { + startGrade=grade;startGradeAt=a.distance; + startGradeFloorDelta=std::abs(b.floor-a.floor); + startGradeHorizontal=horizontal; + } + if(total-b.distanceendGrade) + { + endGrade=grade;endGradeAt=total-b.distance; + endGradeFloorDelta=std::abs(b.floor-a.floor); + endGradeHorizontal=horizontal; + } + } + audit.startMaximumFloorDeviationMetres=startDeviation; + audit.endMaximumFloorDeviationMetres=endDeviation; + audit.startMaximumGradeDegrees=startGrade; + audit.endMaximumGradeDegrees=endGrade; + constexpr double FlatToleranceMetres=0.001; + constexpr double GradeToleranceDegrees=0.10; + if(startGrade>GradeToleranceDegrees||endGrade>GradeToleranceDegrees) + std::printf("[PassageEndRunGradePeak] id=%s start_s_m=%.6f start_delta_m=%.9f start_horizontal_m=%.9f end_s_from_mouth_m=%.6f end_delta_m=%.9f end_horizontal_m=%.9f\n", + passage.stableId.c_str(),startGradeAt,startGradeFloorDelta, + startGradeHorizontal,endGradeAt,endGradeFloorDelta,endGradeHorizontal); + audit.passed=audit.hasMeasuredCoverage + &&audit.startMaximumFloorDeviationMetres<=FlatToleranceMetres + &&audit.endMaximumFloorDeviationMetres<=FlatToleranceMetres + &&audit.startMaximumGradeDegrees<=GradeToleranceDegrees + &&audit.endMaximumGradeDegrees<=GradeToleranceDegrees; + return audit; +} + bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed, const std::vector& configs,std::string& error) { @@ -4325,6 +4713,9 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t (upperIsSurfaceWorld?2.0:1.5)*qPerMetre)); const std::int64_t mouthRadius=std::max(minimumMouthRadius, cfg?cfg->mouthRadiusSubvoxels:0); + const std::int64_t minimumEndRunRadius=std::max( + 3*qPerMetre,mouthRadius+qPerMetre); + const std::int64_t minimumEndRun=minimumEndRunRadius+qPerMetre; const std::int64_t midRadius=std::max( static_cast(std::llround((upperIsSurfaceWorld?3.5:1.5)*qPerMetre)), cfg?cfg->midRadiusSubvoxels:mouthRadius); @@ -4435,6 +4826,47 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t reach/static_cast(qPerMetre)); } } + std::int64_t endRunLength=minimumEndRun; + const std::int64_t straightLeadLength=endRunLength + +std::max(1,qPerMetre/100); + const std::int64_t minimumRingRadius=std::max( + 6*qPerMetre,midRadius+static_cast(std::llround( + (0.75+0.34+0.50)*qPerMetre))); + std::int64_t anchoredHelixRadius=std::max(spiralRadius,minimumRingRadius); + if(anchoredSpiral&&passageAnchor) + { + const double dx=static_cast(upperMouth[0]-passageAnchor->values[0]); + const double dy=static_cast(upperMouth[1]-passageAnchor->values[1]); + const double radial=std::hypot(dx,dy); + if(radial>0.0) + { + const auto oldLowerMouth=lowerMouth; + lowerMouth[0]=passageAnchor->values[0] + +static_cast(std::llround(dx*anchoredHelixRadius/radial)); + lowerMouth[1]=passageAnchor->values[1] + +static_cast(std::llround(dy*anchoredHelixRadius/radial)); + if(!radialWithin(lowerMouth,placementRadius)) + {error=passage.stableId+" lower spiral mouth ring lies outside the resolved placement radius";return false;} + for(auto& host:layout.strates) + if(host.strateIndex==lower.strateIndex) + for(auto& anchor:host.anchors) + if(anchor.kind=="passage_arrival"&&anchor.values.size()>=3 + &&anchor.values[0]==oldLowerMouth[0] + &&anchor.values[1]==oldLowerMouth[1]) + { + anchor.values[0]=lowerMouth[0]; + anchor.values[1]=lowerMouth[1]; + std::printf("[PassageSpiralLowerMouthOnRing] id=%s radius_m=%.3f old_xy_m=(%.3f,%.3f) new_xy_m=(%.3f,%.3f) status=PASS\n", + passage.stableId.c_str(), + anchoredHelixRadius/static_cast(qPerMetre), + oldLowerMouth[0]/static_cast(qPerMetre), + oldLowerMouth[1]/static_cast(qPerMetre), + lowerMouth[0]/static_cast(qPerMetre), + lowerMouth[1]/static_cast(qPerMetre)); + break; + } + } + } // Reserve each landing envelope inside its host band before building h(s). // SurfaceWorld can raise its floor by at most the 4.5 m headroom allowance, // so include that bounded rise as well as the grade reach and floor shell. @@ -4540,8 +4972,12 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t ringRadius=std::min(outerRingRadius, std::max(mouthRadius+2*qPerMetre,placementRadius*5/8)); else if(pathStyle==2u) - ringRadius=std::clamp(spiralRadius, - std::min(outerRingRadius,mouthRadius+qPerMetre/2),outerRingRadius); + { + if(outerRingRadius(outerRingRadius/3, @@ -4568,8 +5004,9 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t ?std::max(midRadius, passageAnchor->values[3]+2*qPerMetre) :mouthRadius+qPerMetre/2; - ringRadius=std::min(ringRadius,anchoredSpiral - ?mouthRadius+qPerMetre/2:craterRimRouteRadius); + ringRadius=anchoredSpiral + ?std::min(outerRingRadius,anchoredHelixRadius) + :std::min(ringRadius,craterRimRouteRadius); } std::uint32_t phase=static_cast(PassageSeed(seed, upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16); @@ -4707,6 +5144,53 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t static_cast(std::llround(centerX+radius*directions[directionIndex][0]/65536.0)), static_cast(std::llround(centerY+radius*directions[directionIndex][1]/65536.0))}; }; + if(pathStyle==1u&&!anchoredSpiral&&hasUpperRoute) + { + double bestTurn=std::numeric_limits::infinity(); + double bestRouteAlignment=-std::numeric_limits::infinity(); + std::uint32_t selectedPhase=phase; + for(std::uint32_t candidate=0;candidate<16;++candidate) + { + const auto first=ringPoint(candidate); + const auto next=ringPoint(static_cast( + (static_cast(candidate)+direction+16)%16)); + const std::array incoming{ + static_cast(first[0]-upperMouth[0]), + static_cast(first[1]-upperMouth[1])}; + const std::array outgoing{ + static_cast(next[0]-first[0]), + static_cast(next[1]-first[1])}; + const double incomingLength=std::hypot(incoming[0],incoming[1]); + const double outgoingLength=std::hypot(outgoing[0],outgoing[1]); + if(incomingLength+1.0e-6(upperTangent[0]), + static_cast(upperTangent[1])); + routeAlignment=tangentLength>0.0 + ?(incoming[0]*upperTangent[0] + +incoming[1]*upperTangent[1]) + /(incomingLength*tangentLength):0.0; + if(routeAlignment<0.0)continue; + } + if(turnbestRouteAlignment)) + {bestTurn=turn;bestRouteAlignment=routeAlignment; + selectedPhase=candidate;} + } + phase=selectedPhase; + std::printf("[PassageEntryTurnPlan] id=%s selected_phase=%u turn_deg=%.3f upper_tangent_alignment=%.3f limit_deg=60.000 status=%s\n", + passage.stableId.c_str(),phase,bestTurn,bestRouteAlignment, + bestTurn<=60.0?"PASS":"FAIL"); + } if(generatedWormEntry) { struct FPhaseScore @@ -4764,9 +5248,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t if(loops>=256)return std::uint64_t{0}; const std::int64_t mouthPlanSeparation=distanceQ( lowerMouth[0]-upperMouth[0],lowerMouth[1]-upperMouth[1]); - if(!hasLowerTangent&&!anchoredSpiral&&pathStyle==1u&&loops<=1 - &&mouthPlanSeparation<=2*mouthRadius - &&entryVerticalDistance<=2*mouthRadius+qPerMetre/2) + if(!hasLowerTangent&&!anchoredSpiral&&pathStyle==1u + &&mouthPlanSeparation<=2*(minimumEndRunRadius+midRadius)) { double bestExitScore=-std::numeric_limits::infinity(); std::uint32_t lowerPhase=candidate; @@ -4784,7 +5267,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const double outgoingY=static_cast(lowerMouth[1]-terminal[1]); const double incomingLength=std::hypot(incomingX,incomingY); const double outgoingLength=std::hypot(outgoingX,outgoingY); - if(incomingLength<=1.0e-6||outgoingLength<=1.0e-6)continue; + if(incomingLength<=1.0e-6 + ||outgoingLength+1.0e-6(point[0]-upperMouth[0]); const double entryY=static_cast(point[1]-upperMouth[1]); const double entryLength=std::hypot(entryX,entryY); - if(entryLength<=1.0e-6)continue; + if(entryLength+1.0e-6( (static_cast(candidate)+candidateDirection+16)%16); const auto nextPoint=ringPoint(next); @@ -4831,6 +5312,24 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t if(tangentLength<=1.0e-6)continue; const double tangentAlignment=(entryX*tangentX+entryY*tangentY) /(entryLength*tangentLength); + const double turnCross=entryX*tangentY-entryY*tangentX; + const double turnDot=entryX*tangentX+entryY*tangentY; + const double turnAngle=std::atan2(turnCross,turnDot); + const double halfTangent=std::tan(std::abs(turnAngle)*0.5); + const double requestedFilletRadius=std::max(mouthRadius,midRadius) + +0.75*qPerMetre; + const double maximumCut=0.80*std::min(entryLength,tangentLength); + const double filletRadius=std::abs(halfTangent)>1.0e-6 + ?std::min(static_cast(requestedFilletRadius), + maximumCut/std::abs(halfTangent)):0.0; + const double filletCut=filletRadius*std::abs(halfTangent); + // The entry fillet moves the first route control back toward + // the mouth. Reserve the full flat end run beyond that move, + // or the spline starts curving inside its required level span. + if(entryLength-filletCut+1.0e-6(upperMouth[0]-anchorX), static_cast(upperMouth[1]-anchorY)) @@ -5013,17 +5512,41 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t *static_cast(helixRadius) *(std::sin(3.14159265358979323846/pointsPerTurn) /(3.14159265358979323846/pointsPerTurn)); - const double horizontalPerTurn=polygonCircumference - *static_cast(TargetGradeQ16)/65536.0; + const double mouthOffset=std::clamp(std::hypot( + static_cast(upperMouth[0]-anchorX), + static_cast(upperMouth[1]-anchorY)),0.0, + static_cast(rootRadius)); + const double firstTurnRadius=std::min(mouthOffset, + static_cast(helixRadius)); + const double firstTurnCircumference=2.0*3.14159265358979323846 + *firstTurnRadius + *(std::sin(3.14159265358979323846/pointsPerTurn) + /(3.14159265358979323846/pointsPerTurn)); const std::uint32_t authoredTurns=static_cast(std::max(1, (spiralTurnsQ16+65535)/65536)); - const std::uint32_t gradeTurns=static_cast(std::max(1.0, - std::ceil(drop/std::max(1.0,horizontalPerTurn)))); - // The first coil grows from the island mouth offset to the full authored - // radius, so reserve one extra turn for that shorter opening revolution. - const std::uint32_t turns=std::max(authoredTurns,gradeTurns+1); + constexpr double MaximumEndRunGrade=0.2679491924311227; + const double requiredGradeLength=drop/MaximumEndRunGrade + +static_cast(straightLeadLength); + std::uint32_t gradeTurns=1; + while(gradeTurns<513&&firstTurnCircumference + +static_cast(gradeTurns-1)*polygonCircumference + (std::llround(2.0*0.75*qPerMetre + +0.50*qPerMetre)); + const std::uint32_t maximumPitchTurns=static_cast( + std::max(0.0,std::floor(drop/std::max(1,minimumPitch)))); + const std::uint32_t turns=std::max(authoredTurns,gradeTurns); + if(maximumPitchTurns==0||turns>maximumPitchTurns) + {error=passage.stableId+" cannot satisfy both the 15 degree end-run grade and helix clearance pitch";return false;} if(turns>512) {error=passage.stableId+" authored spiral radius needs more than 512 turns to meet walkable grade";return false;} + std::printf("[PassageSpiralPitchPlan] id=%s ring_radius_m=%.3f turns=%u pitch_m=%.3f minimum_pitch_m=%.3f grade_limit_deg=15.000 end_run_m=%.3f status=PASS\n", + passage.stableId.c_str(),helixRadius/static_cast(qPerMetre),turns, + drop/(static_cast(turns)*qPerMetre), + minimumPitch/static_cast(qPerMetre), + endRunLength/static_cast(qPerMetre)); const double startAngle=std::atan2(static_cast(upperMouth[1]-anchorY), static_cast(upperMouth[0]-anchorX)); const std::uint32_t helixPoints=turns*segments; @@ -5033,8 +5556,6 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const double angle=startAngle+static_cast(clockwise?1:-1) *2.0*3.14159265358979323846*t*turns; const double radialT=std::min(1.0,static_cast(point)/segments); - const double mouthOffset=std::clamp(static_cast(passageAnchor->values[3]), - 0.0,static_cast(helixRadius)); const double radial=mouthOffset+(static_cast(helixRadius)-mouthOffset) *radialT; const std::array p{ @@ -5046,9 +5567,27 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t } helixEndIndex=xy.size()-1; hasAnchorHelix=helixEndIndex>0; + if(hasAnchorHelix&&anchoredSpiral) + { + constexpr double MaximumEndRunGrade=0.2679491924311227; + const std::int64_t bodyVertical=std::abs(lowerPlannedFloor + -(helixBedrockZ-playerCenterAboveFloorQ)); + const double requiredBodyLength=bodyVertical/MaximumEndRunGrade + +static_cast(straightLeadLength); + const std::int64_t twoTurnRadius=static_cast(std::ceil( + (requiredBodyLength/(4.0*3.14159265358979323846) + +0.50*qPerMetre)/qPerMetre))*qPerMetre; + ringRadius=std::max({ringRadius,minimumRingRadius,twoTurnRadius}); + if(ringRadius>outerRingRadius) + {error=passage.stableId+" lower spiral approach cannot meet grade and coil-clearance pitch inside its placement radius";return false;} + std::printf("[PassageSpiralBodyRingPlan] id=%s ring_radius_m=%.3f body_drop_m=%.3f end_run_m=%.3f status=PASS\n", + passage.stableId.c_str(),ringRadius/static_cast(qPerMetre), + bodyVertical/static_cast(qPerMetre), + endRunLength/static_cast(qPerMetre)); + } } const std::int64_t routeGradeQ16=profiledSurfaceJoin?8000:hasAnchorHelix - &&lower.generatorKind=="SurfaceWorld"?8000 + &&lower.generatorKind=="SurfaceWorld"?16000 :style==0&&anchoredIslandPassage&&!anchoredSpiral?15500:TargetGradeQ16; const std::array firstRing=ringPoint(phase); if(hasAnchorHelix&&pathStyle==1u) @@ -5063,10 +5602,10 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const double targetX=static_cast(firstRing[0])-anchorX; const double targetY=static_cast(firstRing[1])-anchorY; const double targetAngle=std::atan2(targetY,targetX); - const double angleDelta=static_cast(direction)*2.0 + const double angleDelta=(anchoredSpiral?0.0:static_cast(direction)*2.0 *3.14159265358979323846 - +std::remainder(targetAngle-startAngle,2.0*3.14159265358979323846); - const double startRadius=static_cast(spiralRadius); + )+std::remainder(targetAngle-startAngle,2.0*3.14159265358979323846); + const double startRadius=static_cast(anchoredHelixRadius); const std::int64_t targetDx=firstRing[0]-passageAnchor->values[0]; const std::int64_t targetDy=firstRing[1]-passageAnchor->values[1]; const double targetRadius=static_cast(PassageIsqrt( @@ -5164,7 +5703,12 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t while(loops<256) { totalHorizontal=measurePath(); - if(totalHorizontal*static_cast(routeGradeQ16) + const std::uint64_t reserved=static_cast(straightLeadLength) + *(hasAnchorHelix?1ull:2ull); + const std::uint64_t gradeLength=totalHorizontal>reserved + ?totalHorizontal-reserved:0; + if(totalHorizontal>=reserved + &&gradeLength*static_cast(routeGradeQ16) >=static_cast(verticalDistance)*65536ull)break; for(int step=1;step<=16;++step) { @@ -5178,9 +5722,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const std::int64_t mouthPlanSeparation=distanceQ( lowerMouth[0]-upperMouth[0],lowerMouth[1]-upperMouth[1]); const bool shortGeneratedClosure=!hasLowerTangent&&!hasAnchorHelix - &&pathStyle==1u&&loops<=1 - &&mouthPlanSeparation<=2*mouthRadius - &&verticalDistance<=2*mouthRadius+qPerMetre/2; + &&pathStyle==1u + &&mouthPlanSeparation<=2*(minimumEndRunRadius+midRadius); if(hasLowerTangent) { std::uint32_t lowerPhase=static_cast(PassageSeed(seed, @@ -5197,8 +5740,9 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const std::int64_t dy=lowerMouth[1]-terminal[1]; const std::int64_t alignment=dx*lowerTangent[0]+dy*lowerTangent[1]; const std::int64_t cross=std::abs(dx*lowerTangent[1]-dy*lowerTangent[0]); - if(alignment<=0||cross==0)continue; const auto length=distanceQ(dx,dy); + if(alignment<=0||cross==0 + ||length(straightLeadLength))continue; const std::int64_t score=alignment*65536/static_cast(length); if(score>bestAlignment) {bestAlignment=score;lowerPhase=candidate;} @@ -5217,8 +5761,9 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const std::int64_t dy=lowerMouth[1]-terminal[1]; const std::int64_t alignment=dx*lowerTangent[0]+dy*lowerTangent[1]; const std::int64_t cross=std::abs(dx*lowerTangent[1]-dy*lowerTangent[0]); - if(alignment>=0||cross==0)continue; const auto length=distanceQ(dx,dy); + if(alignment>=0||cross==0 + ||length(straightLeadLength))continue; const std::int64_t score=-alignment*65536/static_cast(length); if(score>bestAlignment) {bestAlignment=score;lowerPhase=candidate;lowerRouteReversed=true;} @@ -5260,7 +5805,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const double outgoingY=static_cast(lowerMouth[1]-terminal[1]); const double incomingLength=std::hypot(incomingX,incomingY); const double outgoingLength=std::hypot(outgoingX,outgoingY); - if(incomingLength<=1.0e-6||outgoingLength<=1.0e-6)continue; + if(incomingLength<=1.0e-6 + ||outgoingLength+1.0e-63)lowerMouthIndex+=lowerMouthIndex-3; xy.swap(refined); } + if(xy.size()<2) + {error=passage.stableId+" has no lower end-run lead";return false;} + lowerMouthIndex=xy.size()-1; totalHorizontal=measurePath(); if(loops>=256||totalHorizontal==0) {error=passage.stableId+" could not fit a supported grade inside the resolved placement radius";return false;} @@ -5317,6 +5866,29 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t {error=passage.stableId+" has no horizontal reach before its lower mouth";return false;} if(hasAnchorHelix&&helixHorizontal==0) {error=passage.stableId+" anchor spiral has no horizontal reach";return false;} + const auto routeEnd=cumulative.begin()+static_cast( + lowerMouthIndex+1); + const auto upperBoundary=std::lower_bound(cumulative.begin(),routeEnd, + static_cast(straightLeadLength)); + if(upperBoundary==routeEnd) + {error=passage.stableId+" has no control point past its upper level end run";return false;} + const std::uint64_t upperFlatEndDistance=*upperBoundary; + const std::uint64_t lowerThreshold=mouthHorizontal + -static_cast(straightLeadLength); + const auto lowerBoundary=std::lower_bound(cumulative.begin(),routeEnd, + lowerThreshold); + if(lowerBoundary==routeEnd) + {error=passage.stableId+" has no control point inside its lower level end run";return false;} + const std::uint64_t lowerFlatStartDistance=*lowerBoundary==lowerThreshold + ?*lowerBoundary:*(lowerBoundary-1); + const std::uint64_t reservedEndRuns=static_cast(straightLeadLength) + *(hasAnchorHelix?1ull:2ull); + if(mouthHorizontal(straightLeadLength) + ||mouthHorizontal-helixHorizontal + <=static_cast(straightLeadLength)))) + {error=passage.stableId+" cannot fit the requested level end runs";return false;} + passage.endRunSubvoxels=endRunLength; passage.controlPoints.reserve(xy.size()); const std::int64_t surfacePortalRadius=surfaceGroundDeparture ?std::min(mouthRadius,static_cast(std::llround(1.5*qPerMetre))) @@ -5366,33 +5938,49 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t (mouthRadius-midRadius)*(1.0-smooth))); } } - std::int64_t playerZ=0; + const std::int64_t upperFloor=upperMouth[2]-upperPlayerCenterAboveFloorQ; + const std::int64_t lowerFloor=lowerMouth[2]-playerCenterAboveFloorQ; + std::int64_t floor=upperFloor; if(hasAnchorHelix&&i<=helixEndIndex) - playerZ=upperMouth[2]+PassageRoundRatio(helixBedrockZ-upperMouth[2], - static_cast(cumulative[i]), - static_cast(helixHorizontal)); + { + const std::int64_t helixFloor=helixBedrockZ-playerCenterAboveFloorQ; + if(cumulative[i]>upperFlatEndDistance) + { + const std::uint64_t gradeLength=helixHorizontal + -upperFlatEndDistance; + floor=upperFloor+PassageRoundRatio(helixFloor-upperFloor, + static_cast(cumulative[i] + -upperFlatEndDistance), + static_cast(gradeLength)); + } + } + else if(hasAnchorHelix) + { + const std::int64_t helixFloor=helixBedrockZ-playerCenterAboveFloorQ; + const std::uint64_t bodyDistance=cumulative[i]-helixHorizontal; + const std::uint64_t bodyGradeLength=lowerFlatStartDistance + -helixHorizontal; + if(bodyDistance>0&&cumulative[i](bodyDistance), + static_cast(bodyGradeLength)); + else if(cumulative[i]>=lowerFlatStartDistance)floor=lowerFloor; + } else { - const std::uint64_t segmentStart=hasAnchorHelix?helixHorizontal:0; - const std::int64_t startZ=hasAnchorHelix?helixBedrockZ:upperMouth[2]; - const std::uint64_t segmentLength=mouthHorizontal-segmentStart; - playerZ=startZ+PassageRoundRatio(lowerMouth[2]-startZ, - static_cast(cumulative[i]-segmentStart), - static_cast(segmentLength)); + const std::uint64_t bodyGradeLength=lowerFlatStartDistance + -upperFlatEndDistance; + if(cumulative[i]>upperFlatEndDistance + &&cumulative[i](cumulative[i] + -upperFlatEndDistance), + static_cast(bodyGradeLength)); + else if(cumulative[i]>=lowerFlatStartDistance)floor=lowerFloor; } ResolvedLayoutPassagePointV1 point; - std::int64_t pointPlayerCenterAboveFloorQ=playerCenterAboveFloorQ; - if(surfaceGroundDeparture) - { - const double t=std::clamp(static_cast(cumulative[i]) - /surfaceMouthBlendLength,0.0,1.0); - const double smooth=t*t*(3.0-2.0*t); - pointPlayerCenterAboveFloorQ=upperPlayerCenterAboveFloorQ - +static_cast(std::llround( - (playerCenterAboveFloorQ-upperPlayerCenterAboveFloorQ)*smooth)); - } point.positionSubvoxels={xy[i][0],xy[i][1], - playerZ+radius-pointPlayerCenterAboveFloorQ}; + floor+radius}; point.radiusSubvoxels=radius;passage.controlPoints.push_back(point); } if(surfaceGroundDeparture) @@ -5476,6 +6064,15 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t chamberFloorControls,chamberFloorProfile, lowerMouthIndex,route,qPerMetre); } + const double upperEndRunFloor=chamberFloorProfile.front(); + const double lowerEndRunFloor=chamberFloorProfile[lowerMouthIndex]; + for(std::size_t i=0;i=lowerFlatStartDistance) + chamberFloorProfile[i]=lowerEndRunFloor; + } // Commit the endpoint route fit into h(s) before chamber records are // resolved. The passage field and both landing planes then consume the // same deterministic profile instead of blending the chamber in later. @@ -5483,6 +6080,16 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t passage.controlPoints[i].positionSubvoxels[2]=static_cast( std::llround(chamberFloorProfile[i])) +passage.controlPoints[i].radiusSubvoxels; + const auto endRunAudit=AuditResolvedLayoutPassageEndRunsV1( + passage,layout.coreVoxelSizeMicrometres,endRunLength); + std::printf("[PassageEndRunPlan] id=%s required_m=%.3f measured_start_m=%.3f measured_end_m=%.3f start_deviation_m=%.6f end_deviation_m=%.6f start_grade_deg=%.6f end_grade_deg=%.6f status=%s\n", + passage.stableId.c_str(),endRunAudit.requiredEndRunMetres, + endRunAudit.measuredStartMetres,endRunAudit.measuredEndMetres, + endRunAudit.startMaximumFloorDeviationMetres, + endRunAudit.endMaximumFloorDeviationMetres, + endRunAudit.startMaximumGradeDegrees, + endRunAudit.endMaximumGradeDegrees, + endRunAudit.passed?"PASS":"FAIL"); const auto resolveLandingChamber=[&]( ResolvedLayoutPassageLandingChamberV1& chamber, const std::array& mouth,std::size_t endpointIndex, @@ -5594,6 +6201,61 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t lowerMouthIndex,lowerMouthIndex-1,lower,"lower")) return false; passage.floorProfileIncludesLandingChambers=true; + const auto overlapCensus=AuditResolvedLayoutPassageSelfOverlapV1( + passage,layout.coreVoxelSizeMicrometres); + std::printf("[PassageTubeSelfOverlapCensus] id=%s segment_pairs=%llu measured_pairs=%llu local_exempt_pairs=%llu overlap_pairs=%llu first_overlap_segments=(%llu,%llu) end_turn_max_deg=%.3f end_turn_violations=%u landing_column_violations=%llu minimum_clearance_m=%.6f status=%s\n", + passage.stableId.c_str(), + static_cast(overlapCensus.segmentPairs), + static_cast(overlapCensus.measuredSegmentPairs), + static_cast(overlapCensus.localExemptSegmentPairs), + static_cast(overlapCensus.overlappingSegmentPairs), + static_cast(overlapCensus.firstOverlapSegmentA), + static_cast(overlapCensus.firstOverlapSegmentB), + overlapCensus.maximumEndTurnDegrees,overlapCensus.endTurnViolations, + static_cast(overlapCensus.landingColumnViolations), + overlapCensus.minimumClearanceMetres, + overlapCensus.passed?"PASS":"FAIL"); + if(!overlapCensus.passed) + { + if(overlapCensus.hasOverlapLocation + &&overlapCensus.firstOverlapSegmentB+1(qPerMetre);}; + std::printf("[PassageTubeSelfOverlapWitness] id=%s pair=(%llu,%llu) a_m=(%.3f,%.3f,%.3f) b_m=(%.3f,%.3f,%.3f) c_m=(%.3f,%.3f,%.3f) d_m=(%.3f,%.3f,%.3f) radii_m=(%.3f,%.3f,%.3f,%.3f)\n", + passage.stableId.c_str(), + static_cast(overlapCensus.firstOverlapSegmentA), + static_cast(overlapCensus.firstOverlapSegmentB), + metres(a.positionSubvoxels[0]),metres(a.positionSubvoxels[1]),metres(a.positionSubvoxels[2]), + metres(b.positionSubvoxels[0]),metres(b.positionSubvoxels[1]),metres(b.positionSubvoxels[2]), + metres(c.positionSubvoxels[0]),metres(c.positionSubvoxels[1]),metres(c.positionSubvoxels[2]), + metres(d.positionSubvoxels[0]),metres(d.positionSubvoxels[1]),metres(d.positionSubvoxels[2]), + metres(a.radiusSubvoxels),metres(b.radiusSubvoxels), + metres(c.radiusSubvoxels),metres(d.radiusSubvoxels)); + } + if(overlapCensus.landingColumnViolations>0 + &&overlapCensus.firstLandingColumnSegment+1(qPerMetre);}; + std::printf("[PassageLandingColumnWitness] id=%s segment=%llu side=%s a_m=(%.3f,%.3f,%.3f) b_m=(%.3f,%.3f,%.3f) disc_radius_m=%.3f end_run_m=%.3f\n", + passage.stableId.c_str(), + static_cast(overlapCensus.firstLandingColumnSegment), + overlapCensus.firstLandingColumnIsLower?"lower":"upper", + metres(a.positionSubvoxels[0]),metres(a.positionSubvoxels[1]),metres(a.positionSubvoxels[2]), + metres(b.positionSubvoxels[0]),metres(b.positionSubvoxels[1]),metres(b.positionSubvoxels[2]), + ResolvedLayoutPassageLandingDiscRadiusSubvoxels( + passage.mouthRadiusSubvoxels,layout.coreVoxelSizeMicrometres) + /static_cast(qPerMetre), + endRunLength/static_cast(qPerMetre)); + } + } std::printf("[PassageLandingChamberPlan] id=%s upper_radius_m=%.3f lower_radius_m=%.3f clear_height_m=4.500 upper_floor_grade_q16=(%lld,%lld) lower_floor_grade_q16=(%lld,%lld) status=PASS\n", passage.stableId.c_str(), passage.upperLandingChamber.radiusSubvoxels/static_cast(qPerMetre), @@ -5668,6 +6330,94 @@ ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3() expectReject("bedrock band below minimum thickness",thinBand); auto genericOverlap=base;genericOverlap.strates[0].bedrock.bottomMicrometres-=1;expectReject("nominal band overlap",genericOverlap); auto genericGap=base;genericGap.strates[0].bedrock.bottomMicrometres+=1;expectReject("nominal band gap",genericGap); + ResolvedLayoutPassageV1 frozenFIFixture; + constexpr std::uint32_t FixturePointsPerTurn=24; + constexpr std::uint32_t FixtureTurns=3; + constexpr double FixtureRingRadiusMetres=2.0; + constexpr double FixturePitchMetres=1.15; + constexpr double FixtureBoreRadiusMetres=0.75; + const std::int64_t fixtureQPerMetre=static_cast( + std::llround(static_cast(WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /base.coreVoxelSizeMicrometres)); + for(std::uint32_t i=0;i<=FixturePointsPerTurn*FixtureTurns;++i) + { + const double t=static_cast(i)/FixturePointsPerTurn; + const double angle=2.0*3.14159265358979323846*t; + ResolvedLayoutPassagePointV1 point; + point.positionSubvoxels={ + static_cast(std::llround(FixtureRingRadiusMetres + *fixtureQPerMetre*std::cos(angle))), + static_cast(std::llround(FixtureRingRadiusMetres + *fixtureQPerMetre*std::sin(angle))), + -static_cast(std::llround(FixturePitchMetres + *fixtureQPerMetre*t))}; + point.radiusSubvoxels=static_cast( + std::llround(FixtureBoreRadiusMetres*fixtureQPerMetre)); + frozenFIFixture.controlPoints.push_back(point); + } + const auto frozenFICensus=CensusPassageTubeSelfOverlaps( + frozenFIFixture,fixtureQPerMetre,base.coreVoxelSizeMicrometres); + ++result.controlsRun; + if(frozenFICensus.overlappingSegmentPairs>0)++result.controlsRejectedAsExpected; + else result.failures.push_back({"self_overlap_fixture_missed", + "frozen L173 Floating Islands helix was not rejected by the passage census"}); + std::printf("[PassageSelfOverlapFrozenFIFixture] ring_radius_m=%.3f pitch_m=%.3f bore_radius_m=%.3f overlap_pairs=%llu expected=REJECTED status=%s\n", + FixtureRingRadiusMetres,FixturePitchMetres,FixtureBoreRadiusMetres, + static_cast(frozenFICensus.overlappingSegmentPairs), + frozenFICensus.overlappingSegmentPairs>0?"PASS":"FAIL"); + ResolvedLayoutPassageV1 twoPercentEndRun; + twoPercentEndRun.mouthRadiusSubvoxels=2*fixtureQPerMetre; + twoPercentEndRun.midRadiusSubvoxels=2*fixtureQPerMetre; + const auto addEndRunPoint=[&](std::int64_t x,std::int64_t floor) + { + ResolvedLayoutPassagePointV1 point; + point.positionSubvoxels={x,0,floor+twoPercentEndRun.midRadiusSubvoxels}; + point.radiusSubvoxels=twoPercentEndRun.midRadiusSubvoxels; + twoPercentEndRun.controlPoints.push_back(point); + }; + addEndRunPoint(0,0); + addEndRunPoint(4*fixtureQPerMetre, + static_cast(std::llround(0.08*fixtureQPerMetre))); + addEndRunPoint(8*fixtureQPerMetre, + static_cast(std::llround(0.08*fixtureQPerMetre))); + const auto twoPercentAudit=AuditResolvedLayoutPassageEndRunsV1( + twoPercentEndRun,base.coreVoxelSizeMicrometres,4*fixtureQPerMetre); + ++result.controlsRun; + if(!twoPercentAudit.passed)++result.controlsRejectedAsExpected; + else result.failures.push_back({"end_run_audit_missed_2_percent_control", + "the end-run audit accepted a synthetic 2 percent slope"}); + std::printf("[PassageEndRunNegativeControl] slope_percent=2 measured_start_m=%.3f max_grade_deg=%.3f expected=REJECTED status=%s\n", + twoPercentAudit.measuredStartMetres,twoPercentAudit.startMaximumGradeDegrees, + !twoPercentAudit.passed?"PASS":"FAIL"); + ResolvedLayoutPassageV1 hairpinFixture; + constexpr double HairpinTurnRadians=160.0*3.14159265358979323846/180.0; + const double hairpinRadius=0.125*fixtureQPerMetre; + const std::array,4> hairpinXY{{ + {{-2.0,0.0}},{{0.0,0.0}}, + {{2.0*std::cos(HairpinTurnRadians),2.0*std::sin(HairpinTurnRadians)}}, + {{4.0*std::cos(HairpinTurnRadians),4.0*std::sin(HairpinTurnRadians)}}}}; + for(const auto& xy:hairpinXY) + { + ResolvedLayoutPassagePointV1 point; + point.positionSubvoxels={ + static_cast(std::llround(xy[0]*fixtureQPerMetre)), + static_cast(std::llround(xy[1]*fixtureQPerMetre)),0}; + point.radiusSubvoxels=static_cast(std::llround(hairpinRadius)); + hairpinFixture.controlPoints.push_back(point); + } + const auto hairpinAudit=AuditResolvedLayoutPassageSelfOverlapV1( + hairpinFixture,base.coreVoxelSizeMicrometres); + ++result.controlsRun; + if(!hairpinAudit.passed&&hairpinAudit.endTurnViolations>0 + &&hairpinAudit.overlappingSegmentPairs==0) + ++result.controlsRejectedAsExpected; + else result.failures.push_back({"self_overlap_hairpin_fixture_missed", + "the 160 degree hairpin was not rejected specifically by the end-turn audit"}); + std::printf("[PassageSelfOverlapHairpinFixture] turn_deg=%.3f violations=%u overlaps=%llu expected=REJECTED status=%s\n", + hairpinAudit.maximumEndTurnDegrees,hairpinAudit.endTurnViolations, + static_cast(hairpinAudit.overlappingSegmentPairs), + hairpinAudit.endTurnViolations>0&&hairpinAudit.overlappingSegmentPairs==0 + ?"PASS":"FAIL"); result.passed=result.controlsRun==result.controlsRejectedAsExpected&&result.failures.empty();return result; } } diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 104f6ac..3089b3f 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -2890,6 +2890,7 @@ namespace VoxelForgeCore const auto MarchingCubesStart=std::chrono::steady_clock::now(); FVoxelUniformBlockProofAuditStats BlockProofAudit; FVoxelMeshData mesh; + FIntVector DebugDensityOrigin,DebugDensityDimensions; { FStandaloneShapeWorld::FScopedStampSamplingStep stampSamplingStep(MeshStep); mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr, @@ -2897,7 +2898,9 @@ namespace VoxelForgeCore (options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr, options.profileMesherStages?&McStageTimings:nullptr, options.auditUniformBlockProofs?&BlockProofAudit:nullptr, - options.forceFullDensityCapture); + options.forceFullDensityCapture, + options.captureDensitySamples?&DebugDensityOrigin:nullptr, + options.captureDensitySamples?&DebugDensityDimensions:nullptr); } passageFeatureScope.copySweptTubeRuntimeCounts(); if(options.auditUniformBlockProofs) @@ -3222,7 +3225,15 @@ namespace VoxelForgeCore std::chrono::steady_clock::now()-materialQueryStart).count():0.0; out.indices.reserve(static_cast(mesh.Triangles.Num())); for(const int32 index:mesh.Triangles)out.indices.push_back(static_cast(index)); - if(options.captureDensitySamples)out.debugDensitySamples=std::move(densityValues); + if(options.captureDensitySamples) + { + out.debugDensitySamples=std::move(densityValues); + out.debugDensityOriginVoxels={DebugDensityOrigin.X,DebugDensityOrigin.Y, + DebugDensityOrigin.Z}; + out.debugDensityDimensions={DebugDensityDimensions.X, + DebugDensityDimensions.Y,DebugDensityDimensions.Z}; + out.debugDensityStepVoxels=MeshStep; + } if(options.hashDensity) { std::uint64_t densityHash=14695981039346656037ull; diff --git a/Standalone/Research/WorldAssemblerM3/Design50/README.md b/Standalone/Research/WorldAssemblerM3/Design50/README.md new file mode 100644 index 0000000..c604128 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Design50/README.md @@ -0,0 +1,104 @@ +# Design50 — why the tunnel landings fail, and the plan (review 2026-11-03) + +A read-only review (Claude Opus 5.5, extra-high effort) of `wip/combined-cave` (b097283), with the L133–L173 history and +the L173 logs. The orchestrator verified R6 and R7a in the code. + +**Headline:** one wrong assumption, that a tunnel end is a separate 2-D "landing chamber" owned by the tube, the layer +and the audits at once. The plan replaces it with a level end run of the same spline plus the Design49 stamp, +applied last. + +## Root causes + +- **R1:** the chamber plane and the tube's 1-D h(s) disagree off the centreline (up to 1.3 m at the 7.5 m rim on FI). +- **R2:** the departure tube descends from the disc centre and undercuts its own landing. Unsupported samples: upper + chambers 316, lower 0 (L173); L159 upper pads 96/96 failing, lower 2/96. +- **R3:** the passage composer overrides the host: + - a forced-solid chamber floor slab (`ShapeWorldInternal.h` ~1665); + - shell walls filled over host air (~1869–1898); + - per-layer corridor exemptions only (Surface World, planned tunnels); + - the liminal route stamp re-applied after the carve (~1997, the L146 refill). +- **R4:** planner self-overlap. The FI helix pitch (3.2–3.9 m) is less than the bore height (~5.8 m), with 2,738 + overlapping control-point pairs. Upper and lower mouths share XY, so 32/32 final junctions turn 107–178° + (curl-back). +- **R5:** the passage picks the landing height, not the layer. FI departures land inside the FI→TN transition band. +- **R6 (bug, verified):** `floorProfileIncludesLandingChambers` (set for every passage) makes + `PassageFlatFloorCenterZSubvoxels` return early, skipping both blends. +- **R7 (audits):** + - (a) **verified:** the composed audit returns 10 before the seam audit, so seams were never measured; + - (b) audits re-model chambers (L159's interior-air 0/16 was a phantom); + - (c) exempt probes count as supported; zero-coverage gates pass; + - (d) only 8 failures are printed; + - (e) the TN and VS sweeps are the same 4-strata world. + +## Model + +1. **One spline, one floor:** h(s) is level and straight for the first and last a ≥ R+1 m. +2. **The landing = the end run:** a flat disc radius R at P = h(end); the descent starts outside the disc. +3. **One composition order:** layer content (including liminal decks) → bore carve (air only, plus a floor shell under + the bore where the host is air) → Design49 clamp stamp last, masked by the bore → edits. No walls or roofs over + host air; no corridor exemptions. +4. **Who sets P:** fixed-ground layers (SW, FI, liminal) propose the site and P; for carved layers (TN, VS) the passage + picks P and the cave planner connects. + +**Keep:** +- the spline, h(s) and the grade audit; +- the interior-air gate; +- cave binding to the end anchors; +- the L148 envelope, the L156 band fit and the L130 bounds; +- `l159-pad-stamp.patch` (stamp, bounds, LandingPadAudit plus 4 controls). + +**Delete:** +- the chamber records and their composition, and the slab return; +- the corridor exemptions and the shell-over-air fills; +- the flag and both floor blends; +- the SW plane fit; +- the chamber audits. + +## Steps + +- **0:** audits that measure: + - collect gates, exit at the end; + - seams over discs and end runs at LOD0/1; + - tagged failing samples, with no 8-sample cap; + - exempt probes counted separately, with a coverage minimum; + - a self-overlap census; + - Core-exported predicates. +- **1:** level end runs. +- **2:** no self-overlap or hairpins: + - pitch ≥ bore height + 2 shells + margin; + - lower mouth on the ring; + - end turns ≤ 60°; + - the helix kept out of the landing columns. +- **3:** the composer and the audits together, with the stamp last. +- **4:** site selection for fixed-ground layers. +- **5:** verify (fast tier; TN/VS/native-FI sweeps 16/16; versioned hashes; Clang/GCC W1/W8/W12; captures). + +**Owner look decisions:** +- the disc radius, the end-run length, the cone angle (12°) and reach (8 m); +- no walls on open-air landings; +- headroom underground; +- whether a FI landing may float or must sit on an island; +- wider spirals (~6 m or more); +- landings may move. + +## Owner decisions (2026-11-03) + +- Open-air landings: **no walls**. +- Floating Islands landings: **add a landing island to the FI plan** at each tunnel end: top at P, footprint wider + than the pad, normal island shaping at the edges. It's planned from the seed before generation, and the FI route + connects to it. (Not pathfinding the tunnel to an existing island.) + +## Owner decision: uniform layer planners (2026-11-03) + +- **Planning side: a `LayerPlanner` parent with one child per layer type:** + - `Plan(seed, settings)`; + - `OnTunnelArrival(end)`, the landing hook: nothing (stamp only), a plan record (FI landing island, liminal walkway, + cave connection), or a content request for Unreal; + - `ProvideRouteAnchors()`; + - `MakeDrawer()`. + + Planning runs once per world, so virtual dispatch costs nothing there. +- **Drawing side:** keep the `std::variant` adapters with a shared `densityAt` shape (no virtual call per sample; + `ShapeWorldInternal.h` ~121), and choose the drawer once per tile. +- **Migration:** one planner at a time; owner canonical hashes identical after each move (refactor only); the landing + hook then implements step 4. diff --git a/Standalone/Research/WorldAssemblerM3/Linux174/RunL174CaveSeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux174/RunL174CaveSeedSweep.py new file mode 100644 index 0000000..84d4247 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux174/RunL174CaveSeedSweep.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Capture composed passage, route, shell, and seam attribution for seeds 0..15.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +DEFAULT_BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l174-step0-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP_TOOL = ROOT / "Standalone/Tools/verify/source_stamp.py" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + +GATES = { + "passages": r"^\[ResolvedPassageAuditSummary\].*status=PASS$", + "seams": r"^\[PassageTileSeamAudit\].*status=PASS$", + "shell": r"^\[PassageShellAuditSummary\].*status=PASS$", + "naturalness": r"^\[PassageNaturalnessSummary\].*status=PASS$", + "mouth_profile": r"^\[PassageMouthProfileSummary\].*status=PASS$", + "ripple": r"^\[PassageRippleAuditSummary\].*status=PASS$", + "passage_interior": r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", + "self_overlap": r"^\[PassageTubeSelfOverlapAuditSummary\].*status=PASS$", +} + + +def evaluate(content: str, expected_stamp: str, layout_path: Path, + require_end_runs: bool = False) -> tuple[dict, dict, str | None]: + stamp = STAMP_RE.search(content) + checks = {name: re.search(pattern, content, re.M) is not None + for name, pattern in GATES.items()} + if require_end_runs: + checks["end_runs"] = re.search( + r"^\[PassageEndRunAuditSummary\].*status=PASS$", content, re.M) is not None + checks["source_stamp"] = stamp is not None and stamp.group(1) == expected_stamp + for kind, strate in (("tunnel_network", 2), ("vertical_shafts", 3)): + route = re.compile( + rf"^\[ResolvedStrateRouteAudit\] strate={strate} .*unsupported=0 " + rf"grade_failures=0 .*status=PASS$", re.M) + checks[f"{kind}_route"] = route.search(content) is not None + checks[f"{kind}_blocked_route_control"] = re.search( + rf"^\[{kind.title().replace('_', '')}BlockedRouteControl\].*" + r"expected=FAIL status=PASS$", content, re.M) is not None + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\] .*walkability=PASS$", content, re.M)) == 3 + checks["negative_controls"] = ( + len(re.findall(r"^\[ResolvedPassageInteriorNegativeControl\].*" + r"detected=true status=PASS$", content, re.M)) == 3 + and len(re.findall(r"^\[ResolvedPassageBlockedControl\].*detected=true$", + content, re.M)) == 3 + ) + attribution = { + "seam_samples_by_tag": { + tag: len(re.findall(rf"^\[PassageTileSeamSample\] tag={re.escape(tag)} ", + content, re.M)) + for tag in ("disc", "rim", "end run", "body", "route") + }, + "landing_support_failures_upper": len(re.findall( + r"^\[PassageLandingSupportFailure\].*side=upper", content, re.M)), + "landing_support_failures_lower": len(re.findall( + r"^\[PassageLandingSupportFailure\].*side=lower", content, re.M)), + "unsupported_walk_samples": len(re.findall( + r"^\[ResolvedWalkDispatch\] kind=unsupported", content, re.M)), + "shell_floor_samples": len(re.findall( + r"^\[PassageShellAuditSample\]", content, re.M)), + "shell_wall_samples": len(re.findall( + r"^\[PassageShellAuditSample\].*kind=wall_missing", content, re.M)), + "tube_self_overlap_rows": len(re.findall( + r"^\[PassageTubeSelfOverlapAudit\]", content, re.M)), + "tube_self_overlap_pairs": sum(int(value) for value in re.findall( + r"^\[PassageTubeSelfOverlapAudit\].*overlap_pairs=(\d+)", + content, re.M)), + "tube_end_turn_violations": sum(int(value) for value in re.findall( + r"^\[PassageTubeSelfOverlapAudit\].*end_turn_violations=(\d+)", + content, re.M)), + "tube_landing_column_violations": sum(int(value) for value in re.findall( + r"^\[PassageTubeSelfOverlapAudit\].*landing_column_violations=(\d+)", + content, re.M)), + "end_run_failures": len(re.findall( + r"^\[PassageEndRunAudit\].*status=FAIL$", content, re.M)), + "end_run_measured": len(re.findall( + r"^\[PassageEndRunAudit\].*measured_start_m=(?!0\.000)" + r".*measured_end_m=(?!0\.000)", content, re.M)), + } + seam_summary = re.search(r"^\[PassageTileSeamAudit\].*$", content, re.M) + if seam_summary: + for key in ("candidates", "examined", "pairs", "lod0_pairs", "lod0_samples", + "lod1_pairs", "lod1_samples", "landing_discs", "end_runs", + "lod0_disc_samples", "lod1_disc_samples", "lod0_rim_samples", + "lod1_rim_samples", "lod0_end_run_samples", + "lod1_end_run_samples", "exempt_pairs", "mismatches"): + match = re.search(rf"\b{key}=(\d+)", seam_summary.group(0)) + attribution[f"seam_{key}"] = int(match.group(1)) if match else None + for key in ("landing_discs", "end_runs"): + match = re.search(rf"\b{key}=(\d+)/(\d+)", seam_summary.group(0)) + if match: + attribution[f"seam_{key}_covered"] = int(match.group(1)) + attribution[f"seam_{key}_required"] = int(match.group(2)) + attribution["seam_uncovered_disc_regions"] = len(re.findall( + r"^\[PassageTileSeamCoverage\].*\bdisc_status=FAIL\b", content, re.M)) + attribution["seam_uncovered_end_run_regions"] = len(re.findall( + r"^\[PassageTileSeamCoverage\].*\bend_run_status=FAIL\b", content, re.M)) + attribution["seam_uncovered_regions"] = ( + attribution["seam_uncovered_disc_regions"] + +attribution["seam_uncovered_end_run_regions"]) + passage_summary = re.search(r"^\[ResolvedPassageAuditSummary\].*$", content, re.M) + if passage_summary: + for key in ("support_probes", "exempt_support_probes", "unsupported"): + match = re.search(rf"\b{key}=(\d+)", passage_summary.group(0)) + if match: + attribution[key] = int(match.group(1)) + identity = None + try: + if layout_path.is_file(): + identity = json.loads(layout_path.read_text(encoding="utf-8")).get( + "stable_identity") + except (OSError, json.JSONDecodeError): + pass + checks["layout_output"] = identity is not None + return checks, attribution, identity + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--binary", type=Path, default=DEFAULT_BIN) + parser.add_argument("--out", type=Path, + default=ROOT / "Saved/Linux/l174/rebaseline-l173/seeds") + parser.add_argument("--seed-start", type=int, default=0) + parser.add_argument("--seed-stop", type=int, default=16, + help="exclusive seed bound") + parser.add_argument("--expected-stamp", type=str, + help="source stamp embedded in an earlier binary") + parser.add_argument("--require-end-runs", action="store_true", + help="require the L174 end-run audit (use for post-Step-1 sweeps)") + parser.add_argument("--no-resume", action="store_true", + help="rerun even when both the log and layout already exist") + args = parser.parse_args() + if not args.binary.is_file() or not DATA.is_file(): + raise SystemExit(f"missing binary or owner fixture: {args.binary}") + args.binary = args.binary.resolve() + args.out = args.out.resolve() + args.out.mkdir(parents=True, exist_ok=True) + stamp = args.expected_stamp or subprocess.check_output( + [sys.executable, str(STAMP_TOOL), "stamp", "--root", str(ROOT)], text=True).strip() + results = [] + partial = args.out / "runs-partial.json" + for seed in range(args.seed_start, args.seed_stop): + log = args.out / f"seed{seed}.log" + layout = args.out / f"seed{seed}.layout.json" + exit_path = args.out / f"seed{seed}.exit" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(args.binary), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + "--force-generate", f"--seed={seed}", "--recipe=surface-world", + "--density-dispatch-report", "--audit-resolved-layout-passages", + "--audit-passage-tile-seams", f"--resolved-layout-output={layout}", + ] + started = time.monotonic() + resumed = False + if not args.no_resume and log.is_file() and layout.is_file(): + content = log.read_text(encoding="utf-8", errors="replace") + if exit_path.is_file(): + code: int | str = int(exit_path.read_text().strip()) + elif ("[PipelineExactQueryHash]" in content + and "[ResolvedPassageAuditSummary]" in content): + code = 10 if re.search(r"^\[[^]]*(?:Summary|Audit)\].*status=FAIL$", + content, re.M) else 0 + else: + code = "NO_EXIT_RECORD" + resumed = code != "NO_EXIT_RECORD" + if not resumed: + try: + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + code = proc.returncode + except subprocess.TimeoutExpired: + code = "OUTER_TIMEOUT" + if isinstance(code, int): + exit_path.write_text(str(code) + "\n", encoding="utf-8") + content = log.read_text(encoding="utf-8", errors="replace") if log.exists() else "" + checks, attribution, identity = evaluate( + content, stamp, layout, args.require_end_runs) + passed = code == 0 and all(checks.values()) + failed = [name for name, ok in checks.items() if not ok] + row = { + "seed": seed, + "verdict": "PASS" if passed else "FAIL", + "exit": code, + "failed_gates": failed, + "checks": checks, + "attribution": attribution, + "source_stamp": stamp, + "layout_identity": identity, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + "layout": str(layout.relative_to(ROOT)), + "resumed": resumed, + } + results.append(row) + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + print("[L174CaveSeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + summary = { + "source_stamp": stamp, + "seeds": list(range(args.seed_start, args.seed_stop)), + "passed": sum(row["verdict"] == "PASS" for row in results), + "failed": sum(row["verdict"] != "PASS" for row in results), + "results": results, + } + (args.out / "summary.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L174CaveSeedSweepSummary] passed={summary['passed']}/{len(results)} " + f"failed={summary['failed']}", flush=True) + return 0 if summary["failed"] == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 64a402a..bc9728b 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -60,6 +60,7 @@ namespace bool AuditBasePageEquivalence=false; bool AuditRouteParity=false; bool AuditPassageTileSeams=false; + bool InjectPassageSeamMismatch=false; bool AuditPassageSourceClamp=false; bool AuditPassageCarveReach=false; bool RegionCrossing=false; @@ -1476,7 +1477,8 @@ namespace " --shape-helper-limit=8|12 (default 8; shape build A/B)\n" " --audit-base-page-equivalence (hash one tile through both shape routes)\n" " --audit-route-parity (compare page, native-only, exact-query and mesh routes)\n" - " --audit-passage-tile-seams (compare shared LOD0 density samples in adjacent route tiles)\n" + " --audit-passage-tile-seams (compare shared LOD0/1 density samples across passage regions)\n" + " --inject-passage-seam-mismatch (negative control for seam gate)\n" " --audit-passage-source-clamp (compare passage source floors 2,3,4,6 over all near tiles)\n" " --audit-passage-carve-reach (compare post-composer reaches 1.5,2,2.5,4 m over all near tiles)\n" " --audit-tile-shape-readiness (run a deliberately mis-marked tile against the native-only guard)\n" @@ -1568,6 +1570,8 @@ namespace else if(arg=="--audit-base-page-equivalence")options.AuditBasePageEquivalence=true; else if(arg=="--audit-route-parity")options.AuditRouteParity=true; else if(arg=="--audit-passage-tile-seams")options.AuditPassageTileSeams=true; + else if(arg=="--inject-passage-seam-mismatch") + {options.AuditPassageTileSeams=true;options.InjectPassageSeamMismatch=true;} else if(arg=="--audit-passage-source-clamp")options.AuditPassageSourceClamp=true; else if(arg=="--audit-passage-carve-reach")options.AuditPassageCarveReach=true; else if(arg=="--region-crossing")options.RegionCrossing=true; @@ -1742,6 +1746,7 @@ int main(int argc,char** argv) const auto coldStart=Clock::now(); FOptions options; if(!ParseOptions(argc,argv,options))return 2; + bool preflightGateFailure=false; if(options.TlsProbeRequested) return StandaloneTlsProbe::Run(options.TlsProbeWorkers,options.TlsProbeTouch); std::printf("[HeadlessConfig] data=%s workers=%d clip_radius=%d max_clip_level=%d max_level=%d hash=%s hash_uniform_skips=%s column_overlap=%s force_generate=%s recipe=%s lattice_strate=%d shared_column_cache=%s camera_step_m=(%.3f,%.3f,%.3f) camera_override=%s camera_m=(%.6f,%.6f,%.6f) camera_rotation_deg=(%.3f,%.3f) seed_override=%s render_distance_override=%s\n", @@ -2392,24 +2397,24 @@ int main(int argc,char** argv) if(actualRecipe!=requestedRecipe) {std::fprintf(stderr,"Requested recipe %.*s resolved to %.*s\n", static_cast(requestedRecipe.size()),requestedRecipe.data(), - static_cast(actualRecipe.size()),actualRecipe.data());return 2;} + static_cast(actualRecipe.size()),actualRecipe.data());preflightGateFailure=true;} std::printf("[ResolvedLayoutCompatibility] accepted=%s issues=%zu error=%s\n", compatibilityAccepted?"true":"false",compatibilityIssues.size(),compatibilityError.c_str()); for(const auto& issue:compatibilityIssues) std::printf("[ResolvedLayoutCompatibilityIssue] code=%s detail=%s\n", issue.code.c_str(),issue.detail.c_str()); if(!compatibilityAccepted) - {std::fprintf(stderr,"Resolved-layout compatibility validation failed: %s\n",compatibilityError.c_str());return 2;} + {std::fprintf(stderr,"Resolved-layout compatibility validation failed: %s\n",compatibilityError.c_str());preflightGateFailure=true;} std::printf("[ResolvedLayoutStrict] accepted=%s issues=%zu error=%s\n", strictAccepted?"true":"false",strictIssues.size(),strictError.c_str()); for(const auto& issue:strictIssues) std::printf("[ResolvedLayoutStrictIssue] code=%s detail=%s\n",issue.code.c_str(),issue.detail.c_str()); if(!strictAccepted) - {std::fprintf(stderr,"Resolved-layout strict validation failed: %s\n",strictError.c_str());return 2;} + {std::fprintf(stderr,"Resolved-layout strict validation failed: %s\n",strictError.c_str());preflightGateFailure=true;} std::printf("[ResolvedLayoutValidatorControls] rejected=%u/%u status=%s\n", controls.controlsRejectedAsExpected,controls.controlsRun,controls.passed?"PASS":"FAIL"); if(!controls.passed) - {for(const auto& failure:controls.failures)std::printf("[ResolvedLayoutValidatorControlFailure] %s %s\n",failure.code.c_str(),failure.detail.c_str());return 2;} + {for(const auto& failure:controls.failures)std::printf("[ResolvedLayoutValidatorControlFailure] %s %s\n",failure.code.c_str(),failure.detail.c_str());preflightGateFailure=true;} if(options.DensityDispatchReport) { const auto& slots=ownerWorld->Manager->GetLayout(); @@ -2577,7 +2582,7 @@ int main(int argc,char** argv) plan->route.size(),MaximumRouteZ,CeilingFace-MaximumRouteZ, MaximumRouteZ(1,ownerWorld->CoreVoxelSizeMicrons), options.CameraStepMeters.Y*1000000.0f/std::max(1,ownerWorld->CoreVoxelSizeMicrons), options.CameraStepMeters.Z*1000000.0f/std::max(1,ownerWorld->CoreVoxelSizeMicrons)):FVector::ZeroVector; - return StandaloneStreamBenchmark::RunPipelinedStreamingBenchmark(ownerWorld,snapshot, + const int pipelineResult=StandaloneStreamBenchmark::RunPipelinedStreamingBenchmark(ownerWorld,snapshot, cameraVoxels,options.WorkerCount,options.MaxLevel,VoxelForgeCore::DetailVariant::coherent, scheduler,options.StreamHash,false,false,false,false,coldStart, options.ProfileColumnOverlap,options.FarHorizonLOD8InteriorStepVoxels, @@ -2690,6 +2695,8 @@ int main(int argc,char** argv) options.MovingCameraSpeedMetersPerSecond,options.MovingCameraDurationSeconds, options.MovingCameraDwellSeconds,options.ColdWorkerTimeline, options.AuditPassageSourceClamp,options.AuditPassageCarveReach, - options.AuditPassageTileSeams,options.AuditLiminalRoadWalk, + options.AuditPassageTileSeams,options.InjectPassageSeamMismatch, + options.AuditLiminalRoadWalk, options.LiminalStampWalkAuditPath); + return pipelineResult!=0?pipelineResult:(preflightGateFailure?2:0); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index bdb65d0..bfb17d3 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -2595,10 +2595,12 @@ namespace StandaloneStreamBenchmark double MovingCameraDurationSeconds=12.0,double MovingCameraDwellSeconds=5.0, bool bColdWorkerTimeline=false,bool bAuditPassageSourceClamp=false, bool bAuditPassageCarveReach=false,bool bAuditPassageTileSeams=false, + bool bInjectPassageSeamMismatch=false, bool bAuditLiminalRoadWalk=false, const std::filesystem::path& LiminalStampWalkAuditPath={}) { using Clock=std::chrono::steady_clock; + bool bAuditFailureDetected=false; const std::clock_t ColdCpuOrigin=std::clock(); const bool bColdTimeline=bColdWorkerTimeline||bInstrumentTileDrain; const auto EmitColdTimeline=[&](const char* Event,const char* Edge,Clock::time_point At) @@ -3044,9 +3046,8 @@ namespace StandaloneStreamBenchmark std::uint64_t passageInteriorCapsuleSamples=0,passageInteriorSolidSamples=0; std::uint64_t supportSamples=0,unsupported=0,gradeFailures=0; std::uint64_t supportOverlapSkips=0; - std::array,8> firstBlocked{}; - std::array,8> firstUnsupported{}; - std::size_t firstBlockedCount=0,firstUnsupportedCount=0; + std::vector> firstBlocked; + std::vector> firstUnsupported; bool passed=false; }; const double coreMetres=static_cast(World.CoreVoxelSizeMicrons)/1000000.0; @@ -3056,7 +3057,8 @@ namespace StandaloneStreamBenchmark double supportMaximumMetres,std::uint32_t minimumSupportProbes, double supportProbeRadiusMetres, const std::function& supportExemption={}, - const std::function& passageInteriorPredicate={}) + const std::function& passageInteriorPredicate={}, + std::string_view supportTraceTag={}) { FWalkAudit audit; if(path.size()<2)return audit; @@ -3077,12 +3079,9 @@ namespace StandaloneStreamBenchmark if(density<0.0f) { if(inPassageInterior)++audit.passageInteriorSolidSamples; - if(audit.firstBlockedCounthorizontal*0.2679491924311227) { - if(audit.gradeFailures<8) - std::printf("[ResolvedWalkGradeFailure] segment=%zu a_xyz_m=(%.3f,%.3f,%.3f) b_xyz_m=(%.3f,%.3f,%.3f) horizontal_m=%.6f vertical_m=%.6f grade_deg=%.3f\n", - segment-1,a[0]*coreMetres,a[1]*coreMetres, - a[2]*coreMetres,b[0]*coreMetres,b[1]*coreMetres, - b[2]*coreMetres,horizontal,vertical, - std::atan2(vertical,horizontal) - *180.0/3.14159265358979323846); + std::printf("[ResolvedWalkGradeFailure] segment=%zu a_xyz_m=(%.3f,%.3f,%.3f) b_xyz_m=(%.3f,%.3f,%.3f) horizontal_m=%.6f vertical_m=%.6f grade_deg=%.3f\n", + segment-1,a[0]*coreMetres,a[1]*coreMetres, + a[2]*coreMetres,b[0]*coreMetres,b[1]*coreMetres, + b[2]*coreMetres,horizontal,vertical, + std::atan2(vertical,horizontal) + *180.0/3.14159265358979323846); ++audit.gradeFailures; } const double length=std::sqrt(dx*dx+dy*dy+dz*dz)*coreMetres; @@ -3135,6 +3133,9 @@ namespace StandaloneStreamBenchmark double firstUnsupportedX=x,firstUnsupportedY=y, firstUnsupportedZ=footZ,lastUnsupportedDensity=0.0; std::array supportMask{}; + std::array supportExempt{}; + std::array supportLastDepth{},supportFirstSolidDepth{}; + std::array supportLastDensity{}; for(std::size_t offsetIndex=0;offsetIndex(supportTraceTag.size()),supportTraceTag.data(), + x,y,z,probe,supportOffsets[probe][0],supportOffsets[probe][1], + supportExempt[probe]?"exempt":"yes", + supportMask[probe]?"true":"false", + supportLastDepth[probe],supportFirstSolidDepth[probe], + supportLastDensity[probe],probeX,probeY,probeZ, + supportExempt[probe]?"exempt":Field.densityDispatchPathAt({ + probeX/coreMetres,probeY/coreMetres,probeZ/coreMetres}).c_str()); } } } } - audit.passed=audit.centers>0&&audit.blocked==0&&audit.unsupported==0 - &&audit.gradeFailures==0; + audit.passed=audit.centers>0&&audit.supportSamples>0 + &&audit.blocked==0&&audit.unsupported==0&&audit.gradeFailures==0; return audit; }; const auto auditLiminalRoadWalk=[&](bool required) @@ -3690,6 +3709,9 @@ namespace StandaloneStreamBenchmark double x=0.0,y=0.0,z=0.0,floorZ=0.0,radius=0.0,distance=0.0; std::size_t segmentIndex=0; }; + const double landingSubvoxelsPerMetre=static_cast( + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /World.CoreVoxelSizeMicrons; const auto blendLandingChamberFloor=[&]( const VoxelForgeCore::ResolvedLayoutPassageV1& passage, double alongMetres,double totalLengthMetres,double x,double y, @@ -3715,11 +3737,9 @@ namespace StandaloneStreamBenchmark const auto floorAt=[&]( const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) { - const double cx=qToMetres(chamber.floorCenterSubvoxels[0]); - const double cy=qToMetres(chamber.floorCenterSubvoxels[1]); - const double cz=qToMetres(chamber.floorCenterSubvoxels[2]); - return cz+chamber.floorGradientQ16[0]/65536.0*(x-cx) - +chamber.floorGradientQ16[1]/65536.0*(y-cy); + return VoxelForgeCore::ResolvedLayoutPassageLandingFloorAtXYSubvoxels( + chamber,x*landingSubvoxelsPerMetre,y*landingSubvoxelsPerMetre) + /landingSubvoxelsPerMetre; }; const double upperFloor=floorAt(passage.upperLandingChamber); const double lowerFloor=floorAt(passage.lowerLandingChamber); @@ -4063,6 +4083,9 @@ namespace StandaloneStreamBenchmark std::size_t ignoredSegment=std::numeric_limits::max(), std::size_t selectedPassage=std::numeric_limits::max()) { + const double subvoxelsPerMetre=static_cast( + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /World.CoreVoxelSizeMicrons; constexpr double CornerBlendMetres=0.50; constexpr double WallReliefMetres=0.21; const auto smoothMax=[](double left,double right,double radius) @@ -4082,20 +4105,9 @@ namespace StandaloneStreamBenchmark const auto inLandingChamber=[&]( const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) { - if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0) - return false; - const double centerX=qToMetres(chamber.floorCenterSubvoxels[0]); - const double centerY=qToMetres(chamber.floorCenterSubvoxels[1]); - const double floorCenterZ=qToMetres( - chamber.floorCenterSubvoxels[2]) - +chamber.floorGradientQ16[0]/65536.0*(xMetres-centerX) - +chamber.floorGradientQ16[1]/65536.0*(yMetres-centerY); - const double radialDistance=std::hypot(xMetres-centerX, - yMetres-centerY)-qToMetres(chamber.radiusSubvoxels); - const double floorDistance=floorCenterZ-zMetres; - const double ceilingDistance=zMetres-(floorCenterZ - +qToMetres(chamber.clearHeightSubvoxels)); - return std::max({radialDistance,floorDistance,ceilingDistance})<=0.0; + return VoxelForgeCore::IsInsideResolvedLayoutPassageLandingChamberV1( + chamber,xMetres*subvoxelsPerMetre, + yMetres*subvoxelsPerMetre,zMetres*subvoxelsPerMetre); }; if(inLandingChamber(passage.upperLandingChamber) ||inLandingChamber(passage.lowerLandingChamber))return true; @@ -4505,7 +4517,7 @@ namespace StandaloneStreamBenchmark } } } - audit.passed=worstEndpointFloorCoverage>=0.75 + audit.passed=audit.floorSamples>0&&worstEndpointFloorCoverage>=0.75 &&audit.wallSamples>=8 &&audit.floorFlatStepRunMetres==0.0&&audit.wallFlatStepRunMetres==0.0; return audit; @@ -4526,14 +4538,28 @@ namespace StandaloneStreamBenchmark const auto& path=allPassageSmoothRoutes[passageIndex]; if(path.size()<3)return audit; const double bodyFloorDepth=allPassageBodyFloorDepths[passageIndex]; + const auto& passage=ResolvedLayout->passages[passageIndex]; + const double endRun=qToMetres(passage.endRunSubvoxels); + const double bodyStart=endRun; + const double bodyEnd=path.back().distance-endRun; double flatRun=0.0; for(std::size_t i=1;i1.0e-9 + ?(clippedStart-a.distance)/span:0.0; + const double endT=span>1.0e-9 + ?(clippedEnd-a.distance)/span:1.0; + const double dx=(b.x-a.x)*(endT-startT); + const double dy=(b.y-a.y)*(endT-startT); + const double floorA=std::lerp(a.floorZ,b.floorZ,startT)+1.13; + const double floorB=std::lerp(a.floorZ,b.floorZ,endT)+1.13; const double dz=floorB-floorA; const double horizontal=std::hypot(dx,dy); if(horizontal>1.0e-6&&std::abs(dz)/horizontal @@ -5132,10 +5158,9 @@ namespace StandaloneStreamBenchmark &&blockedWorld.compile(ShapeRequest,Scheduler,blockedCompileError); if(!blockedCompiled) { - Streamer.Stop(); std::fprintf(stderr,"[ResolvedPassageBlockedControl] compile_failed=%s\n", blockedCompileError.c_str()); - return 10; + bAuditFailureDetected=true; } struct FPassageShellAudit { @@ -5148,10 +5173,8 @@ namespace StandaloneStreamBenchmark std::uint64_t floorMouthBlendSkips=0,wallMouthBlendSkips=0; std::uint64_t interiorSamples=0,interiorViolations=0; std::vector openAirDistancesMetres; - std::array,8> firstViolations{}; - std::size_t firstViolationCount=0; - std::array,8> firstWallViolations{}; - std::size_t firstWallViolationCount=0; + std::vector> firstViolations; + std::vector> firstWallViolations; }; constexpr double PassageShellAuditSpacingMetres=0.25; constexpr std::array PassageShellAuditDepthsMetres{{0.125,0.375}}; @@ -5416,12 +5439,10 @@ namespace StandaloneStreamBenchmark ||(!shellExpected&&density<-PassageShellDensityTolerance)) { ++audit.floorViolations; - if(audit.firstViolationCount=stratum->content.bottomMicrometres &&zUmcontent.topMicrometres; }; - const auto auditLandingChamber=[&](const VoxelForgeCore::World& field, + const auto auditLandingDisc=[&](const VoxelForgeCore::World& field, const VoxelForgeCore::ResolvedLayoutPassageV1& passage, std::size_t passageIndex,bool upper) { @@ -5608,18 +5626,25 @@ namespace StandaloneStreamBenchmark :passage.lowerLandingChamber; if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0) {total.passed=false;return total;} + const auto& mouth=upper?passage.upperMouthSubvoxels + :passage.lowerMouthSubvoxels; constexpr std::array,8> directions{{ {{1.0,0.0}},{{0.7071067811865475,0.7071067811865475}}, {{0.0,1.0}},{{-0.7071067811865475,0.7071067811865475}}, {{-1.0,0.0}},{{-0.7071067811865475,-0.7071067811865475}}, {{0.0,-1.0}},{{0.7071067811865475,-0.7071067811865475}}}}; - const double centerX=qToMetres(chamber.floorCenterSubvoxels[0]); - const double centerY=qToMetres(chamber.floorCenterSubvoxels[1]); + const double centerX=qToMetres(mouth[0]); + const double centerY=qToMetres(mouth[1]); const double centerZ=qToMetres(chamber.floorCenterSubvoxels[2]); - const double radius=std::min(5.75, - qToMetres(chamber.radiusSubvoxels)-1.5); - const double gx=chamber.floorGradientQ16[0]/65536.0; - const double gy=chamber.floorGradientQ16[1]/65536.0; + const std::int64_t discRadiusSubvoxels= + VoxelForgeCore::ResolvedLayoutPassageLandingDiscRadiusSubvoxels( + passage.mouthRadiusSubvoxels,World.CoreVoxelSizeMicrons); + constexpr double SupportProbeRadiusMetres=0.45; + const double radius=qToMetres(discRadiusSubvoxels) + -SupportProbeRadiusMetres; + if(discRadiusSubvoxels<=0||radius<=0.0) + {total.passed=false;return total;} + const std::array discCenter={mouth[0],mouth[1]}; const std::function interiorPredicate= [&](double x,double y,double z) {return passageInteriorWinsAt(x,y,z, @@ -5636,12 +5661,17 @@ namespace StandaloneStreamBenchmark const double distance=radius*step/intervals; const double x=centerX+direction[0]*distance; const double y=centerY+direction[1]*distance; - const double floorZ=centerZ+gx*(x-centerX)+gy*(y-centerY); + const double floorZ=centerZ; + if(!VoxelForgeCore::IsInsideResolvedLayoutPassageLandingDiscV1( + discCenter,discRadiusSubvoxels, + x*landingSubvoxelsPerMetre,y*landingSubvoxelsPerMetre)) + continue; chamberPath.push_back({x/coreMetres,y/coreMetres, (floorZ+1.13)/coreMetres}); } const FWalkAudit part=auditPlayerPath(field,chamberPath, - 0.125,0.125,1.125,3,0.45,{},interiorPredicate); + 0.125,0.125,1.125,3,0.45,{},interiorPredicate, + upper?"upper_landing":"lower_landing"); total.segments+=part.segments;total.centers+=part.centers; total.capsuleSamples+=part.capsuleSamples; total.blocked+=part.blocked; @@ -5656,6 +5686,7 @@ namespace StandaloneStreamBenchmark return total; }; std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0; + std::uint64_t passageSupportSamples=0,passageSupportExempt=0; std::uint64_t passageInteriorCapsuleSamples=0,passageInteriorSolidSamples=0; std::uint64_t passageInteriorControls=0; std::uint64_t landingChamberCount=0,landingChamberNegativeControls=0; @@ -5692,12 +5723,17 @@ namespace StandaloneStreamBenchmark std::numeric_limits::max(),passageIndex);}; const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.375,0.125, 1.125,3,0.45,passageSupportExemption,passageInteriorPredicate); - const FWalkAudit upperChamberAudit=auditLandingChamber(Shape, + const FWalkAudit upperChamberAudit=auditLandingDisc(Shape, passage,passageIndex,true); - const FWalkAudit lowerChamberAudit=auditLandingChamber(Shape, + const FWalkAudit lowerChamberAudit=auditLandingDisc(Shape, passage,passageIndex,false); const bool landingChambersWalkable=upperChamberAudit.passed &&lowerChamberAudit.passed; + passageSupportSamples+=passageAudit.supportSamples + +upperChamberAudit.supportSamples+lowerChamberAudit.supportSamples; + passageSupportExempt+=passageAudit.supportOverlapSkips + +upperChamberAudit.supportOverlapSkips + +lowerChamberAudit.supportOverlapSkips; const bool passageInteriorAirPassed= passageAudit.passageInteriorCapsuleSamples>0 &&passageAudit.passageInteriorSolidSamples==0 @@ -5793,24 +5829,32 @@ namespace StandaloneStreamBenchmark static_cast(passageAudit.gradeFailures),mouthsInContent?2:0, mouthsInRadius?2:0, passageAudit.passed&&landingChambersWalkable?"PASS":"FAIL"); - std::printf("[PassageLandingChamberAudit] id=%s side=upper capsule_samples=%llu solid_samples=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n", + std::printf("[PassageLandingDiscSupportAudit] id=%s side=upper capsule_samples=%llu solid_samples=%llu support_samples=%llu unsupported=%llu exempt_probes=%llu grade_failures=%llu status=%s\n", passage.stableId.c_str(), static_cast(upperChamberAudit.passageInteriorCapsuleSamples), static_cast(upperChamberAudit.passageInteriorSolidSamples), static_cast(upperChamberAudit.supportSamples), static_cast(upperChamberAudit.unsupported), + static_cast(upperChamberAudit.supportOverlapSkips), static_cast(upperChamberAudit.gradeFailures), upperChamberAudit.passed&&upperChamberAudit.passageInteriorSolidSamples==0 &&upperChamberAudit.passageInteriorCapsuleSamples>0?"PASS":"FAIL"); - std::printf("[PassageLandingChamberAudit] id=%s side=lower capsule_samples=%llu solid_samples=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n", + for(const auto& sample:upperChamberAudit.firstUnsupported) + std::printf("[PassageLandingSupportFailure] id=%s side=upper xyz_m=(%.3f,%.3f,%.3f) density=%.9g\n", + passage.stableId.c_str(),sample[0],sample[1],sample[2],sample[3]); + std::printf("[PassageLandingDiscSupportAudit] id=%s side=lower capsule_samples=%llu solid_samples=%llu support_samples=%llu unsupported=%llu exempt_probes=%llu grade_failures=%llu status=%s\n", passage.stableId.c_str(), static_cast(lowerChamberAudit.passageInteriorCapsuleSamples), static_cast(lowerChamberAudit.passageInteriorSolidSamples), static_cast(lowerChamberAudit.supportSamples), static_cast(lowerChamberAudit.unsupported), + static_cast(lowerChamberAudit.supportOverlapSkips), static_cast(lowerChamberAudit.gradeFailures), lowerChamberAudit.passed&&lowerChamberAudit.passageInteriorSolidSamples==0 &&lowerChamberAudit.passageInteriorCapsuleSamples>0?"PASS":"FAIL"); + for(const auto& sample:lowerChamberAudit.firstUnsupported) + std::printf("[PassageLandingSupportFailure] id=%s side=lower xyz_m=(%.3f,%.3f,%.3f) density=%.9g\n", + passage.stableId.c_str(),sample[0],sample[1],sample[2],sample[3]); std::printf("[PassageNaturalnessAudit] id=%s flat_run_max_m=%.3f limit_m=%.3f wall_relief_p2p_m=%.3f minimum_m=%.3f samples=%llu missing=%llu status=%s\n", passage.stableId.c_str(),naturalAudit.maximumFlatRunMetres,MaximumFlatRunMetres, naturalAudit.wallReliefPeakToPeakMetres,MinimumWallReliefMetres, @@ -5890,7 +5934,7 @@ namespace StandaloneStreamBenchmark &&shellAudit.chamberWallViolations==0 &&shellAudit.interiorViolations==0 ?"PASS":"FAIL"); - for(std::size_t i=0;i::infinity(); @@ -5959,7 +6003,7 @@ namespace StandaloneStreamBenchmark blockedWorld.densityDispatchPathAt(nativePosition).c_str(), passageInterior?"true":"false"); } - for(std::size_t i=0;i(routeAudit.unsupported), static_cast(routeAudit.gradeFailures),binding?"PASS":"FAIL", routeAudit.passed&&binding?"PASS":"FAIL"); - for(std::size_t i=0;i::max(),passageIndex);}; const FWalkAudit blockedAudit=auditPlayerPath(blockedWorld,path, 0.125,0.125,0.25,3,0.45,{},passageInteriorPredicate); - const FWalkAudit upperChamberBlocked=auditLandingChamber(blockedWorld, + const FWalkAudit upperChamberBlocked=auditLandingDisc(blockedWorld, passage,passageIndex,true); - const FWalkAudit lowerChamberBlocked=auditLandingChamber(blockedWorld, + const FWalkAudit lowerChamberBlocked=auditLandingDisc(blockedWorld, passage,passageIndex,false); blockedControls+=blockedAudit.blocked>0&&!blockedAudit.passed; const bool upperChamberControlDetected= @@ -6242,6 +6286,7 @@ namespace StandaloneStreamBenchmark static_cast(landingChamberCount -landingChamberNegativeControls)); const bool shellAuditPassed=shellAudits==passageCount + &&shellFloorSamples>0&&shellWallSamples>0&&shellInteriorSamples>0 &&shellFloorViolations==0&&shellWallViolations==0 &&shellChamberFloorViolations==0&&shellChamberWallViolations==0 &&shellInteriorViolations==0&&shellOpenAirSamples>0; @@ -6285,21 +6330,87 @@ namespace StandaloneStreamBenchmark floorRippleControlsDetected==passageCount?"DETECTED":"MISSED", floorStepRippleControlsDetected==passageCount?"DETECTED":"MISSED", ripplePasses==passageCount?"PASS":"FAIL"); + std::uint64_t selfOverlapAudits=0,selfOverlapMeasured=0, + selfOverlapExempt=0,selfOverlapPairs=0,selfOverlapPassages=0, + selfOverlapColumnViolations=0,selfOverlapTurnViolations=0; + for(const auto& passage:ResolvedLayout->passages) + { + const auto overlap=VoxelForgeCore:: + AuditResolvedLayoutPassageSelfOverlapV1(passage, + World.CoreVoxelSizeMicrons); + ++selfOverlapAudits; + selfOverlapMeasured+=overlap.measuredSegmentPairs; + selfOverlapExempt+=overlap.localExemptSegmentPairs; + selfOverlapPairs+=overlap.overlappingSegmentPairs; + selfOverlapColumnViolations+=overlap.landingColumnViolations; + selfOverlapTurnViolations+=overlap.endTurnViolations; + selfOverlapPassages+=overlap.passed?1u:0u; + std::printf("[PassageTubeSelfOverlapAudit] id=%s measured_pairs=%llu local_exempt_pairs=%llu overlap_pairs=%llu first_overlap_segments=(%llu,%llu) end_turn_max_deg=%.3f end_turn_violations=%u landing_column_violations=%llu minimum_clearance_m=%.6f status=%s\n", + passage.stableId.c_str(), + static_cast(overlap.measuredSegmentPairs), + static_cast(overlap.localExemptSegmentPairs), + static_cast(overlap.overlappingSegmentPairs), + static_cast(overlap.firstOverlapSegmentA), + static_cast(overlap.firstOverlapSegmentB), + overlap.maximumEndTurnDegrees,overlap.endTurnViolations, + static_cast(overlap.landingColumnViolations), + overlap.minimumClearanceMetres,overlap.passed?"PASS":"FAIL"); + } + const bool selfOverlapAuditPassed=passageCount>0 + &&selfOverlapAudits==passageCount&&selfOverlapMeasured>0 + &&selfOverlapPairs==0&&selfOverlapPassages==passageCount; + std::printf("[PassageTubeSelfOverlapAuditSummary] passages=%llu passed=%llu measured_pairs=%llu local_exempt_pairs=%llu overlap_pairs=%llu end_turn_violations=%llu landing_column_violations=%llu status=%s\n", + static_cast(selfOverlapAudits), + static_cast(selfOverlapPassages), + static_cast(selfOverlapMeasured), + static_cast(selfOverlapExempt), + static_cast(selfOverlapPairs), + static_cast(selfOverlapTurnViolations), + static_cast(selfOverlapColumnViolations), + selfOverlapAuditPassed?"PASS":"FAIL"); + std::uint64_t endRunAudits=0,endRunMeasured=0,endRunPassages=0; + for(const auto& passage:ResolvedLayout->passages) + { + const auto endRun=VoxelForgeCore::AuditResolvedLayoutPassageEndRunsV1( + passage,World.CoreVoxelSizeMicrons); + ++endRunAudits; + endRunMeasured+=endRun.hasMeasuredCoverage?1u:0u; + endRunPassages+=endRun.passed?1u:0u; + std::printf("[PassageEndRunAudit] id=%s required_m=%.3f measured_start_m=%.3f measured_end_m=%.3f start_deviation_m=%.6f end_deviation_m=%.6f start_grade_deg=%.6f end_grade_deg=%.6f status=%s\n", + passage.stableId.c_str(),endRun.requiredEndRunMetres, + endRun.measuredStartMetres,endRun.measuredEndMetres, + endRun.startMaximumFloorDeviationMetres, + endRun.endMaximumFloorDeviationMetres, + endRun.startMaximumGradeDegrees,endRun.endMaximumGradeDegrees, + endRun.passed?"PASS":"FAIL"); + } + const bool endRunAuditPassed=passageCount>0&&endRunAudits==passageCount + &&endRunMeasured>0&&endRunPassages==passageCount; + std::printf("[PassageEndRunAuditSummary] passages=%llu measured=%llu passed=%llu status=%s\n", + static_cast(endRunAudits), + static_cast(endRunMeasured), + static_cast(endRunPassages), + endRunAuditPassed?"PASS":"FAIL"); const bool auditPassed=passageCount>0&&passagePasses==passageCount + &&passageSupportSamples>0 &&passageInteriorAuditPassed &&naturalPasses==passageCount&&l23SmoothWallDetected &&mouthProfilePasses==passageCount&&l23TerraceControlDetected &&ripplePasses==passageCount &&shellAuditPassed + &&selfOverlapAuditPassed + &&endRunAuditPassed &&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings &&routeBindingFailures==0 &&liminalRoadAuditPassed &&tunnelNetworkRoutes==tunnelNetworkBlockedControls &&verticalShaftRoutes==verticalShaftBlockedControls &&blockedCompiled&&blockedControls==passageCount; - std::printf("[ResolvedPassageAuditSummary] interfaces=%llu passed=%llu natural=%llu/%llu strate_routes=%llu route_walkable=%llu route_points=%llu route_bindings=%llu route_binding_failures=%llu tunnel_network_block_controls=%llu/%llu vertical_shaft_block_controls=%llu/%llu blocked_controls=%llu/%llu status=%s\n", + std::printf("[ResolvedPassageAuditSummary] interfaces=%llu passed=%llu natural=%llu/%llu support_probes=%llu exempt_support_probes=%llu strate_routes=%llu route_walkable=%llu route_points=%llu route_bindings=%llu route_binding_failures=%llu tunnel_network_block_controls=%llu/%llu vertical_shaft_block_controls=%llu/%llu blocked_controls=%llu/%llu status=%s\n", static_cast(passageCount),static_cast(passagePasses), static_cast(naturalPasses),static_cast(passageCount), + static_cast(passageSupportSamples), + static_cast(passageSupportExempt), static_cast(routePasses),static_cast(routeWalkPasses), static_cast(routePoints), static_cast(routeBindings),static_cast(routeBindingFailures), @@ -6309,13 +6420,14 @@ namespace StandaloneStreamBenchmark static_cast(verticalShaftRoutes), static_cast(blockedControls),static_cast(passageCount), auditPassed?"PASS":"FAIL"); - if(!auditPassed) - {Streamer.Stop();return 10;} + bAuditFailureDetected=bAuditFailureDetected||!auditPassed; } if(bAuditResolvedLayoutBoundaries) { if(ResolvedLayout==nullptr) - {Streamer.Stop();std::fprintf(stderr,"[ResolvedLayoutProbe] missing plan-time snapshot\n");return 2;} + {std::fprintf(stderr,"[ResolvedLayoutProbe] missing plan-time snapshot\n");bAuditFailureDetected=true;} + else + { std::vector endpoints; for(const auto& Stratum:ResolvedLayout->strates) { @@ -6401,8 +6513,12 @@ namespace StandaloneStreamBenchmark std::printf("[ResolvedLayoutProbeSummary] probes=%llu disagreements=%llu status=%s\n", static_cast(probes),static_cast(disagreements), disagreements==0?"PASS":"DEVIATIONS_RECORDED"); - Streamer.Stop(); - return 0; + } + if(!bAuditPassageTileSeams) + { + Streamer.Stop(); + return bAuditFailureDetected?10:0; + } } const auto ShapeReady=Clock::now(); const double ShapeSeconds=std::chrono::duration(ShapeReady-ShapeBuildStart).count(); @@ -6441,9 +6557,8 @@ namespace StandaloneStreamBenchmark Compared,Mismatched,Mismatched==0?"PASS":"FAIL"); if(Mismatched!=0) { - Streamer.Stop(); std::fprintf(stderr,"[StreamPipeline] native-only tile generation differs from compiled generation.\n"); - return 9; + bAuditFailureDetected=true; } } if(bAuditBasePageEquivalence) @@ -6472,8 +6587,7 @@ namespace StandaloneStreamBenchmark if(AuditKeys.empty()) { std::printf("[BasePageTileEquivalence] status=UNAVAILABLE reason=no_tile_reachable_through_both_routes\n"); - Streamer.Stop(); - return 9; + bAuditFailureDetected=true; } VoxelForgeCore::TileGenerationOptions HashOptions; HashOptions.hashDensity=true; @@ -6542,8 +6656,7 @@ namespace StandaloneStreamBenchmark bFoundEquivalentTile?"PASS":"FAIL"); if(!bFoundEquivalentTile) { - Streamer.Stop(); - return 9; + bAuditFailureDetected=true; } } if(bAuditRouteParity) @@ -6617,8 +6730,7 @@ namespace StandaloneStreamBenchmark if(Rows.empty()) { std::printf("[RouteParityAudit] status=UNAVAILABLE reason=no_page_route_candidates\n"); - Streamer.Stop(); - return 9; + bAuditFailureDetected=true; } VoxelForgeCore::TileGenerationOptions PageOptions=CaptureOptions; @@ -6649,8 +6761,7 @@ namespace StandaloneStreamBenchmark if(!bPagesCompiled) { std::printf("[RouteParityAudit] page_compile=FAIL error=%s\n",PageCompileError.c_str()); - Streamer.Stop(); - return 9; + bAuditFailureDetected=true; } for(FRouteAuditRow& Row:Rows) { @@ -6983,127 +7094,692 @@ namespace StandaloneStreamBenchmark bRouteParityPassing?"PASS":"FAIL"); if(!bRouteParityPassing) { - Streamer.Stop(); - return 9; + bAuditFailureDetected=true; } } if(bAuditPassageTileSeams) { - std::unordered_set desiredKeys( - Desired.Tiles.begin(),Desired.Tiles.end()); - std::vector> candidates; - candidates.reserve(128); - for(const FVoxelTileKey& nativeA:Desired.Tiles) + struct FSeamPair { - if(nativeA.Level!=0)continue; - const VoxelForgeCore::TileKey a{{nativeA.Coord.X,nativeA.Coord.Y, - nativeA.Coord.Z},0}; - if(Shape.passageOverlapScore(a)==0)continue; + VoxelForgeCore::TileKey a,b; + std::size_t axis=0; + std::vector coverageRegions; + }; + struct FSeamCoverage + { + std::uint64_t discSamples[2][2]{}; + std::uint64_t rimSamples[2][2]{}; + std::uint64_t endRunSamples[2][2]{}; + }; + struct FSeamCoverageRegion + { + std::size_t passageIndex=0; + int side=0; + int kind=0; // 0=landing disc, 1=landing rim, 2=first/last end run. + }; + std::vector coverage(ResolvedLayout + ?ResolvedLayout->passages.size():0); + std::vector coverageRegions; + coverageRegions.reserve(coverage.size()*6); + for(std::size_t passageIndex=0;passageIndex> passageAlong(coverage.size()); + std::vector passageLengths(coverage.size(),0.0); + std::vector candidates; + std::uint64_t exemptPairs=0; + const double coreMetres=static_cast(World.CoreVoxelSizeMicrons)/1.0e6; + const double subvoxelsPerMetre=static_cast( + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)/coreMetres; + const auto addBounds=[&](const std::array& minimum, + const std::array& maximum,int lod, + std::size_t passageIndex,int coverageKind,int side) + { + const std::int64_t span=static_cast( + VoxelForgeCore::CoreTileVoxelCount)<(std::floor(voxel/span));}; + const std::array low={tileCoordinate(minimum[0]), + tileCoordinate(minimum[1]),tileCoordinate(minimum[2])}; + const std::array high={tileCoordinate(maximum[0]), + tileCoordinate(maximum[1]),tileCoordinate(maximum[2])}; + std::unordered_set regionTiles; + for(std::int32_t z=low[2];z<=high[2];++z) + for(std::int32_t y=low[1];y<=high[1];++y) + for(std::int32_t x=low[0];x<=high[0];++x) + regionTiles.insert({FIntVector(x,y,z),lod}); + for(const FVoxelTileKey& key:regionTiles) for(std::size_t axis=0;axis<3;++axis) { - FVoxelTileKey nativeB=nativeA; - if(axis==0)++nativeB.Coord.X; - else if(axis==1)++nativeB.Coord.Y; - else ++nativeB.Coord.Z; - if(!desiredKeys.contains(nativeB))continue; - const VoxelForgeCore::TileKey b{{nativeB.Coord.X,nativeB.Coord.Y, - nativeB.Coord.Z},0}; - if(Shape.passageOverlapScore(b)>0) - candidates.emplace_back(a,b,axis); + FVoxelTileKey adjacent=key; + if(axis==0)++adjacent.Coord.X; + else if(axis==1)++adjacent.Coord.Y; + else ++adjacent.Coord.Z; + if(!regionTiles.contains(adjacent))continue; + FSeamPair candidate{ + {{key.Coord.X,key.Coord.Y,key.Coord.Z},lod}, + {{adjacent.Coord.X,adjacent.Coord.Y,adjacent.Coord.Z},lod},axis,{}}; + if(passageIndex=0&&side<2) + { + const int regionKind=coverageKind==0?0 + :(coverageKind==1?2:(coverageKind==4?1:-1)); + if(regionKind>=0) + candidate.coverageRegions.push_back(passageIndex*6 + +static_cast(side*3+regionKind)); + } + candidates.push_back(std::move(candidate)); + } + }; + if(ResolvedLayout) + for(std::size_t passageIndex=0; + passageIndexpassages.size();++passageIndex) + { + const auto& passage=ResolvedLayout->passages[passageIndex]; + if(passage.controlPoints.size()<2)continue; + const auto qToMetres=[&](std::int64_t q) + {return static_cast(q)*coreMetres + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;}; + std::vector along(passage.controlPoints.size(),0.0); + for(std::size_t i=1;i aMetres={qToMetres(a.positionSubvoxels[0]), + qToMetres(a.positionSubvoxels[1]),qToMetres(a.positionSubvoxels[2])}; + const std::array bMetres={qToMetres(b.positionSubvoxels[0]), + qToMetres(b.positionSubvoxels[1]),qToMetres(b.positionSubvoxels[2])}; + for(const int lod:{0,1}) + { + const auto tubeBounds=[&](const std::array& from, + const std::array& to,double expand,int kind,int side) + { + std::array low{},high{}; + for(std::size_t axis=0;axis<3;++axis) + { + low[axis]=(std::min(from[axis],to[axis])-expand) + /coreMetres; + high[axis]=(std::max(from[axis],to[axis])+expand) + /coreMetres; + } + addBounds(low,high,lod,passageIndex,kind,side); + }; + const auto pointAt=[&](double t) + { + std::array point{}; + for(std::size_t axis=0;axis<3;++axis) + point[axis]=std::lerp(aMetres[axis],bMetres[axis],t); + return point; + }; + const double segmentLength=end-start; + if(firstRun) + { + const double clippedStart=start; + const double clippedEnd=std::min(end,20.0); + if(clippedEnd>=clippedStart) + tubeBounds(pointAt(segmentLength>0.0 + ?(clippedStart-start)/segmentLength:0.0), + pointAt(segmentLength>0.0 + ?(clippedEnd-start)/segmentLength:0.0), + lowRadius,1,0); + } + if(lastRun) + { + const double clippedStart=std::max(start,routeLength-20.0); + const double clippedEnd=end; + if(clippedEnd>=clippedStart) + tubeBounds(pointAt(segmentLength>0.0 + ?(clippedStart-start)/segmentLength:0.0), + pointAt(segmentLength>0.0 + ?(clippedEnd-start)/segmentLength:0.0), + lowRadius,1,1); + } + if(!firstRun&&!lastRun) + tubeBounds(aMetres,bMetres,lowRadius,2,-1); + tubeBounds(aMetres,bMetres,radius+1.0,3,-1); + } + } + for(int side=0;side<2;++side) + { + const auto& chamber=side==0?passage.upperLandingChamber + :passage.lowerLandingChamber; + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0)continue; + const auto& mouth=side==0?passage.upperMouthSubvoxels + :passage.lowerMouthSubvoxels; + const double cx=qToMetres(mouth[0]); + const double cy=qToMetres(mouth[1]); + const double floor=qToMetres(chamber.floorCenterSubvoxels[2]); + const std::int64_t radiusSubvoxels= + VoxelForgeCore::ResolvedLayoutPassageLandingDiscRadiusSubvoxels( + passage.mouthRadiusSubvoxels,World.CoreVoxelSizeMicrons); + const double radius=qToMetres(radiusSubvoxels); + const double floorExtent=0.0; + const double top=floor+qToMetres(chamber.clearHeightSubvoxels); + const std::array minimum={ + (cx-radius)/coreMetres,(cy-radius)/coreMetres, + (floor-floorExtent-0.75)/coreMetres}; + const std::array maximum={ + (cx+radius)/coreMetres,(cy+radius)/coreMetres, + (top+floorExtent)/coreMetres}; + const std::array rimMinimum={ + (cx-radius-0.5)/coreMetres,(cy-radius-0.5)/coreMetres, + (floor-floorExtent-1.0)/coreMetres}; + const std::array rimMaximum={ + (cx+radius+0.5)/coreMetres,(cy+radius+0.5)/coreMetres, + (top+floorExtent+0.5)/coreMetres}; + for(const int lod:{0,1}) + { + addBounds(minimum,maximum,lod,passageIndex,0,side); + addBounds(rimMinimum,rimMaximum,lod,passageIndex,4,side); + } } } - + const auto pairKey=[](const FSeamPair& pair) + {return std::tuple(pair.a.lod,pair.a.coordinate.x,pair.a.coordinate.y, + pair.a.coordinate.z,pair.axis);}; + std::sort(candidates.begin(),candidates.end(),[&](const auto& a,const auto& b) + {return pairKey(a) mergedCandidates; + mergedCandidates.reserve(candidates.size()); + for(auto& candidate:candidates) + { + if(mergedCandidates.empty() + ||pairKey(mergedCandidates.back())!=pairKey(candidate)) + { + mergedCandidates.push_back(std::move(candidate)); + continue; + } + auto& regions=mergedCandidates.back().coverageRegions; + regions.insert(regions.end(),candidate.coverageRegions.begin(), + candidate.coverageRegions.end()); + } + for(auto& candidate:mergedCandidates) + { + auto& regions=candidate.coverageRegions; + std::sort(regions.begin(),regions.end()); + regions.erase(std::unique(regions.begin(),regions.end()),regions.end()); + } + candidates=std::move(mergedCandidates); VoxelForgeCore::TileGenerationOptions seamOptions; seamOptions.hashDensity=true; seamOptions.captureDensitySamples=true; seamOptions.forceFullDensityCapture=true; - seamOptions.profilePassageComposer=true; - const auto sampleIndex=[](std::int32_t x,std::int32_t y,std::int32_t z) + std::unordered_map, + FStandaloneTileKeyHash> tileCache; + const auto generateSeamTile=[&](const VoxelForgeCore::TileKey& key, + std::string& error)->const VoxelForgeCore::TileMesh* { - constexpr std::size_t Dim=VoxelForgeCore::CoreTileVoxelCount+3; - return (static_cast(z)*Dim+static_cast(y))*Dim - +static_cast(x); + const FVoxelTileKey native{FIntVector(key.coordinate.x,key.coordinate.y, + key.coordinate.z),key.lod}; + const auto found=tileCache.find(native); + if(found!=tileCache.end())return found->second.get(); + auto generated=Shape.generateTile(key,seamOptions,error); + if(!generated)return nullptr; + auto tile=std::make_shared(std::move(*generated)); + const auto* result=tile.get(); + tileCache.emplace(native,std::move(tile)); + return result; }; - std::uint64_t comparedSamples=0,mismatches=0; + const auto sampleIndex=[](const VoxelForgeCore::TileMesh& tile, + const std::array& index) + { + const auto& dimensions=tile.debugDensityDimensions; + return (static_cast(index[2]) + *static_cast(dimensions[1]) + +static_cast(index[1])) + *static_cast(dimensions[0]) + +static_cast(index[0]); + }; + const auto sampleIndexAt=[](const VoxelForgeCore::TileMesh& tile, + const std::array& world, + std::array& index) + { + if(tile.debugDensityStepVoxels<=0)return false; + for(std::size_t axis=0;axis<3;++axis) + { + const std::int64_t delta=world[axis]-tile.debugDensityOriginVoxels[axis]; + if(delta%tile.debugDensityStepVoxels!=0)return false; + const std::int64_t coordinate=delta/tile.debugDensityStepVoxels; + if(coordinate<0||coordinate>=tile.debugDensityDimensions[axis])return false; + index[axis]=static_cast(coordinate); + } + return true; + }; + std::uint64_t comparedSamples=0,mismatches=0,lod0Samples=0,lod1Samples=0; + std::uint64_t lod0Pairs=0,lod1Pairs=0; + std::uint64_t coveredDiscs=0,requiredDiscs=0,coveredEndRuns=0,requiredEndRuns=0; std::size_t testedPairs=0,examinedPairs=0; double maximumDelta=0.0; - bool passed=false; - for(const auto& [a,b,axis]:candidates) + bool injectionUsed=false,directControlReported=false; + for(const auto& candidate:candidates) { - if(examinedPairs++>=128)break; + const auto& a=candidate.a; + const auto& b=candidate.b; + const std::size_t axis=candidate.axis; + ++examinedPairs; std::string errorA,errorB; - const auto tileA=Shape.generateTile(a,seamOptions,errorA); - const auto tileB=Shape.generateTile(b,seamOptions,errorB); + const auto* tileA=generateSeamTile(a,errorA); + const auto* tileB=generateSeamTile(b,errorB); if(!tileA||!tileB) { - if(examinedPairs==128) - std::printf("[PassageTileSeamCandidate] keyA=(%d,%d,%d,lod0) keyB=(%d,%d,%d,lod0) status=SKIP errorA=%s errorB=%s\n", - a.coordinate.x,a.coordinate.y,a.coordinate.z,b.coordinate.x, - b.coordinate.y,b.coordinate.z,errorA.c_str(),errorB.c_str()); + ++exemptPairs; + std::printf("[PassageTileSeamCandidate] lod=%d keyA=(%d,%d,%d) keyB=(%d,%d,%d) axis=%c status=FAIL errorA=%s errorB=%s\n", + a.lod,a.coordinate.x,a.coordinate.y,a.coordinate.z,b.coordinate.x, + b.coordinate.y,b.coordinate.z,"xyz"[axis],errorA.c_str(),errorB.c_str()); continue; } - if(tileA->diagnostics.passageSweepRouteSegmentCandidates==0 - ||tileB->diagnostics.passageSweepRouteSegmentCandidates==0)continue; - constexpr std::int32_t Dim=VoxelForgeCore::CoreTileVoxelCount+3; - const std::size_t expected=static_cast(Dim)*Dim*Dim; - if(tileA->debugDensitySamples.size()!=expected - ||tileB->debugDensitySamples.size()!=expected) + const auto captureValid=[](const VoxelForgeCore::TileMesh& tile) { - std::printf("[PassageTileSeam] axis=%c keyA=(%d,%d,%d,lod0) keyB=(%d,%d,%d,lod0) shared_samples=0 mismatches=1 max_abs_delta=inf status=FAIL reason=missing_full_density_capture\n", - "xyz"[axis],a.coordinate.x,a.coordinate.y,a.coordinate.z, + const auto& dimensions=tile.debugDensityDimensions; + return tile.debugDensityStepVoxels>0&&dimensions[0]>0 + &&dimensions[1]>0&&dimensions[2]>0 + &&tile.debugDensitySamples.size()==static_cast(dimensions[0]) + *static_cast(dimensions[1]) + *static_cast(dimensions[2]); + }; + if(!captureValid(*tileA)||!captureValid(*tileB)) + { + std::printf("[PassageTileSeam] lod=%d axis=%c keyA=(%d,%d,%d) keyB=(%d,%d,%d) shared_samples=0 mismatches=1 max_abs_delta=inf status=FAIL reason=missing_full_density_capture\n", + a.lod,"xyz"[axis],a.coordinate.x,a.coordinate.y,a.coordinate.z, b.coordinate.x,b.coordinate.y,b.coordinate.z); ++mismatches; - break; + ++exemptPairs; + continue; + } + std::printf("[PassageTileSeamCapture] lod=%d axis=%c keyA=(%d,%d,%d) originA=(%d,%d,%d) dimensionsA=(%d,%d,%d) stepA=%d keyB=(%d,%d,%d) originB=(%d,%d,%d) dimensionsB=(%d,%d,%d) stepB=%d\n", + a.lod,"xyz"[axis],a.coordinate.x,a.coordinate.y,a.coordinate.z, + tileA->debugDensityOriginVoxels[0],tileA->debugDensityOriginVoxels[1], + tileA->debugDensityOriginVoxels[2],tileA->debugDensityDimensions[0], + tileA->debugDensityDimensions[1],tileA->debugDensityDimensions[2], + tileA->debugDensityStepVoxels,b.coordinate.x,b.coordinate.y,b.coordinate.z, + tileB->debugDensityOriginVoxels[0],tileB->debugDensityOriginVoxels[1], + tileB->debugDensityOriginVoxels[2],tileB->debugDensityDimensions[0], + tileB->debugDensityDimensions[1],tileB->debugDensityDimensions[2], + tileB->debugDensityStepVoxels); + std::array overlapMinimum{},overlapMaximum{}; + for(std::size_t component=0;component<3;++component) + { + const std::int64_t aOrigin=tileA->debugDensityOriginVoxels[component]; + const std::int64_t bOrigin=tileB->debugDensityOriginVoxels[component]; + const std::int64_t aMaximum=aOrigin + +static_cast(tileA->debugDensityDimensions[component]-1) + *tileA->debugDensityStepVoxels; + const std::int64_t bMaximum=bOrigin + +static_cast(tileB->debugDensityDimensions[component]-1) + *tileB->debugDensityStepVoxels; + overlapMinimum[component]=std::max(aOrigin,bOrigin); + overlapMaximum[component]=std::min(aMaximum,bMaximum); + } + const std::int64_t extent=static_cast( + VoxelForgeCore::CoreTileVoxelCount)< tileCoordinate={ + a.coordinate.x,a.coordinate.y,a.coordinate.z}; + const std::int64_t seamCoordinate= + (static_cast(tileCoordinate[axis])+1)*extent; + overlapMinimum[axis]=std::max(overlapMinimum[axis],seamCoordinate); + overlapMaximum[axis]=std::min(overlapMaximum[axis],seamCoordinate); + if(overlapMinimum[axis]>overlapMaximum[axis]) + { + ++exemptPairs; + std::printf("[PassageTileSeam] lod=%d axis=%c keyA=(%d,%d,%d) keyB=(%d,%d,%d) shared_samples=0 mismatches=0 max_abs_delta=0 status=FAIL reason=no_common_capture_plane\n", + a.lod,"xyz"[axis],a.coordinate.x,a.coordinate.y,a.coordinate.z, + b.coordinate.x,b.coordinate.y,b.coordinate.z); + continue; } std::uint64_t pairMismatches=0; + std::uint64_t pairSamples=0; double pairMaximumDelta=0.0; - for(std::int32_t v=0;v world={x,y,z}; std::array coordinateA{},coordinateB{}; - coordinateA[axis]=Dim-2; - coordinateB[axis]=1; - coordinateA[(axis+1)%3]=v; - coordinateB[(axis+1)%3]=v; - coordinateA[(axis+2)%3]=w; - coordinateB[(axis+2)%3]=w; - const float densityA=tileA->debugDensitySamples[sampleIndex( - coordinateA[0],coordinateA[1],coordinateA[2])]; - const float densityB=tileB->debugDensitySamples[sampleIndex( - coordinateB[0],coordinateB[1],coordinateB[2])]; - if(std::bit_cast(densityA) - !=std::bit_cast(densityB)) + if(!sampleIndexAt(*tileA,world,coordinateA) + ||!sampleIndexAt(*tileB,world,coordinateB))continue; + const float densityA=tileA->debugDensitySamples[ + sampleIndex(*tileA,coordinateA)]; + const float densityB=tileB->debugDensitySamples[ + sampleIndex(*tileB,coordinateB)]; + ++pairSamples; + const std::array pointVoxels={ + static_cast(x),static_cast(y),static_cast(z)}; + if(ResolvedLayout&&!candidate.coverageRegions.empty()) { + const double pointXMetres=pointVoxels[0]*coreMetres; + const double pointYMetres=pointVoxels[1]*coreMetres; + const double pointZMetres=pointVoxels[2]*coreMetres; + const double pointXSubvoxels=pointXMetres*subvoxelsPerMetre; + const double pointYSubvoxels=pointYMetres*subvoxelsPerMetre; + const double pointZSubvoxels=pointZMetres*subvoxelsPerMetre; + const auto qToMetres=[&](std::int64_t q) + {return static_cast(q)*coreMetres + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;}; + for(const std::size_t regionIndex:candidate.coverageRegions) + { + if(regionIndex>=coverageRegions.size())continue; + const auto& region=coverageRegions[regionIndex]; + if(region.passageIndex>=ResolvedLayout->passages.size() + ||region.passageIndex>=coverage.size())continue; + const auto& passage=ResolvedLayout->passages[region.passageIndex]; + if(region.kind<=1) + { + const auto& chamber=region.side==0 + ?passage.upperLandingChamber + :passage.lowerLandingChamber; + const auto& mouth=region.side==0 + ?passage.upperMouthSubvoxels + :passage.lowerMouthSubvoxels; + if(chamber.radiusSubvoxels<=0 + ||chamber.clearHeightSubvoxels<=0)continue; + const std::int64_t radiusSubvoxels= + VoxelForgeCore::ResolvedLayoutPassageLandingDiscRadiusSubvoxels( + passage.mouthRadiusSubvoxels, + World.CoreVoxelSizeMicrons); + const std::array center={mouth[0],mouth[1]}; + const double floorSubvoxels= + VoxelForgeCore::ResolvedLayoutPassageLandingFloorAtXYSubvoxels( + chamber,pointXSubvoxels,pointYSubvoxels); + const bool inVerticalRange=pointZSubvoxels>=floorSubvoxels + -0.75*subvoxelsPerMetre + &&pointZSubvoxels<=floorSubvoxels + +chamber.clearHeightSubvoxels; + bool measured=false; + if(region.kind==0) + { + measured=inVerticalRange + &&VoxelForgeCore::IsInsideResolvedLayoutPassageLandingDiscV1( + center,radiusSubvoxels,pointXSubvoxels, + pointYSubvoxels); + if(measured) + ++coverage[region.passageIndex].discSamples[ + region.side][a.lod]; + } + else + { + const std::int64_t rimHalfWidth=static_cast( + std::llround(0.5*subvoxelsPerMetre)); + const bool inRimVerticalRange=pointZSubvoxels>=floorSubvoxels + -0.75*subvoxelsPerMetre + &&pointZSubvoxels<=floorSubvoxels + +chamber.clearHeightSubvoxels + +0.5*subvoxelsPerMetre; + measured=inRimVerticalRange + &&VoxelForgeCore::IsInsideResolvedLayoutPassageLandingRimV1( + center,radiusSubvoxels,rimHalfWidth, + pointXSubvoxels,pointYSubvoxels); + if(measured) + ++coverage[region.passageIndex].rimSamples[ + region.side][a.lod]; + } + continue; + } + const auto& along=passageAlong[region.passageIndex]; + if(passage.controlPoints.size()<2 + ||along.size()!=passage.controlPoints.size())continue; + bool inEndRun=false; + for(std::size_t segment=1; + segment20.0)break; + if(region.side==1 + &&passageLengths[region.passageIndex] + -along[segment]>20.0)continue; + const auto& start=passage.controlPoints[segment-1]; + const auto& end=passage.controlPoints[segment]; + const double ax=qToMetres(start.positionSubvoxels[0]); + const double ay=qToMetres(start.positionSubvoxels[1]); + const double az=qToMetres(start.positionSubvoxels[2]); + const double dx=qToMetres(end.positionSubvoxels[0] + -start.positionSubvoxels[0]); + const double dy=qToMetres(end.positionSubvoxels[1] + -start.positionSubvoxels[1]); + const double dz=qToMetres(end.positionSubvoxels[2] + -start.positionSubvoxels[2]); + const double lengthSquared=dx*dx+dy*dy+dz*dz; + const double t=lengthSquared>1.0e-12?std::clamp( + ((pointXMetres-ax)*dx+(pointYMetres-ay)*dy + +(pointZMetres-az)*dz)/lengthSquared,0.0,1.0):0.0; + const double station=along[segment-1] + +t*(along[segment]-along[segment-1]); + if((region.side==0&&station>20.0) + ||(region.side==1 + &&passageLengths[region.passageIndex]-station>20.0)) + continue; + const double ex=pointXMetres-(ax+t*dx); + const double ey=pointYMetres-(ay+t*dy); + const double ez=pointZMetres-(az+t*dz); + const double radius=qToMetres(start.radiusSubvoxels) + +t*qToMetres(end.radiusSubvoxels + -start.radiusSubvoxels)+0.25; + if(ex*ex+ey*ey+ez*ez<=radius*radius) + {inEndRun=true;break;} + } + if(inEndRun) + ++coverage[region.passageIndex].endRunSamples[ + region.side][a.lod]; + } + } + const bool inject=bInjectPassageSeamMismatch&&!injectionUsed; + const bool unequal=std::bit_cast(densityA) + !=std::bit_cast(densityB); + if(unequal||inject) + { + if(unequal&&!directControlReported) + { + directControlReported=true; + const float directDensity=Shape.densityAt({ + static_cast(x),static_cast(y), + static_cast(z)}); + std::printf("[PassageTileSeamDirectControl] lod=%d axis=%c xyz_voxels=(%lld,%lld,%lld) density_a=%.9g density_b=%.9g direct_density=%.9g\n", + a.lod,"xyz"[axis],static_cast(x), + static_cast(y),static_cast(z), + densityA,densityB,directDensity); + } + auto sampleTag=std::string_view("route"); + if(ResolvedLayout) + { + const double xMetres=pointVoxels[0]*coreMetres; + const double yMetres=pointVoxels[1]*coreMetres; + const double zMetres=pointVoxels[2]*coreMetres; + double nearest=std::numeric_limits::infinity(); + for(const auto& passage:ResolvedLayout->passages) + { + const auto qToMetres=[&](std::int64_t q) + {return static_cast(q)*coreMetres + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;}; + bool inLanding=false; + for(const int side:{0,1}) + { + const auto& chamber=side==0?passage.upperLandingChamber + :passage.lowerLandingChamber; + const auto& mouth=side==0?passage.upperMouthSubvoxels + :passage.lowerMouthSubvoxels; + const std::int64_t radiusQ= + VoxelForgeCore::ResolvedLayoutPassageLandingDiscRadiusSubvoxels( + passage.mouthRadiusSubvoxels, + World.CoreVoxelSizeMicrons); + const std::array center={mouth[0],mouth[1]}; + const bool inside=VoxelForgeCore:: + IsInsideResolvedLayoutPassageLandingDiscV1(center, + radiusQ,xMetres*subvoxelsPerMetre, + yMetres*subvoxelsPerMetre); + const double floor=qToMetres( + chamber.floorCenterSubvoxels[2]); + const double top=floor+qToMetres( + chamber.clearHeightSubvoxels); + const bool insideDisc=inside + &&zMetres>=floor-0.75&&zMetres<=top; + const std::int64_t halfRimWidth=static_cast( + std::llround(0.5*subvoxelsPerMetre)); + const bool onRim=VoxelForgeCore:: + IsInsideResolvedLayoutPassageLandingRimV1(center, + radiusQ,halfRimWidth, + xMetres*subvoxelsPerMetre, + yMetres*subvoxelsPerMetre) + &&zMetres>=floor-0.75&&zMetres<=top+0.5; + if(insideDisc||onRim) + { + sampleTag=onRim + ?"rim":"disc"; + inLanding=true; + break; + } + } + if(inLanding)break; + double alongMetres=0.0; + double passageNearest=std::numeric_limits::infinity(); + double bestAlong=0.0,bestRadius=0.0; + for(std::size_t routeIndex=1; + routeIndex1.0e-12?std::clamp( + ((xMetres-ax)*dx+(yMetres-ay)*dy+(zMetres-az)*dz) + /length2,0.0,1.0):0.0; + const double ex=xMetres-(ax+t*dx); + const double ey=yMetres-(ay+t*dy); + const double ez=zMetres-(az+t*dz); + const double distance=std::sqrt(ex*ex+ey*ey+ez*ez); + const double segmentLength=std::sqrt(dx*dx+dy*dy+dz*dz); + if(distance(densityA)-densityB)); + inject?1.0e-6:std::abs(static_cast(densityA)-densityB)); + std::printf("[PassageTileSeamSample] tag=%.*s injected=%s lod=%d axis=%c keyA=(%d,%d,%d) keyB=(%d,%d,%d) xyz_m=(%.6f,%.6f,%.6f) density_a=%.9g density_b=%.9g abs_delta=%.9g status=FAIL\n", + static_cast(sampleTag.size()),sampleTag.data(), + inject?"true":"false",a.lod,"xyz"[axis], + a.coordinate.x,a.coordinate.y,a.coordinate.z, + b.coordinate.x,b.coordinate.y,b.coordinate.z, + pointVoxels[0]*coreMetres,pointVoxels[1]*coreMetres, + pointVoxels[2]*coreMetres,densityA,densityB, + inject?1.0e-6:std::abs(static_cast(densityA)-densityB)); } } + if(pairSamples==0) + { + ++exemptPairs; + std::printf("[PassageTileSeam] lod=%d axis=%c keyA=(%d,%d,%d) keyB=(%d,%d,%d) shared_samples=0 mismatches=0 max_abs_delta=0 status=FAIL reason=no_common_capture_samples\n", + a.lod,"xyz"[axis],a.coordinate.x,a.coordinate.y,a.coordinate.z, + b.coordinate.x,b.coordinate.y,b.coordinate.z); + continue; + } ++testedPairs; - comparedSamples+=static_cast(Dim)*Dim; + comparedSamples+=pairSamples; + if(a.lod==0){++lod0Pairs;lod0Samples+=pairSamples;} + else {++lod1Pairs;lod1Samples+=pairSamples;} mismatches+=pairMismatches; maximumDelta=std::max(maximumDelta,pairMaximumDelta); - std::printf("[PassageTileSeam] axis=%c keyA=(%d,%d,%d,lod0) keyB=(%d,%d,%d,lod0) shared_samples=%llu mismatches=%llu max_abs_delta=%.9g status=%s\n", - "xyz"[axis],a.coordinate.x,a.coordinate.y,a.coordinate.z, + std::printf("[PassageTileSeam] lod=%d axis=%c keyA=(%d,%d,%d) keyB=(%d,%d,%d) shared_samples=%llu mismatches=%llu max_abs_delta=%.9g status=%s\n", + a.lod,"xyz"[axis],a.coordinate.x,a.coordinate.y,a.coordinate.z, b.coordinate.x,b.coordinate.y,b.coordinate.z, - static_cast(static_cast(Dim)*Dim), + static_cast(pairSamples), static_cast(pairMismatches),pairMaximumDelta, pairMismatches==0?"PASS":"FAIL"); - if(pairMismatches)break; - if(testedPairs>=4)break; } - passed=testedPairs>0&&mismatches==0; - std::printf("[PassageTileSeamAudit] candidates=%zu examined=%zu pairs=%zu shared_samples=%llu mismatches=%llu max_abs_delta=%.9g status=%s\n", - candidates.size(),examinedPairs,testedPairs, - static_cast(comparedSamples), - static_cast(mismatches),maximumDelta, - passed?"PASS":"FAIL"); - if(!passed) + std::uint64_t landingDiscSamples[2]{},landingRimSamples[2]{}, + endRunSamples[2]{}; + for(std::size_t passageIndex=0;passageIndex0; + endCovered=endCovered + &&coverage[passageIndex].endRunSamples[side][lod]>0; + landingDiscSamples[lod]+=coverage[passageIndex].discSamples[side][lod]; + landingRimSamples[lod]+=coverage[passageIndex].rimSamples[side][lod]; + endRunSamples[lod]+=coverage[passageIndex].endRunSamples[side][lod]; + } + coveredDiscs+=discCovered?1u:0u; + coveredEndRuns+=endCovered?1u:0u; + const auto& passage=ResolvedLayout->passages[passageIndex]; + std::printf("[PassageTileSeamCoverage] id=%s side=%s lod0_disc_samples=%llu lod1_disc_samples=%llu lod0_rim_samples=%llu lod1_rim_samples=%llu lod0_end_run_samples=%llu lod1_end_run_samples=%llu disc_status=%s end_run_status=%s\n", + passage.stableId.c_str(),side==0?"upper":"lower", + static_cast(coverage[passageIndex].discSamples[side][0]), + static_cast(coverage[passageIndex].discSamples[side][1]), + static_cast(coverage[passageIndex].rimSamples[side][0]), + static_cast(coverage[passageIndex].rimSamples[side][1]), + static_cast(coverage[passageIndex].endRunSamples[side][0]), + static_cast(coverage[passageIndex].endRunSamples[side][1]), + discCovered?"PASS":"FAIL",endCovered?"PASS":"FAIL"); } + const bool coveragePassed=testedPairs>0&&lod0Samples>0&&lod1Samples>0 + &&requiredDiscs>0&&coveredDiscs==requiredDiscs + &&requiredEndRuns>0&&coveredEndRuns==requiredEndRuns; + const bool injectedControlPassed=bInjectPassageSeamMismatch + ?(injectionUsed&&mismatches>0):true; + const bool passed=coveragePassed&&exemptPairs==0&&mismatches==0 + &&injectedControlPassed; + std::printf("[PassageTileSeamAudit] candidates=%zu examined=%zu pairs=%zu lod0_pairs=%llu lod0_samples=%llu lod1_pairs=%llu lod1_samples=%llu landing_discs=%llu/%llu end_runs=%llu/%llu lod0_disc_samples=%llu lod1_disc_samples=%llu lod0_rim_samples=%llu lod1_rim_samples=%llu lod0_end_run_samples=%llu lod1_end_run_samples=%llu exempt_pairs=%llu mismatches=%llu max_abs_delta=%.9g injected=%s status=%s\n", + candidates.size(),examinedPairs,testedPairs, + static_cast(lod0Pairs), + static_cast(lod0Samples), + static_cast(lod1Pairs), + static_cast(lod1Samples), + static_cast(coveredDiscs), + static_cast(requiredDiscs), + static_cast(coveredEndRuns), + static_cast(requiredEndRuns), + static_cast(landingDiscSamples[0]), + static_cast(landingDiscSamples[1]), + static_cast(landingRimSamples[0]), + static_cast(landingRimSamples[1]), + static_cast(endRunSamples[0]), + static_cast(endRunSamples[1]), + static_cast(exemptPairs), + static_cast(mismatches),maximumDelta, + bInjectPassageSeamMismatch?(injectionUsed?"DETECTED":"MISSED"):"off", + passed?"PASS":"FAIL"); + bAuditFailureDetected=bAuditFailureDetected||!passed; } FStandaloneDesiredTileSet EarlyDesired=Desired; @@ -8324,10 +9000,11 @@ namespace StandaloneStreamBenchmark static_cast(ExactDetailHash), static_cast(ExactCollisionHash)); } - return NeighborhoodFinished==static_cast(Neighborhood.size()) + const bool bPipelinePassed=NeighborhoodFinished==static_cast(Neighborhood.size()) &&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing &&(!bProfileColumnOverlap||bColumnOverlapCapturePassing) &&(!bExportRequested||bSceneExportPassing)&&bScreenSpaceGatePassing - &&(!bAuditUniformBlockProofs||BlockProofViolations==0)?0:6; + &&(!bAuditUniformBlockProofs||BlockProofViolations==0); + return bPipelinePassed?(bAuditFailureDetected?10:0):6; } } // namespace StandaloneStreamBenchmark diff --git a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index c9c4c2e..4ca229f 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -202,7 +202,9 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, std::vector* OutDebugDensitySamples, FVoxelMarchingCubesStageTimings* OutStageTimings, FVoxelUniformBlockProofAuditStats* OutBlockProofAudit, - bool bForceFullDensityCapture) + bool bForceFullDensityCapture, + FIntVector* OutDebugDensityOriginVoxels, + FIntVector* OutDebugDensityDimensions) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh); FStandaloneShapeWorld::FScopedPassageDensityQueryCaller densityQueryCaller( @@ -215,6 +217,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, if (OutStageTimings) { *OutStageTimings = FVoxelMarchingCubesStageTimings{}; } if (OutBlockProofAudit) { *OutBlockProofAudit = FVoxelUniformBlockProofAuditStats{}; } if (OutDebugDensitySamples) { OutDebugDensitySamples->clear(); } + if (OutDebugDensityOriginVoxels) *OutDebugDensityOriginVoxels=FIntVector::ZeroValue; + if (OutDebugDensityDimensions) *OutDebugDensityDimensions=FIntVector::ZeroValue; if (!Generator) return MeshData; const bool bEmitUV = !bStandaloneSkipUvAndColors && !bStandaloneSkipUV; const bool bEmitColors = !bStandaloneSkipUvAndColors && !bStandaloneSkipColors; @@ -640,6 +644,14 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, } const int32 GzLo = CzLo - 1; // coins CzLo..CzHi+1, gradients ±1 const int32 GzHi = CzHi + 2; // (== -1..GridDim sans bande) + if(OutDebugDensitySamples) + { + if(OutDebugDensityOriginVoxels) + *OutDebugDensityOriginVoxels=OriginVoxels + +FIntVector(-Step,-Step,GzLo*Step); + if(OutDebugDensityDimensions) + *OutDebugDensityDimensions=FIntVector(MDim,MDim,GzHi-GzLo+1); + } MesherSetupTimer.End(); diff --git a/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h b/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h index e5ccf62..c5a5fbe 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h +++ b/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h @@ -171,7 +171,9 @@ public: std::vector* OutDebugDensitySamples = nullptr, FVoxelMarchingCubesStageTimings* OutStageTimings = nullptr, FVoxelUniformBlockProofAuditStats* OutBlockProofAudit = nullptr, - bool bForceFullDensityCapture = false); + bool bForceFullDensityCapture = false, + FIntVector* OutDebugDensityOriginVoxels = nullptr, + FIntVector* OutDebugDensityDimensions = nullptr); //========================================================================= // SERVICES (injectés par AVoxelWorld)