diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 04633c4..0adffd3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2076,6 +2076,40 @@ transition one micrometre wider than declared and a band one micrometre below th [`Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md) for audit measurements, hashes, timings, captures and run history. +### 8.47 Ground-bound SurfaceWorld departure routes — World Assembler M3 Linux L33 + +An intra-strate SurfaceWorld route follows the resolved terrain ground profile. Its route-point Z +is the ground height plus the player's half-height; the route does not descend to the strate's +bedrock boundary. Bedrock belongs to the separate inter-strate passage, which begins at the +SurfaceWorld departure mouth on the ground and tunnels outward and down through native rock and +the boundary into the next strate. + +The old fixed candidate envelope used a 64 m chord and 16 m centre spacing. On the L33 upper-SW +fixture, replaying that envelope sampled ground from −507.767 to −464.857 m; 366/648 candidate paths +exceeded the 15° grade limit. The worst failures clustered near the outer west/northwest terrain +shoulder (up to 79.971°) and the steep approach slopes around the origin. Candidate diagnostics +separate grade failures from endpoints outside the bounded arrival/departure placement disks. +The bounded search now limits its longest chord to the two mouth radii minus a 3 m clearance +margin, checks centres every 8 m, and ranks endpoints near the route anchors. It finds a 21.5 m +route from (−2.75, 0) to (18.75, 0), sampled on ground and accepted by the full capsule/support/ +grade audit. + +The SurfaceWorld departure passage starts at the ground mouth with a 1.5 m portal, then smoothsteps +to a 3.5 m rock tunnel over 9 m. Its ring is sized and oriented outward from the mouth, so the +approach does not cut back under the walk route. The `FloatingIslands → SurfaceWorld → TunnelNetwork` +fixture passes all four passage styles, including both interfaces, capsule clearance, support, +grade, walkability, naturalness, open-air, column ends, long-bridge and route-binding audits. + +`FloorProfileBlend` previously compared each curved floor profile with its straight chord and +applied a 2-voxel limit. That is a curvature measurement, not a seam measurement. It now checks +endpoint height and tangent continuity at 0.01 voxel and 0.001 voxel/m; the 3-voxel step and +0.25 voxel/m tangent-step controls are both detected. SurfaceWorld owner hashes remain identical +to L32 because the route-mouth, expanded column sizing and short-span support changes apply only +when SurfaceWorld is the upper strate. No evaluator or recipe version changed. + +[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md) +records the four-style audits, captures, determinism, owner hashes, costs and retries. + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/CODEMAP.md b/CODEMAP.md index cc24c2d..57a98d6 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1229,6 +1229,20 @@ The Unreal adapter now forwards the authored transition type and chunk width; th were not compiled here. See ARCHITECTURE §8.46 and [`Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md). +### 3.30 SurfaceWorld as an upper strate and floor-profile joins — World Assembler L33 + +| Symbol | Canonical file | Role | +|---|---|---| +| `BuildFiniteStrateRoute` | `Standalone/Source/WorldAssemblyPrototype.h` | Samples route heights from the composed terrain ground; bounds candidate chords by the arrival/departure placement radii and records per-segment grade failures. | +| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Starts an upper-SurfaceWorld departure at its terrain mouth, opens a small portal and smoothsteps to the rock tunnel while orienting the ring outward. | +| `BuildPassageVoidCrossingPlan`, `PassageBridgeSignedDistance` | `Standalone/Core/src/ShapeWorldInternal.h` | Sizes unsupported ranges from the full control-segment window only for upper SurfaceWorld; other upper generators keep the previous bracketing-sample bounds. | +| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Uses floor height rather than bore-centre height for naturalness and records failed-only wall probes for column audits. | +| `FloorProfileBlend` | `Standalone/Core/src/ShapeWorldInternal.h` | Measures endpoint height/tangent continuity; chord deviation is informational, with step and tangent-step negative controls. | + +The owner SurfaceWorld hashes match L32 and evaluator/recipe versions are unchanged. See +ARCHITECTURE §8.47 and +[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index faa30c8..f136d08 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -788,10 +788,13 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t cfg->distanceMaximumSubvoxels):8*qPerMetre; const std::int64_t placementRadius=std::max(1,outer); // Leave room for the capsule and bounded wall relief at the floor/wall seam. - const std::int64_t mouthRadius=std::max(static_cast(std::llround(1.5*qPerMetre)), + const bool upperIsSurfaceWorld=upper.generatorKind=="SurfaceWorld"; + const std::int64_t minimumMouthRadius=static_cast(std::llround( + (upperIsSurfaceWorld?2.0:1.5)*qPerMetre)); + const std::int64_t mouthRadius=std::max(minimumMouthRadius, cfg?cfg->mouthRadiusSubvoxels:0); const std::int64_t midRadius=std::max( - static_cast(std::llround(1.5*qPerMetre)), + static_cast(std::llround((upperIsSurfaceWorld?3.5:1.5)*qPerMetre)), cfg?cfg->midRadiusSubvoxels:mouthRadius); const std::int64_t reachMinimum=cfg?std::max(0,cfg->reachMinimumSubvoxels):8*qPerMetre; const std::int64_t reachMaximum=cfg?std::max(reachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre; @@ -813,6 +816,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t std::array upperMouth{},lowerMouth{}; const bool hasUpperRoute=connection==0&&routeAnchor(upper,true,upperMouth); const bool hasLowerRoute=connection==0&&routeAnchor(lower,false,lowerMouth); + const bool surfaceGroundDeparture=hasUpperRoute&&upperIsSurfaceWorld; const ResolvedLayoutAnchorV2* passageAnchor=nullptr; if(connection==0) for(const auto& anchor:upper.anchors) @@ -870,6 +874,17 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t std::max(0,cfg?cfg->cascadeLedgeSubvoxels:0) *static_cast(cfg?cfg->cascadeSteps:4)), mouthRadius+2*qPerMetre,outerRingRadius); + if(surfaceGroundDeparture) + { + const double departureRadius=std::hypot(static_cast(upperMouth[0]), + static_cast(upperMouth[1])); + const std::int64_t clearRingRadius=static_cast(std::ceil( + departureRadius+mouthRadius+0.5*qPerMetre)); + const std::int64_t maximumRingRadius=std::max(1, + placementRadius-qPerMetre); + ringRadius=std::min(maximumRingRadius, + std::max(ringRadius,clearRingRadius)); + } if(anchoredIslandPassage) ringRadius=std::min(ringRadius,mouthRadius+qPerMetre/2); std::uint32_t phase=static_cast(PassageSeed(seed, @@ -890,6 +905,23 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t {bestAlignment=alignment;phase=candidate;} } } + if(hasUpperRoute&&upperIsSurfaceWorld) + { + const auto& directionAtPhase=directions[phase]; + const std::int64_t radialAlignment=upperMouth[0]*directionAtPhase[0] + +upperMouth[1]*directionAtPhase[1]; + if(radialAlignment<0) + { + std::int64_t bestOutwardAlignment=std::numeric_limits::min(); + for(std::uint32_t candidate=0;candidatebestOutwardAlignment) + {bestOutwardAlignment=alignment;phase=candidate;} + } + } + } bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex, connection,0x44495245u)&1u)!=0; if(anchoredSpiral&&passageAnchor&&hasUpperTangent) @@ -1159,10 +1191,23 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t if(hasAnchorHelix&&helixHorizontal==0) {error=passage.stableId+" anchor spiral has no horizontal reach";return false;} passage.controlPoints.reserve(xy.size()); + const std::int64_t surfacePortalRadius=surfaceGroundDeparture + ?std::min(mouthRadius,static_cast(std::llround(1.5*qPerMetre))) + :mouthRadius; + const std::uint64_t surfaceMouthBlendLength=static_cast( + std::max(8*qPerMetre,2*mouthRadius)); for(std::size_t i=0;i=xy.size()-1)?mouthRadius:midRadius; + if(surfaceGroundDeparture&&i(cumulative[i]) + /surfaceMouthBlendLength,0.0,1.0); + const double smooth=t*t*(3.0-2.0*t); + radius=surfacePortalRadius+static_cast(std::llround( + (midRadius-surfacePortalRadius)*smooth)); + } std::int64_t playerZ=0; if(i>lowerMouthIndex) playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2]; @@ -1183,9 +1228,30 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t point.positionSubvoxels={xy[i][0],xy[i][1],playerZ+radius-playerCenterAboveFloorQ}; point.radiusSubvoxels=radius;passage.controlPoints.push_back(point); } - passage.controlPoints.front().positionSubvoxels[2]=upperMouth[2]+mouthRadius-playerCenterAboveFloorQ; - passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]= - lowerMouth[2]+mouthRadius-playerCenterAboveFloorQ; + if(surfaceGroundDeparture) + std::printf("[ResolvedSurfaceWorldDepartureMouth] passage=%s ground_floor_z_m=%.3f portal_radius_m=%.3f tunnel_radius_m=%.3f blend_length_m=%.3f departure_xy_m=(%.3f,%.3f) first_tunnel_xy_m=(%.3f,%.3f) first_tunnel_floor_z_m=%.3f first_tunnel_radius_m=%.3f\n", + passage.stableId.c_str(),(upperMouth[2]-static_cast(std::llround(0.88*qPerMetre)))/static_cast(qPerMetre), + surfacePortalRadius/static_cast(qPerMetre), + midRadius/static_cast(qPerMetre), + surfaceMouthBlendLength/static_cast(qPerMetre), + upperMouth[0]/static_cast(qPerMetre), + upperMouth[1]/static_cast(qPerMetre), + passage.controlPoints.size()>1?passage.controlPoints[1].positionSubvoxels[0] + /static_cast(qPerMetre):0.0, + passage.controlPoints.size()>1?passage.controlPoints[1].positionSubvoxels[1] + /static_cast(qPerMetre):0.0, + passage.controlPoints.size()>1? + (passage.controlPoints[1].positionSubvoxels[2] + -passage.controlPoints[1].radiusSubvoxels+playerCenterAboveFloorQ + -static_cast(std::llround(0.88*qPerMetre))) + /static_cast(qPerMetre):0.0, + passage.controlPoints.size()>1?passage.controlPoints[1].radiusSubvoxels + /static_cast(qPerMetre):0.0); + passage.controlPoints.front().positionSubvoxels[2]=upperMouth[2] + +passage.controlPoints.front().radiusSubvoxels-playerCenterAboveFloorQ; + passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]=lowerMouth[2] + +passage.controlPoints[lowerMouthIndex].radiusSubvoxels + -playerCenterAboveFloorQ; layout.passages.push_back(std::move(passage)); } } diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 74b2771..8db109d 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -1309,6 +1309,10 @@ public: double MaxUnmarkedFloorDirectionSlopeDelta = 0.0; double MaxMarkedFloorSlopeDelta = 0.0; double MaxFloorCurveDeviation = 0.0; + double MaxFloorProfileEndpointValueError = 0.0; + double MaxFloorProfileEndpointSlopeError = 0.0; + bool FloorProfileEndpointStepControlDetected = false; + bool FloorProfileEndpointSlopeControlDetected = false; double LocalPrimitiveSeconds = 0.0; double LocalLineBinSeconds = 0.0; double CandidateMergeSeconds = 0.0; @@ -1380,6 +1384,8 @@ public: double FloorCurveStartBlend = 0.0; double FloorCurveEndBlend = 0.0; double FloorCurveMaxDeviation = 0.0; + double FloorCurveEndpointValueError = 0.0; + double FloorCurveEndpointSlopeError = 0.0; }; struct FInterval { int32 Lo = 0, Hi = -1; }; @@ -1517,6 +1523,7 @@ public: if(NodeCount>160000000ull) {Error="shape interval region exceeds the 160-million-node safety bound";return false;} Stats={};Stats.SignNodes=NodeCount; + AuditFloorProfileBlendNegativeControls(); auto Stage=Clock::now(); BuildLayerBounds(); @@ -1622,13 +1629,19 @@ public: static_cast(Stats.RoomFloorJoinCount)); const double SmoothUnionGrowthBound=JoinBlendRadius/6.0; const bool bFloorSlopePass=Stats.MaxUnmarkedFloorSlopeDelta<=0.75; - const bool bFloorBlendPass=Stats.MaxFloorCurveDeviation<=2.0; + const bool bFloorBlendPass=Stats.MaxFloorProfileEndpointValueError<=0.01 + &&Stats.MaxFloorProfileEndpointSlopeError<=0.001 + &&Stats.FloorProfileEndpointStepControlDetected + &&Stats.FloorProfileEndpointSlopeControlDetected; std::printf("[FloorSlopeJoinAudit] raw_unmarked_max_delta=%.5f vox generated_tangent_delta=%.5f vox bend_direction_slope_change=%.5f vox marked_max_delta=%.5f vox marked_joins=%llu threshold=0.750 status=%s\n", Stats.MaxRawUnmarkedFloorSlopeDelta,Stats.MaxUnmarkedFloorSlopeDelta, Stats.MaxUnmarkedFloorDirectionSlopeDelta,Stats.MaxMarkedFloorSlopeDelta, static_cast(Stats.MarkedFloorSlopeJoins),bFloorSlopePass?"PASS":"FAIL"); - std::printf("[FloorProfileBlend] c1_segments=%llu max_curve_deviation=%.3f vox threshold=2.000 status=%s\n", + std::printf("[FloorProfileBlend] c1_segments=%llu max_chord_deviation=%.3f vox informational endpoint_value_residual=%.8f vox endpoint_slope_residual=%.8f vox_per_m thresholds=(0.010,0.001) negative_controls=%s/%s status=%s\n", static_cast(Stats.CurvedFloorSegments),Stats.MaxFloorCurveDeviation, + Stats.MaxFloorProfileEndpointValueError,Stats.MaxFloorProfileEndpointSlopeError, + Stats.FloorProfileEndpointStepControlDetected?"step_detected":"step_missed", + Stats.FloorProfileEndpointSlopeControlDetected?"slope_detected":"slope_missed", bFloorBlendPass?"PASS":"FAIL"); std::printf("[TunnelRadiusAudit] shared_joins=%llu legacy_fixed_segment_max_step=%.4f vox smoothed_profile_max_step=%.4f vox tapered_endpoint_max_delta=%.6f vox radius_slope_join_delta=%.8f floor_endpoint_max_delta=%.6f vox floor_slope_join_delta=%.5f unmarked_floor_slope_delta=%.5f marked_floor_slope_delta=%.5f marked_slope_joins=%llu smooth_union_growth_bound=%.4f vox thresholds=(0.005,0.000001,0.005,0.25) status=%s\n", static_cast(Stats.CapsuleJoinCount),Stats.LegacySegmentRadiusStep, @@ -2751,8 +2764,23 @@ private: auto& flags= crossing.needsColumn ?PassageColumnSegmentFlags[crossing.passageIndex] :PassageShortSpanSegmentFlags[crossing.passageIndex]; - const std::size_t first=std::min(crossing.firstSegment,flags.size()); - const std::size_t last=std::min(crossing.lastSegment,flags.empty()?0:flags.size()-1); + bool surfaceWorldUpper=false; + if(crossing.passageIndexpassages.size()) + { + const std::int32_t strate=ResolvedLayout->passages[ + crossing.passageIndex].upperStrateIndex; + const auto upper=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[strate](const auto& value) + {return value.strateIndex==strate;}); + surfaceWorldUpper=upper!=ResolvedLayout->strates.end() + &&upper->generatorKind=="SurfaceWorld"; + } + const std::size_t shortSpanPadding=!crossing.needsColumn + &&surfaceWorldUpper&&!flags.empty()?2:0; + const std::size_t first=std::min(crossing.firstSegment>shortSpanPadding + ?crossing.firstSegment-shortSpanPadding:0,flags.size()); + const std::size_t last=std::min(crossing.lastSegment+shortSpanPadding, + flags.empty()?0:flags.size()-1); if(!flags.empty()&&first<=last) std::fill(flags.begin()+first,flags.begin()+last+1,1); } @@ -3011,6 +3039,11 @@ private: ResolvedLayout->coreVoxelSizeMicrometres); const auto& a=passage.controlPoints[SegmentIndex]; const auto& b=passage.controlPoints[SegmentIndex+1]; + const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&passage](const auto& value) + {return value.strateIndex==passage.upperStrateIndex;}); + const bool surfaceWorldUpper=upperStratum!=ResolvedLayout->strates.end() + &&upperStratum->generatorKind=="SurfaceWorld"; const double ax=static_cast(a.positionSubvoxels[0]); const double ay=static_cast(a.positionSubvoxels[1]); const double dx=static_cast(b.positionSubvoxels[0])-ax; @@ -3031,7 +3064,8 @@ private: +PassageBridgeDeckOffsetMetres(PassageIndex,SegmentIndex)*qPerMetre; const double relief=std::clamp(static_cast(VF_FieldSurfaceRelief(X,Y,Z, static_cast(Owner?Owner->Seed:0))),-1.0,1.0); - const double halfWidth=(3.30+0.20*relief)*qPerMetre; + const double halfWidth=(surfaceWorldUpper?3.65+0.10*relief:3.30+0.20*relief) + *qPerMetre; const double thickness=(4.0+1.0*relief)*qPerMetre; const double horizontalDistance=DeterministicSqrt((qx-centerX)*(qx-centerX) +(qy-centerY)*(qy-centerY)); @@ -3214,6 +3248,11 @@ private: ++passageIndex) { const auto& passage=ResolvedLayout->passages[passageIndex]; + const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&passage](const auto& value) + {return value.strateIndex==passage.upperStrateIndex;}); + const bool surfaceWorldUpper=upperStratum!=ResolvedLayout->strates.end() + &&upperStratum->generatorKind=="SurfaceWorld"; if(passage.controlPoints.size()<2)continue; std::vector samples; samples.reserve(passage.controlPoints.size()*2); @@ -3303,14 +3342,32 @@ private: } double maximumOffsetSubvoxels=0.0; double maximumBoreSubvoxels=0.0; - for(std::size_t index=startSample;index<=endSample;++index) + double minimumCrossingZ=std::min(crossing.startPoint[2],crossing.endPoint[2]); + double maximumCrossingZ=std::max(crossing.startPoint[2],crossing.endPoint[2]); + for(std::size_t sampleIndex=0;sampleIndexcrossing.lastSegment)continue; + } + else if(sampleIndexendSample)continue; + const auto& point=sample.point; const double dx=point[0]-crossing.centerXSubvoxels; const double dy=point[1]-crossing.centerYSubvoxels; maximumOffsetSubvoxels=std::max(maximumOffsetSubvoxels, DeterministicSqrt(dx*dx+dy*dy)); maximumBoreSubvoxels=std::max(maximumBoreSubvoxels,point[3]); + if(surfaceWorldUpper) + { + minimumCrossingZ=std::min(minimumCrossingZ,point[2]); + maximumCrossingZ=std::max(maximumCrossingZ,point[2]); + } } const double maximumOffsetMetres=maximumOffsetSubvoxels/qPerMetre; const double maximumBoreMetres=maximumBoreSubvoxels/qPerMetre; @@ -3324,12 +3381,10 @@ private: nominalRadiusMetres*qPerMetre)); crossing.maximumRadiusSubvoxels=crossing.minimumRadiusSubvoxels +static_cast(std::ceil(11.25*qPerMetre)); - const double startZ=crossing.startPoint[2]; - const double endZ=crossing.endPoint[2]; crossing.topZSubvoxels=static_cast(std::llround( - std::max(startZ,endZ)+5.0*qPerMetre)); + maximumCrossingZ+5.0*qPerMetre)); crossing.bottomZSubvoxels=static_cast(std::llround( - std::min(startZ,endZ)-6.0*qPerMetre)); + minimumCrossingZ-6.0*qPerMetre)); } PassageVoidCrossings.push_back(crossing); if(crossing.needsColumn) @@ -3350,7 +3405,7 @@ private: crossing.connectedAtStart&&crossing.connectedAtEnd?"PLANNED":"UNATTACHED"); } else - std::printf("[PassageShortGroundedSpanPlan] id=%s segments=%zu..%zu span_m=%.3f threshold_m=%.3f connected=%d/%d width_m=6.000 thickness_m=3.000..5.000 status=%s\n", + std::printf("[PassageShortGroundedSpanPlan] id=%s segments=%zu..%zu span_m=%.3f threshold_m=%.3f connected=%d/%d width_m=7.300 thickness_m=3.000..5.000 status=%s\n", passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment, gapLength,shortSpanMaximumMetres,static_cast(crossing.connectedAtStart), static_cast(crossing.connectedAtEnd), @@ -4193,7 +4248,10 @@ public: static_cast(S.NearZero),static_cast(S.AxisLocked)); } const bool bFloorProfilePass=Stats.MaxUnmarkedFloorSlopeDelta<=0.75 - &&Stats.MaxFloorCurveDeviation<=2.0; + &&Stats.MaxFloorProfileEndpointValueError<=0.01 + &&Stats.MaxFloorProfileEndpointSlopeError<=0.001 + &&Stats.FloorProfileEndpointStepControlDetected + &&Stats.FloorProfileEndpointSlopeControlDetected; return bDetailPass&&bGridPass&&bThinSheetPass&&bFloorProfilePass; } @@ -4949,6 +5007,44 @@ private: return Result; } + static double EvaluateFloorProfileSlope(double A,double B,double TangentA,double TangentB, + double Length,double StartBlend,double EndBlend,double S) + { + if(S<0.0)return TangentA; + if(S>Length)return TangentB; + const double Secant=(B-A)/Length; + double Result=Secant; + const auto CorrectionSlope=[](double DeltaSlope,double T) + {return DeltaSlope*(1.0-4.0*T+3.0*T*T);}; + if(SValueThreshold; + Stats.FloorProfileEndpointSlopeControlDetected= + std::abs((startSlope+SlopeStep)-TangentA)>SlopeThreshold; + std::printf("[FloorProfileBlendNegativeControls] endpoint_step=%.3f_vox tangent_step=%.3f_vox_per_m detected=%s/%s status=%s\n", + Step,SlopeStep, + Stats.FloorProfileEndpointStepControlDetected?"true":"false", + Stats.FloorProfileEndpointSlopeControlDetected?"true":"false", + Stats.FloorProfileEndpointStepControlDetected + &&Stats.FloorProfileEndpointSlopeControlDetected?"PASS":"FAIL"); + } + static double FloorSurfaceRelief(double X,double Y,uint32 Seed) { // A shared XY field keeps independently authored floor clips from ending on @@ -5014,6 +5110,16 @@ private: const double Linear=std::lerp(FloorA,FloorB,T); P.FloorCurveMaxDeviation=std::max(P.FloorCurveMaxDeviation,std::abs(Curve-Linear)); } + P.FloorCurveEndpointValueError=std::max( + std::abs(EvaluateFloorProfile(FloorA,FloorB,TangentA,TangentB,Length, + P.FloorCurveStartBlend,P.FloorCurveEndBlend,0.0)-FloorA), + std::abs(EvaluateFloorProfile(FloorA,FloorB,TangentA,TangentB,Length, + P.FloorCurveStartBlend,P.FloorCurveEndBlend,Length)-FloorB)); + P.FloorCurveEndpointSlopeError=std::max( + std::abs(EvaluateFloorProfileSlope(FloorA,FloorB,TangentA,TangentB,Length, + P.FloorCurveStartBlend,P.FloorCurveEndBlend,0.0)-TangentA), + std::abs(EvaluateFloorProfileSlope(FloorA,FloorB,TangentA,TangentB,Length, + P.FloorCurveStartBlend,P.FloorCurveEndBlend,Length)-TangentB)); } double FloorAt(const FPrimitive& P,double X,double Y) const @@ -5224,6 +5330,12 @@ private: ++Stats.CurvedFloorSegments; Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation, Primitives.back().FloorCurveMaxDeviation); + Stats.MaxFloorProfileEndpointValueError=std::max( + Stats.MaxFloorProfileEndpointValueError, + Primitives.back().FloorCurveEndpointValueError); + Stats.MaxFloorProfileEndpointSlopeError=std::max( + Stats.MaxFloorProfileEndpointSlopeError, + Primitives.back().FloorCurveEndpointSlopeError); } FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],R0,Points[I+1],R1); SetCapsuleRadiusProfile(Primitives.back(),Points[I],R0,RadiusSlope0, @@ -5348,6 +5460,12 @@ private: ++Stats.CurvedFloorSegments; Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation, Primitives.back().FloorCurveMaxDeviation); + Stats.MaxFloorProfileEndpointValueError=std::max( + Stats.MaxFloorProfileEndpointValueError, + Primitives.back().FloorCurveEndpointValueError); + Stats.MaxFloorProfileEndpointSlopeError=std::max( + Stats.MaxFloorProfileEndpointSlopeError, + Primitives.back().FloorCurveEndpointSlopeError); FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],RadiusA,Points[I+1],RadiusB); SetCapsuleRadiusProfile(Primitives.back(),Points[I],RadiusA,RadiusSlopeA, Points[I+1],RadiusB,RadiusSlopeB); diff --git a/Standalone/DESIGN_WORLD_ASSEMBLER.md b/Standalone/DESIGN_WORLD_ASSEMBLER.md index 01001ee..70bea0e 100644 --- a/Standalone/DESIGN_WORLD_ASSEMBLER.md +++ b/Standalone/DESIGN_WORLD_ASSEMBLER.md @@ -83,6 +83,12 @@ Costs below are design estimates, not timings. Low means bounded analytic featur A passage planner must produce a traversable route or pair a non-walkable visual passage with another guaranteed route. A narrow crack or drop cannot be assumed walkable because its centerline is air. +For a SurfaceWorld strate, the supported arrival-to-departure route follows the sampled terrain +ground and stays within the mouth placement bounds. The inter-strate departure passage owns the +descent from that ground mouth through rock and bedrock. Treating the bottom of a tall SurfaceWorld +strate as the route's departure height would ask the walk planner to cross open terrain air; it is +the wrong endpoint for the intra-strate route. + | Passage type | Planner output | Bound and likely cost | |---|---|---| | SlopedTunnel | Straight-style path with switchbacks as needed, capsule clearance, supported floor, maximum grade and landing geometry. | Swept capsule bounds plus floor envelope; low–medium. | diff --git a/Standalone/Research/WorldAssemblerM3/Linux33/BuildUpperSurfaceFixtureL33.py b/Standalone/Research/WorldAssemblerM3/Linux33/BuildUpperSurfaceFixtureL33.py new file mode 100644 index 0000000..f829d1e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux33/BuildUpperSurfaceFixtureL33.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Build owner-like FloatingIslands -> SurfaceWorld -> TunnelNetwork fixtures.""" +import copy +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +SOURCE = ROOT / "Standalone/data/owner_world.json" +OUT = ROOT / "Saved/Linux/l33/fixtures" + + +def make_fixture(style: int) -> Path: + world = json.loads(SOURCE.read_text(encoding="utf-8")) + source_tunnel, source_surface = world["strates"] + + floating = copy.deepcopy(source_tunnel) + floating.update(index=0, top_chunk_z=0, bottom_chunk_z=-15, height_chunks=16) + floating["asset"]["StrateName"] = "Linux33 Upper FloatingIslands" + floating["asset"]["StrateHeightInChunks"] = 16 + floating["asset"]["GeneratorType"] = 6 + + surface = copy.deepcopy(source_surface) + surface.update(index=1, top_chunk_z=-16, bottom_chunk_z=-79, height_chunks=64) + surface["asset"]["StrateName"] = "Linux33 Middle SurfaceWorld" + surface["asset"]["StrateHeightInChunks"] = 64 + surface["asset"]["PassageConfig"]["Style"] = style + surface["passage_config"]["Style"] = style + + tunnel = copy.deepcopy(source_tunnel) + tunnel.update(index=2, top_chunk_z=-80, bottom_chunk_z=-143, height_chunks=64) + tunnel["asset"]["StrateName"] = "Linux33 Lower TunnelNetwork" + tunnel["asset"]["StrateHeightInChunks"] = 64 + + world["strates"] = [floating, surface, tunnel] + world["total_strates"] = 3 + world["settings_asset"]["TotalStrates"] = 3 + world["settings_asset"]["StratePool"] = [ + "/Standalone/Linux33/UpperFloatingIslands", + "/Standalone/Linux33/MiddleSurfaceWorld", + "/Standalone/Linux33/LowerTunnelNetwork", + ] + world["passages"] = [] # The L23 legacy coordinates are not part of this resolved stack. + world["source"] = "VoxelForge Linux33 owner-like SurfaceWorld-upper fixture" + + OUT.mkdir(parents=True, exist_ok=True) + path = OUT / f"sw-upper-tunnel-network-style{style}.json" + path.write_text(json.dumps(world, separators=(",", ":")), encoding="utf-8") + return path + + +if __name__ == "__main__": + for style in range(4): + path = make_fixture(style) + print(f"style={style} fixture={path}") diff --git a/Standalone/Research/WorldAssemblerM3/Linux33/CaptureUpperSurfaceL33.py b/Standalone/Research/WorldAssemblerM3/Linux33/CaptureUpperSurfaceL33.py new file mode 100644 index 0000000..e7b6610 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux33/CaptureUpperSurfaceL33.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Export and render two bounded SurfaceWorld departure views.""" +import os +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l33" +FIXTURE = OUT / "fixtures/sw-upper-tunnel-network-style0.json" +HEADLESS = OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final" +CAPTURE = OUT / "bin/VoxelForgeOffscreenCapture-clang-l33-final" +PNG_DIR = OUT / "capture-png" +DEST = Path(__file__).resolve().parent / "captures" +PNG_DIR.mkdir(parents=True, exist_ok=True) +DEST.mkdir(parents=True, exist_ok=True) +env = os.environ.copy() +env["LP_NUM_THREADS"] = "12" +env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + +views = ( + { + "name": "sw-departure-mouth", + "export_camera": "18.75,-12,-499,0,-22", + "render_camera": "18.75,0,-490", + "yaw": "0", + "pitch": "-90", + "fov": "38", + "height_center": "-503.166", + "height_range": "4", + "light": "0,-1,0.2", + "headlamp": "off", + "clip": None, + }, + { + "name": "sw-departure-rock-entry", + "export_camera": "26,-16,-499,64,-16", + "render_camera": "14,-4,-500", + "yaw": "78", + "pitch": "-16", + "fov": "32", + "height_center": "-503.166", + "height_range": "4", + "light": "0,-1,0.2", + "headlamp": "off", + "clip": "1,0,0,-18.75", + }, +) + + +def run(name, command): + logfile = OUT / f"capture-{name}.log" + wrapped = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1800s", *command, + ] + started = time.monotonic() + with logfile.open("w", encoding="utf-8") as stream: + result = subprocess.run(wrapped, cwd=ROOT, env=env, stdout=stream, + stderr=subprocess.STDOUT, timeout=1800, check=False) + print(f"[L33CaptureRun] step={name} elapsed={time.monotonic()-started:.1f}s " + f"exit={result.returncode} log={logfile}", flush=True) + if result.returncode != 0: + raise SystemExit(f"{name} failed; see {logfile}") + + +for view in views: + name = view["name"] + mesh = OUT / f"{name}.vfmesh" + png = PNG_DIR / f"{name}.png" + jpg = DEST / f"{name}.jpg" + run(f"{name}-export", [ + str(HEADLESS), str(FIXTURE), "--workers=12", "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2", + "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0", + "--recipe=floating-islands", + f"--camera={view['export_camera']}", f"--mesh-export={mesh}", + ]) + command = [ + str(CAPTURE), str(mesh), str(png), "--size=1600x900", f"--fov-deg={view['fov']}", + f"--camera-m={view['render_camera']}", f"--yaw-deg={view['yaw']}", + f"--pitch-deg={view['pitch']}", "--shade=readable", + f"--headlamp={view['headlamp']}", f"--light-direction={view['light']}", + "--height-tint=on", f"--height-center-m={view['height_center']}", + f"--height-range-m={view['height_range']}", "--tile-edges=off", + ] + if view["clip"]: + command.append(f"--clip-plane={view['clip']}") + run(f"{name}-render", command) + run(f"{name}-jpeg", [ + "ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y", + "-i", str(png), "-frames:v", "1", "-q:v", "4", str(jpg), + ]) + size = jpg.stat().st_size if jpg.exists() else 0 + ok = 0 < size <= 300_000 + print(f"[L33CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} " + f"status={'PASS' if ok else 'FAIL'}", flush=True) + if not ok: + raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB") diff --git a/Standalone/Research/WorldAssemblerM3/Linux33/CheckOwnerHashesL33.py b/Standalone/Research/WorldAssemblerM3/Linux33/CheckOwnerHashesL33.py new file mode 100644 index 0000000..47a3070 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux33/CheckOwnerHashesL33.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Check representative owner hashes against L32 before the full matrix.""" +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l33" +L32 = ROOT / "Saved/Linux/l32" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-gcc-l33-final", +} +HASH_PATTERN = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +failed = False + +for compiler, binary in BINARIES.items(): + for recipe in ("surface-world", "floating-islands"): + name = f"owner-hash-preflight-{compiler}-{recipe}-w12" + logfile = OUT / f"{name}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + str(binary), str(ROOT / "Standalone/data/owner_world.json"), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8", + "--far-horizon-lod8-step=0", "--workers=12", f"--recipe={recipe}", + ] + started = time.monotonic() + try: + with logfile.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + timeout=900, check=False) + except subprocess.TimeoutExpired: + result = None + text = logfile.read_text(encoding="utf-8", errors="replace") + hashes = HASH_PATTERN.findall(text) + baseline_path = L32 / f"l25-settled-final-{compiler}-{recipe}-w12.log" + baseline = HASH_PATTERN.findall(baseline_path.read_text( + encoding="utf-8", errors="replace")) if baseline_path.exists() else [] + stamps = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + text, re.M) + same = len(hashes) == 12 and hashes == baseline + ok = (result is not None and result.returncode == 0 and same + and stamps == [(recipe, recipe, "PASS")]) + failed |= not ok + print(f"{name} elapsed={time.monotonic()-started:.1f}s hashes={len(hashes)}/12 " + f"same_as_l32={'yes' if same else 'no'} " + f"{'PASS' if ok else 'FAIL'}", flush=True) + +raise SystemExit(1 if failed else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md new file mode 100644 index 0000000..0d75ce4 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md @@ -0,0 +1,58 @@ +# Linux33 report + +## Upper SurfaceWorld route + +The old fixed candidate set used 64 m chords, 2 m waypoint spacing and 16 m centre spacing. On the refreshed upper-stack fixture, the legacy replay measured 366/648 candidates above the 15° grade limit; its worst segment was 79.971° at XY (−84,−48) to (−82,−48). These were long paths over steep terrain relief near the outer search area. L23’s earlier fixture reported 648/648 grade rejects; the refreshed fixture gives a different count. The route Z samples are terrain-ground heights (about −503 m at the chosen mouth), not a target at the bottom of the 499 m strate. There was no unit or bedrock-reference error. + +The planner now caps chords at 58.5 m for the 24 m arrival and 37.5 m departure radii, searches six lengths and places route centres on an 8 m grid, with a proximity cost favouring the origin. It found a 21.5 m walk from (−2.75,0) to departure (18.75,0), average grade about 4.2°. The SW passage starts at the ground floor (−503.166 m), with a 1.5 m portal blending to a 3.5 m tunnel over 9 m, then descends through SW rock. The first tunnel point is at (32.5,0), floor −505.637 m for style 0. + +## Upper-stack fixture audits + +Fixture: FloatingIslands → SurfaceWorld → TunnelNetwork; recipe stamp `floating-islands`; layout identity `resolved-layout-v3-fnv64:fc4e73619037f88c`. Clang and GCC passed every row for every style. + +| Passage style | Floor chord deviation (voxels) | Passage and route audits | Open air | Naturalness | Bridge / columns | Bindings / walkability | +|---:|---:|---|---|---|---|---| +| 0 | 2.081 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass | +| 1 | 10.901 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass | +| 2 | 14.822 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass | +| 3 | 16.374 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass | + +All rows are recipe-stamped and passed on both compilers. Capsule blocking, unsupported samples and grade failures were zero; mouth-content and mouth-radius checks were 2/2. This includes open-air, naturalness, no-long-bridge, rock-column wall/support, route binding and walkability audits. + +## FloorProfileBlend + +The old 2-voxel check measured maximum curve-to-straight-chord deviation, which grows with valid curvature and does not measure a seam. The check now compares endpoint height and tangent residuals. All styles have 0.00000000 voxel height residual and 0.00000000 voxel/m tangent residual (limits 0.010 and 0.001). Negative controls detected a 3-voxel step and a 0.25 voxel/m tangent step. Verdict: check corrected; no floor geometry change was needed. + +## Captures + +Both 1600×900 JPEGs are under 300 KB. The first shows the SW ground mouth; the second is a cutaway through the rock entry. + +Sizes and SHA-256: mouth 26,939 B, `561d3a1e334ea3a3115fb781a1130ec850d8777b74c1be3704c70b865878640a`; rock entry 37,572 B, `f85d150bf44cd2cdae1ee49de023ad9e4c657b8fa7e8d8f8b93abc86586ad6e5`. + +![SurfaceWorld departure mouth](captures/sw-departure-mouth.jpg) + +![SurfaceWorld departure rock entry](captures/sw-departure-rock-entry.jpg) + +## Hashes, determinism and cost + +Owner-world hashes match L32 exactly: all 12 hash lines per run across Clang/GCC and both recipes passed the owner preflight; the L25 and 24 m crossing matrix also passed at W1/W8/W12 (24/24 owner rows). W12 whole-view hashes: SurfaceWorld density `953eb1df1705f277`, mesh `d8006cb43f44aa5b`, payload `9217bff766970de5`, topology `83e3aa6f3a063e60`; FloatingIslands density `806537cdd98f8afb`, mesh `a4b06dd4d260943d`, payload `763a99aa04a73eae`, topology `baa37d80d3620d87`. The upper fixture was stable across W1/W8/W12 on both compilers. Total determinism: 30/30. Invariant audits: 16/16. + +Five interleaved W12 cost pairs per recipe against L32: median whole-view ready time changed +0.99% for SurfaceWorld and +0.41% for FloatingIslands; paired medians were +0.57% and +0.41%, within run noise. Recipe/evaluator versions remain 2/20. No Unreal files changed; no Unreal build was run. No version bump was needed because the owner hashes are unchanged. + +## Failed and retried runs + +- CLI setup rejected `--compiler clang`; the accepted form was `--compiler=clang++-19`. One shell quoting attempt ended at EOF. A FloatingIslands fixture initially had a `surface-world` recipe stamp and was regenerated with `floating-islands`. +- A translated ring centre failed the strict placement disk and was reverted. Fixture audit iterations: r1 had style 1–3 failures (TN capsule pinch, route undercut, naturalness); r2 had 141 open-air violations plus route/naturalness failures; r3 had strict ring-placement and route-support failures; r4 had four edge open-air samples. The broad column-window fix passed passage audits but changed owner hashes. Its first scoped retry dropped supported bracket samples and failed SurfaceWorld hash preflight on both compilers; restoring the original non-SW bracket bounds fixed it. +- An intermediate Clang build failed on `coreVoxelSizeMetres` and `gradeSegments`; both names were corrected. Final Clang 19 and GCC 14 `--fp-contract=off`, `-j12` builds succeeded. An early determinism run on stale binaries was stopped after 21 rows and showed the interim hash mismatch; the final binaries passed all 30 rows. +- The first capture export included the far ring and grew to 2.49 GB; it was stopped before rendering. The LOD0 streamed export reduced the scene to about 2.2 MB. Capture framings r5–r8 rendered successfully but obscured the mouth; r9 is the inspected final framing. + +No run exceeded 30 minutes and no step remained stuck for 15 minutes. Logs and exports are in `Saved/Linux/l33/`; no commit, push or stash was made. + +## Orchestrator review (Claude) + +- Merged: the owner world is unchanged (hashes equal L32); SW-as-upper now plans its route and passes the full audit set + on the FI→SW→TN fixture. The FloorProfileBlend check now measures seams (endpoint height/tangent residuals) with + step/tangent negative controls. Accepted: chord deviation measured curvature, not a defect. +- Captures: `sw-departure-mouth.jpg` is out of focus and unreadable; `sw-departure-rock-entry.jpg` shows a natural opening + but **horizontal step-like ledges inside**, reminiscent of the L23 terraces. L34 checks passage mouths and entries for + stepping across styles and fixes the blurry capture setup. diff --git a/Standalone/Research/WorldAssemblerM3/Linux33/RunCostL33.py b/Standalone/Research/WorldAssemblerM3/Linux33/RunCostL33.py new file mode 100644 index 0000000..e8b108e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux33/RunCostL33.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Run five interleaved W12 whole-view pairs against the L32 binary.""" +import re +import statistics +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l33" +OUT.mkdir(parents=True, exist_ok=True) +BINARIES = { + "l32": ROOT / "Saved/Linux/l32/bin/VoxelForgeCoreHeadless-l32-clang-v20", + "l33": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final", +} +ORDERS = (("l32", "l33"), ("l33", "l32"), ("l32", "l33"), + ("l33", "l32"), ("l32", "l33")) +READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s") +rows = [] +failed = False + + +for recipe in ("surface-world", "floating-islands"): + for pair, order in enumerate(ORDERS, 1): + for build in order: + name = f"cost-{recipe}-pair{pair}-{build}" + logfile = OUT / f"{name}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"), + "--workers=12", "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", + ] + started = time.monotonic() + try: + with logfile.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + timeout=900, check=False) + except subprocess.TimeoutExpired: + result = None + elapsed = time.monotonic() - started + text = logfile.read_text(encoding="utf-8", errors="replace") + matches = READY.findall(text) + stamps = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + text, re.M) + stamp_ok = stamps == [(recipe, recipe, "PASS")] + valid = (result is not None and result.returncode == 0 and len(matches) == 1 + and "hash=off" in text and "cache_hits=0" in text and stamp_ok) + ready = float(matches[0]) if len(matches) == 1 else float("nan") + rows.append((recipe, pair, build, ready, elapsed, + result.returncode if result else "TIMEOUT", valid)) + print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s " + f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True) + failed |= not valid + +summary = OUT / "cost-runs.csv" +summary.write_text( + "recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" + + "".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code}," + f"{'PASS' if ok else 'FAIL'}\n" + for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8") + +for recipe in ("surface-world", "floating-islands"): + by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build] + for build in BINARIES} + baseline = statistics.median(by_build["l32"]) + current = statistics.median(by_build["l33"]) + paired = [] + for pair in range(1, 6): + values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair} + paired.append((values["l33"] - values["l32"]) / values["l32"] * 100.0) + print(f"[L33CostSummary] recipe={recipe} pairs=5 median_l32_s={baseline:.3f} " + f"median_l33_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} " + f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}", flush=True) + +print(f"[L33Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}", + flush=True) +raise SystemExit(1 if failed or len(rows) != 20 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux33/RunDeterminismL33.py b/Standalone/Research/WorldAssemblerM3/Linux33/RunDeterminismL33.py new file mode 100644 index 0000000..5dcb4eb --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux33/RunDeterminismL33.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Run owner L25 hashes, 24 m crossings, and upper-SurfaceWorld fixture determinism.""" +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l33" +L32 = ROOT / "Saved/Linux/l32" +FIXTURE = OUT / "fixtures/sw-upper-tunnel-network-style0.json" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-gcc-l33-final", +} +HASH_PATTERN = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +LAYOUT_PATTERN = re.compile( + r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$", + re.M) +PLAN_PATTERN = re.compile( + r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*" + r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M) +ROUTE_PATTERN = re.compile( + r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) " + r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M) +rows = [] +failed = False +owner_reference = {} +fixture_reference = None + + +def run(command, logfile): + started = time.monotonic() + try: + with logfile.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + timeout=1800, check=False) + except subprocess.TimeoutExpired: + result = None + return result, time.monotonic() - started, logfile.read_text( + encoding="utf-8", errors="replace") + + +for kind in ("l25", "crossing24"): + for recipe in ("surface-world", "floating-islands"): + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"{kind}-{compiler}-{recipe}-w{workers}" + logfile = OUT / f"determinism-{name}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + str(binary), str(ROOT / "Standalone/data/owner_world.json"), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--stream-hash", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", "--far-horizon-lod8-step=0", + f"--workers={workers}", f"--recipe={recipe}", + ] + if kind == "crossing24": + command.extend(("--camera-step=24,0,0", "--region-crossing")) + result, elapsed, text = run(command, logfile) + hashes = HASH_PATTERN.findall(text) + stamps = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + text, re.M) + ref = L32 / f"{kind}-settled-final-{compiler}-{recipe}-w{workers}.log" + reference_hashes = HASH_PATTERN.findall(ref.read_text( + encoding="utf-8", errors="replace")) if ref.exists() else [] + stable_key = (kind, recipe) + stable = stable_key not in owner_reference or hashes == owner_reference[stable_key] + if stable_key not in owner_reference and hashes: + owner_reference[stable_key] = hashes + equal_l32 = len(hashes) == 12 and hashes == reference_hashes + ok = (result is not None and result.returncode == 0 + and stamps == [(recipe, recipe, "PASS")] + and "hash=on" in text and len(hashes) == 12 and stable and equal_l32) + rows.append((kind, recipe, compiler, workers, elapsed, + result.returncode if result else "TIMEOUT", ok)) + print(f"{name} elapsed={elapsed:.1f}s hashes={len(hashes)}/12 " + f"same_across_matrix={'yes' if stable else 'no'} " + f"same_as_l32={'yes' if equal_l32 else 'no'} {'PASS' if ok else 'FAIL'}", + flush=True) + failed |= not ok + + +for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"upper-fixture-{compiler}-w{workers}" + logfile = OUT / f"determinism-{name}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + str(binary), str(FIXTURE), "--workers=" + str(workers), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--force-generate", "--clip-radius=3", "--max-clip-level=8", + "--max-level=-1", "--recipe=floating-islands", "--audit-resolved-layout-passages", + ] + result, elapsed, text = run(command, logfile) + layouts = LAYOUT_PATTERN.findall(text) + plans = PLAN_PATTERN.findall(text) + routes = ROUTE_PATTERN.findall(text) + stamps = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + text, re.M) + passage_summary = re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", text, re.M) + signature = (tuple(layouts), tuple(plans), tuple(routes)) + stable = fixture_reference is None or signature == fixture_reference + if fixture_reference is None and layouts and plans and routes: + fixture_reference = signature + ok = (result is not None and result.returncode == 0 and len(layouts) == 1 + and len(plans) == 1 and len(routes) == 1 + and stamps == [("floating-islands", "floating-islands", "PASS")] + and passage_summary is not None and stable) + rows.append(("upper-fixture", "floating-islands", compiler, workers, elapsed, + result.returncode if result else "TIMEOUT", ok)) + print(f"{name} elapsed={elapsed:.1f}s layout={layouts[0] if layouts else 'MISSING'} " + f"stable_across_matrix={'yes' if stable else 'no'} {'PASS' if ok else 'FAIL'}", + flush=True) + failed |= not ok + + +summary = OUT / "determinism-l33.csv" +summary.write_text( + "kind,recipe,compiler,workers,elapsed_seconds,exit_code,status\n" + + "".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code}," + f"{'PASS' if ok else 'FAIL'}\n" + for kind, recipe, compiler, workers, elapsed, code, ok in rows), + encoding="utf-8") +print(f"[L33Determinism] runs={len(rows)} pass={sum(row[-1] for row in rows)} " + f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True) +raise SystemExit(1 if failed or len(rows) != 30 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux33/RunInvariantAuditsL33.py b/Standalone/Research/WorldAssemblerM3/Linux33/RunInvariantAuditsL33.py new file mode 100644 index 0000000..b53916c --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux33/RunInvariantAuditsL33.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Run L33 route/proof and transition invariants with recipe-stamped logs.""" +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l33" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-gcc-l33-final", +} +rows = [] +failed = False + + +def run(name, binary, data, recipe, extra, checks): + global failed + logfile = OUT / f"{name}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + str(binary), str(data), "--workers=12", "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", *extra, + ] + started = time.monotonic() + try: + with logfile.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + timeout=900, check=False) + except subprocess.TimeoutExpired: + result = None + elapsed = time.monotonic() - started + text = logfile.read_text(encoding="utf-8", errors="replace") + stamps = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + text, re.M) + stamp_ok = stamps == [(recipe, recipe, "PASS")] + checks_ok = all(check(text) for check in checks) + ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok + rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok)) + print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} " + f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} " + f"{'PASS' if ok else 'FAIL'}", flush=True) + failed |= not ok + + +owner = ROOT / "Standalone/data/owner_world.json" +for recipe in ("surface-world", "floating-islands"): + for compiler, binary in BINARIES.items(): + name = f"invariant-route-proof-{recipe}-{compiler}" + run(name, binary, owner, recipe, + ["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"], + [lambda text: bool(re.search( + r"^\[RouteParityAudit\].*status=PASS$", text, re.M)), + lambda text: bool(re.search( + r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))]) + +for recipe in ("surface-world", "floating-islands"): + for compiler, binary in BINARIES.items(): + for mode in ("hard", "gradient", "interleaved"): + name = f"invariant-transition-{recipe}-{mode}-{compiler}" + run(name, binary, owner, recipe, + [f"--transition-mode={mode}", "--audit-strate-transitions"], + [lambda text: bool(re.search( + r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$", + text, re.M)), + lambda text: bool(re.search( + r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))]) + +summary = OUT / "invariant-audits.csv" +summary.write_text( + "run,recipe,elapsed_seconds,exit_code,status\n" + + "".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n" + for name, recipe, elapsed, code, ok in rows), + encoding="utf-8") +print(f"[L33InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} " + f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True) +raise SystemExit(1 if failed or len(rows) != 16 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux33/RunUpperFixtureAuditsL33.py b/Standalone/Research/WorldAssemblerM3/Linux33/RunUpperFixtureAuditsL33.py new file mode 100644 index 0000000..d1848a6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux33/RunUpperFixtureAuditsL33.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Recipe-stamped resolved-passage audits for the L33 upper-SurfaceWorld stack.""" +import os +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l33" +TAG = os.environ.get("VOXELFORGE_L33_AUDIT_TAG", "clang").strip() or "clang" +BINARY = Path(os.environ.get("VOXELFORGE_L33_AUDIT_BINARY", + str(OUT / f"bin/VoxelForgeCoreHeadless-{TAG}-l33-final"))) +ROWS = [] +FAILED = False + +for style in range(4): + name = f"upper-sw-three-strata-{TAG}-style{style}" + fixture = OUT / f"fixtures/sw-upper-tunnel-network-style{style}.json" + logfile = OUT / f"{name}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + str(BINARY), str(fixture), "--workers=12", "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--force-generate", + "--clip-radius=3", "--max-clip-level=8", "--max-level=-1", + "--recipe=floating-islands", "--audit-resolved-layout-passages", + ] + started = time.monotonic() + try: + with logfile.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + timeout=1800, check=False) + except subprocess.TimeoutExpired: + result = None + elapsed = time.monotonic() - started + text = logfile.read_text(encoding="utf-8", errors="replace") + stamp_ok = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + text, re.M) == [("floating-islands", "floating-islands", "PASS")] + summary = re.search(r"^\[ResolvedPassageAuditSummary\].*$", text, re.M) + summary_ok = bool(summary and "interfaces=2 passed=2 natural=2/2" in summary.group() + and "route_walkable=2" in summary.group() + and "route_binding_failures=0" in summary.group() + and "status=PASS" in summary.group()) + markers = ( + r"^\[PassageOpenAirAuditSummary\].*violations=0.*status=PASS$", + r"^\[PassageNoLongBridgeAudit\].*status=PASS$", + r"^\[PassageRockColumnAuditSummary\].*wall_or_support_failures=0.*status=PASS$", + r"^\[PassageNaturalnessSummary\].*status=PASS$", + ) + markers_ok = all(re.search(pattern, text, re.M) for pattern in markers) + passage_rows = re.findall( + r"^\[ResolvedPassageAudit\].*blocked=0 support_samples=\d+ unsupported=0 " + r"grade_failures=0 mouth_content=2/2 mouth_radius=2/2 walkability=PASS$", + text, re.M) + floor_rows = re.findall(r"^\[FloorProfileBlend\].*status=PASS$", text, re.M) + route_rows = re.findall( + r"^\[ResolvedStrateRouteAudit\].*blocked=0 support_samples=\d+ unsupported=0 " + r"grade_failures=0 mouth_binding=PASS status=PASS$", text, re.M) + ok = (result is not None and result.returncode == 0 and stamp_ok and summary_ok + and markers_ok and len(passage_rows) == 2 and len(route_rows) == 2 + and len(floor_rows) >= 1 and "[Walkability] PASS" in text) + FAILED |= not ok + ROWS.append((style, elapsed, result.returncode if result else "TIMEOUT", ok)) + print(f"{name} elapsed={elapsed:.1f}s exit={result.returncode if result else 'TIMEOUT'} " + f"recipe={'PASS' if stamp_ok else 'FAIL'} passages={len(passage_rows)}/2 " + f"routes={len(route_rows)}/2 floor_blend_rows={len(floor_rows)} " + f"{'PASS' if ok else 'FAIL'}", flush=True) + +summary_path = OUT / f"upper-sw-three-strata-{TAG}-audits.csv" +summary_path.write_text( + "style,elapsed_seconds,exit_code,status\n" + + "".join(f"{style},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n" + for style, elapsed, code, ok in ROWS), encoding="utf-8") +print(f"[L33UpperSurfaceFixtureAudit] compiler={TAG} runs={len(ROWS)} pass={sum(row[-1] for row in ROWS)} " + f"fail={sum(not row[-1] for row in ROWS)} summary={summary_path}", flush=True) +raise SystemExit(1 if FAILED or len(ROWS) != 4 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux33/captures/sw-departure-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux33/captures/sw-departure-mouth.jpg new file mode 100644 index 0000000..468698a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux33/captures/sw-departure-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux33/captures/sw-departure-rock-entry.jpg b/Standalone/Research/WorldAssemblerM3/Linux33/captures/sw-departure-rock-entry.jpg new file mode 100644 index 0000000..f7fef1b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux33/captures/sw-departure-rock-entry.jpg differ diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 389b8b7..74a1825 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -820,7 +820,9 @@ namespace StandaloneStreamBenchmark const auto& b=passage.controlPoints[i]; const double dx=qToMetres(b.positionSubvoxels[0]-a.positionSubvoxels[0]); const double dy=qToMetres(b.positionSubvoxels[1]-a.positionSubvoxels[1]); - const double dz=qToMetres(b.positionSubvoxels[2]-a.positionSubvoxels[2]); + const double floorA=qToMetres(a.positionSubvoxels[2]-a.radiusSubvoxels)+1.13; + const double floorB=qToMetres(b.positionSubvoxels[2]-b.radiusSubvoxels)+1.13; + const double dz=floorB-floorA; const double horizontal=std::hypot(dx,dy); if(horizontal>1.0e-6&&std::abs(dz)/horizontal ::infinity(); double crossingMaximumSurfaceRadius=0.0; @@ -1378,12 +1383,13 @@ namespace StandaloneStreamBenchmark &&crossingWallFailures==0&&std::isfinite(measuredWall) &&measuredWall>=1.50&&crossingSurfaceRelief>=0.20; audit.passed&=columnPass; - std::printf("[PassageRockColumnAudit] id=%s segments=%zu..%zu span_m=%.3f axis_m=(%.3f,%.3f) connected=%d/2 route_support=%llu/%llu wall_margin_m=%.3f threshold_m=1.500 radii_m=%.3f..%.3f relief_p2p_m=%.3f nominal_radius_m=%.3f status=%s\n", + std::printf("[PassageRockColumnAudit] id=%s segments=%zu..%zu span_m=%.3f axis_m=(%.3f,%.3f) connected=%d/2 route_support=%llu/%llu wall_failures=%llu wall_margin_m=%.3f threshold_m=1.500 radii_m=%.3f..%.3f relief_p2p_m=%.3f nominal_radius_m=%.3f status=%s\n", passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment, crossing.unsupportedLengthMetres,axisX,axisY, static_cast(topCapConnected)+static_cast(bottomCapConnected), static_cast(probeCount-crossingRouteFailures), - static_cast(probeCount),measuredWall, + static_cast(probeCount), + static_cast(crossingWallFailures),measuredWall, crossingMinimumSurfaceRadius,crossingMaximumSurfaceRadius, crossingSurfaceRelief,qToMetres(crossing.columnNominalRadiusSubvoxels), columnPass?"PASS":"FAIL"); diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index b01a142..f8bea9e 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -44,7 +45,8 @@ namespace StandaloneWorldAssemblyPrototype inline bool BuildFiniteStrateRoute(VoxelForgeCore::WorldAssemblyRecipeV1& recipe, VoxelForgeCore::WorldAssemblyStratumPlanV1& stratum,std::uint64_t& tested, std::uint64_t& gradeRejected,std::uint64_t& placementRejected, - std::uint64_t& sweepRejected,std::int64_t arrivalRadiusSubvoxels, + std::uint64_t& sweepRejected,std::uint32_t coreVoxelSizeMicrons, + std::int64_t arrivalRadiusSubvoxels, std::int64_t departureRadiusSubvoxels,std::string& error) { using namespace VoxelForgeCore; @@ -63,9 +65,20 @@ namespace StandaloneWorldAssemblyPrototype struct Candidate { std::int64_t score=0; + std::uint64_t endpointProximityCost=0; std::int32_t centerX=0,centerY=0,direction=0; std::vector points; }; + struct GradeSegmentAudit + { + std::uint64_t failures=0; + double maximumGrade=0.0; + std::int64_t fromGround=0,toGround=0,fromX=0,fromY=0,toX=0,toY=0; + }; + std::array placementValidGradeSegments{}; + std::uint64_t gradeFailCandidates=0; + double minimumGround=std::numeric_limits::infinity(); + double maximumGround=-std::numeric_limits::infinity(); auto& constraints=stratum.routeConstraints; constraints.version=1; @@ -84,10 +97,112 @@ namespace StandaloneWorldAssemblyPrototype if(!finalField.isValid()) {error="route planner received an invalid final composed field";return false;} std::vector candidates; - constexpr std::int32_t halfLength=128; - constexpr std::int32_t waypointSpacing=8; - for(std::int32_t centerY=-256;centerY<=256;centerY+=64) - for(std::int32_t centerX=-256;centerX<=256;centerX+=64) + std::vector candidateHalfLengths{128}; + std::int32_t candidateCenterSpacing=64; + if(arrivalRadiusSubvoxels>0&&departureRadiusSubvoxels>0) + { + // A centered line cannot join two bounded mouths when its chord is + // longer than the sum of their placement radii. Leave 3 m for the + // capsule and terrain-profile repair, then search centers at 8 m + // spacing so asymmetric arrival/departure radii can both be reached. + constexpr std::int64_t placementMarginQ=12*WorldAssemblySubvoxelsPerVoxel; + const std::int64_t availableLengthVoxels=std::max(1, + (arrivalRadiusSubvoxels+departureRadiusSubvoxels-placementMarginQ) + /WorldAssemblySubvoxelsPerVoxel); + const std::int32_t maximumHalfLength=static_cast(std::min(128, + std::max(1,availableLengthVoxels/2))); + candidateHalfLengths.clear(); + for(const std::int32_t numerator:std::array{{8,7,6,5,4,3}}) + { + const std::int32_t halfLength=std::max(1,maximumHalfLength*numerator/8); + if(std::find(candidateHalfLengths.begin(),candidateHalfLengths.end(),halfLength) + ==candidateHalfLengths.end())candidateHalfLengths.push_back(halfLength); + } + candidateCenterSpacing=32; + } + const double coreVoxelMetres=static_cast(coreVoxelSizeMicrons)/1000000.0; + if(std::getenv("VOXELFORGE_AUDIT_LEGACY_ROUTE_GRADE")) + { + constexpr std::int32_t legacyCenterSpacing=64; + constexpr std::int32_t legacyWaypointSpacing=8; + std::array legacySegments{}; + std::uint64_t legacyGradeFailCandidates=0; + double legacyMinimumGround=std::numeric_limits::infinity(); + double legacyMaximumGround=-std::numeric_limits::infinity(); + for(std::int32_t centerY=-256;centerY<=256;centerY+=legacyCenterSpacing) + for(std::int32_t centerX=-256;centerX<=256;centerX+=legacyCenterSpacing) + for(const Direction& direction:directions) + { + bool candidateGradeFail=false; + std::int64_t previousGround=0,previousX=0,previousY=0; + for(std::int32_t index=0;index<=32;++index) + { + const std::int64_t along=(index-16)*legacyWaypointSpacing; + const std::int64_t x=centerX+std::llround( + static_cast(along)*direction.x/65536.0); + const std::int64_t y=centerY+std::llround( + static_cast(along)*direction.y/65536.0); + const auto column=finalField.point(WorldAssemblyFixed(static_cast(x)), + WorldAssemblyFixed(static_cast(y)),0); + const double groundMetres=static_cast(column.groundHeightSubvoxels) + *coreVoxelMetres/WorldAssemblySubvoxelsPerVoxel; + legacyMinimumGround=std::min(legacyMinimumGround,groundMetres); + legacyMaximumGround=std::max(legacyMaximumGround,groundMetres); + if(index>0) + { + const std::int64_t horizontalQ=static_cast(std::llround( + std::hypot(static_cast(x-previousX), + static_cast(y-previousY))*WorldAssemblySubvoxelsPerVoxel)); + const std::int64_t riseQ=std::abs( + column.groundHeightSubvoxels-previousGround); + if(horizontalQ==0||riseQ*65536>horizontalQ*GradeQ16) + { + candidateGradeFail=true; + GradeSegmentAudit& segment=legacySegments[ + static_cast(index-1)]; + ++segment.failures; + const double grade=horizontalQ>0 + ?static_cast(riseQ)/static_cast(horizontalQ) + :std::numeric_limits::infinity(); + if(grade>segment.maximumGrade) + { + segment.maximumGrade=grade; + segment.fromGround=previousGround; + segment.toGround=column.groundHeightSubvoxels; + segment.fromX=previousX;segment.fromY=previousY; + segment.toX=x;segment.toY=y; + } + } + } + previousGround=column.groundHeightSubvoxels;previousX=x;previousY=y; + } + if(candidateGradeFail)++legacyGradeFailCandidates; + } + std::printf("[WorldAssemblerLegacyRouteGradeProfile] candidates=648 grade_fail_candidates=%llu ground_range_m=%.3f..%.3f grade_limit_deg=15.000 chord_m=%.3f waypoint_step_m=%.3f center_step_m=%.3f\n", + static_cast(legacyGradeFailCandidates), + legacyMinimumGround,legacyMaximumGround,256.0*coreVoxelMetres, + legacyWaypointSpacing*coreVoxelMetres,legacyCenterSpacing*coreVoxelMetres); + const double metresPerSubvoxel=coreVoxelMetres/WorldAssemblySubvoxelsPerVoxel; + for(std::size_t segmentIndex=0;segmentIndex%.3f xy_m=(%.3f,%.3f)->(%.3f,%.3f)\n", + segmentIndex,static_cast(segment.failures), + std::atan(segment.maximumGrade)*57.29577951308232, + segment.fromGround*metresPerSubvoxel,segment.toGround*metresPerSubvoxel, + segment.fromX*coreVoxelMetres,segment.fromY*coreVoxelMetres, + segment.toX*coreVoxelMetres,segment.toY*coreVoxelMetres); + } + } + std::printf("[WorldAssemblerRouteCandidateEnvelope] max_chord_m=%.3f length_variants=%zu center_step_m=%.3f arrival_radius_m=%.3f departure_radius_m=%.3f\n", + 2.0*candidateHalfLengths.front()*coreVoxelMetres,candidateHalfLengths.size(), + candidateCenterSpacing*coreVoxelMetres, + arrivalRadiusSubvoxels*coreVoxelMetres/WorldAssemblySubvoxelsPerVoxel, + departureRadiusSubvoxels*coreVoxelMetres/WorldAssemblySubvoxelsPerVoxel); + for(const std::int32_t candidateHalfLength:candidateHalfLengths) + for(std::int32_t centerY=-256;centerY<=256;centerY+=candidateCenterSpacing) + for(std::int32_t centerX=-256;centerX<=256;centerX+=candidateCenterSpacing) for(std::int32_t directionIndex=0;directionIndex(directions.size());++directionIndex) { Candidate candidate; @@ -95,22 +210,36 @@ namespace StandaloneWorldAssemblyPrototype candidate.points.reserve(33); std::int64_t previousGround=0,previousX=0,previousY=0; bool gradePass=true,placementPass=true; + std::array candidateGradeSegments{}; for(std::int32_t index=0;index<=32;++index) { - const std::int64_t along=(index-16)*waypointSpacing; + const std::int64_t along=(index-16)*candidateHalfLength/16; const std::int64_t x=centerX+std::llround( static_cast(along)*directions[directionIndex].x/65536.0); const std::int64_t y=centerY+std::llround( static_cast(along)*directions[directionIndex].y/65536.0); const std::int64_t xQ=WorldAssemblyFixed(static_cast(x)); const std::int64_t yQ=WorldAssemblyFixed(static_cast(y)); + if(arrivalRadiusSubvoxels>0&&departureRadiusSubvoxels>0 + &&(index==0||index==32)) + { + const std::uint64_t radiusSquared=static_cast( + xQ*xQ+yQ*yQ); + candidate.endpointProximityCost+=radiusSquared*(index==0?16ull:1ull); + } const auto inRadius=[](std::int64_t xValue,std::int64_t yValue,std::int64_t radius) {return radius<=0||xValue*xValue+yValue*yValue<=radius*radius;}; if((index==0&&!inRadius(xQ,yQ,arrivalRadiusSubvoxels)) ||(index==32&&!inRadius(xQ,yQ,departureRadiusSubvoxels))) - {placementPass=false;break;} + placementPass=false; const auto column=finalField.point(WorldAssemblyFixed(static_cast(x)), WorldAssemblyFixed(static_cast(y)),0); + const double metresPerSubvoxel=static_cast(coreVoxelSizeMicrons) + /(WorldAssemblySubvoxelsPerVoxel*1000000.0); + const double groundMetres=static_cast(column.groundHeightSubvoxels) + *metresPerSubvoxel; + minimumGround=std::min(minimumGround,groundMetres); + maximumGround=std::max(maximumGround,groundMetres); const std::int64_t horizontalQ=static_cast(std::llround( std::hypot(static_cast(x-previousX),static_cast(y-previousY)) *WorldAssemblySubvoxelsPerVoxel)); @@ -118,7 +247,23 @@ namespace StandaloneWorldAssemblyPrototype { const std::int64_t riseQ=std::abs(column.groundHeightSubvoxels-previousGround); if(horizontalQ==0||riseQ*65536>horizontalQ*GradeQ16) - {gradePass=false;break;} + { + gradePass=false; + GradeSegmentAudit& segment=candidateGradeSegments[ + static_cast(index-1)]; + ++segment.failures; + const double grade=horizontalQ>0 + ?static_cast(riseQ)/static_cast(horizontalQ) + :std::numeric_limits::infinity(); + if(grade>segment.maximumGrade) + { + segment.maximumGrade=grade; + segment.fromGround=previousGround; + segment.toGround=column.groundHeightSubvoxels; + segment.fromX=previousX;segment.fromY=previousY; + segment.toX=x;segment.toY=y; + } + } candidate.score+=riseQ*65536/std::max(1,horizontalQ); } WorldAssemblyRoutePointV1 point; @@ -131,13 +276,60 @@ namespace StandaloneWorldAssemblyPrototype candidate.points.push_back(point); previousGround=column.groundHeightSubvoxels;previousX=x;previousY=y; } + candidate.score/=32; ++tested; + if(!gradePass)++gradeFailCandidates; if(placementPass&&gradePass)candidates.push_back(std::move(candidate)); else if(!placementPass)++placementRejected; - else ++gradeRejected; + else + { + ++gradeRejected; + for(std::size_t segmentIndex=0;segmentIndexaggregate.maximumGrade) + { + aggregate.maximumGrade=rejected.maximumGrade; + aggregate.fromGround=rejected.fromGround; + aggregate.toGround=rejected.toGround; + aggregate.fromX=rejected.fromX;aggregate.fromY=rejected.fromY; + aggregate.toX=rejected.toX;aggregate.toY=rejected.toY; + } + } + } + } + if(gradeRejected>0||placementRejected>0) + { + const double metresPerSubvoxel=static_cast(coreVoxelSizeMicrons) + /(WorldAssemblySubvoxelsPerVoxel*1000000.0); + std::printf("[WorldAssemblerRouteGradeProfile] tested=%llu grade_fail_candidates=%llu grade_rejected_after_placement=%llu placement_rejected=%llu ground_range_m=%.3f..%.3f grade_limit_deg=15.000\n", + static_cast(tested), + static_cast(gradeFailCandidates), + static_cast(gradeRejected), + static_cast(placementRejected),minimumGround,maximumGround); + for(std::size_t segmentIndex=0;segmentIndex%.3f xy_m=(%.3f,%.3f)->(%.3f,%.3f)\n", + segmentIndex,static_cast(segment.failures),gradeDegrees, + segment.fromGround*metresPerSubvoxel,segment.toGround*metresPerSubvoxel, + segment.fromX*coreVoxelSizeMicrons/1000000.0, + segment.fromY*coreVoxelSizeMicrons/1000000.0, + segment.toX*coreVoxelSizeMicrons/1000000.0, + segment.toY*coreVoxelSizeMicrons/1000000.0); + } } std::stable_sort(candidates.begin(),candidates.end(),[](const Candidate& a,const Candidate& b) { + if(a.endpointProximityCost!=b.endpointProximityCost) + return a.endpointProximityCost0) std::printf("[WorldAssemblerRouteRepaired] uniform_lift_q=%lld support_limit_q=%lld\n", static_cast(liftQ),static_cast(SupportDepthQ)); + const double metresPerSubvoxel=coreVoxelMetres + /WorldAssemblySubvoxelsPerVoxel; + std::printf("[WorldAssemblerRouteAccepted] arrival_xy_m=(%.3f,%.3f) departure_xy_m=(%.3f,%.3f) endpoint_proximity_cost=%llu average_grade_q16=%lld\n", + candidate.points.front().positionSubvoxels[0]*metresPerSubvoxel, + candidate.points.front().positionSubvoxels[1]*metresPerSubvoxel, + candidate.points.back().positionSubvoxels[0]*metresPerSubvoxel, + candidate.points.back().positionSubvoxels[1]*metresPerSubvoxel, + static_cast(candidate.endpointProximityCost), + static_cast(candidate.score)); return true; } if(audit.blockedCapsuleSamples==0||liftQ>=MaximumRepairLiftQ)break; @@ -410,7 +611,8 @@ namespace StandaloneWorldAssemblyPrototype } std::uint64_t routeCandidates=0,routeGradeRejected=0,routePlacementRejected=0,routeSweepRejected=0; if(!BuildFiniteStrateRoute(recipe,stratum,routeCandidates,routeGradeRejected, - routePlacementRejected,routeSweepRejected,arrivalRadiusQ,departureRadiusQ,error))return false; + routePlacementRejected,routeSweepRejected,owner.CoreVoxelSizeMicrons, + arrivalRadiusQ,departureRadiusQ,error))return false; std::printf("[WorldAssemblerRoutePlan] candidates=%llu grade_rejected=%llu placement_rejected=%llu sweep_rejected=%llu accepted_points=%zu capsule_m=0.34x1.76 corridor_radius_vox=4 support_probe_points=5 minimum_support_probes=%u arrival_radius_m=%.3f departure_radius_m=%.3f status=PASS\n", static_cast(routeCandidates), static_cast(routeGradeRejected),