diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 95223ef..947f994 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2316,6 +2316,37 @@ spatial-property inventory: legacy chunk distances, owner/Core voxel lengths and placement vectors, and other centimetre fields remain in their prior units. The U1 report records the five BuildHost asset values, hashes, and remaining work. +### 8.56 Reach-bounded ownership slabs for prepared passages — World Assembler M3 L128 + +The support radius varies along the route as `S(s) = r(s) + route_reach + safety`. On the owner +world's 3.5 m body bore, `S = 3.5 + 4.335 + 0.05 = 7.885 m`; at the 4.5 m mouths, `S = 8.885 m` +and those joints belong to transition owners. `route_reach = 4.0 m sweep + 0.21 m wall relief + +0.125 m quarter-corner allowance`; the 0.75 m floor shell and 1 m open-air side wall fit inside +that conservative reach. For a smooth centreline with curvature `κ`, the plane coordinate +`g_s(x) = (x - c(s)) · T(s)` is strictly decreasing throughout support when `κS < 1`, or bend +radius `ρ > S`. On a circular joint with chord length `L` and turn `θ`, the equivalent bound is +`L / (2 sin(θ/2)) > S`. The audit also checks doubly-critical nonlocal route pairs against the +sum of their two support radii, since a large local bend radius alone cannot prevent a route from +folding back across itself. + +Each normal body frame owns the half-open region between its start and end tangent planes. The +outside planes are omitted at the two mouths; tight mouth fans are combined into transition +owners. When routes meet at a joint, their passage fields remain independent contributors to the +deterministic union. The same plane predicate filters prepared tile sweeps and exact off-grid +queries. Floor height remains independent of round-distance selection: `PassageOwnershipFloorT` +projects onto the owner's XY route segment because authored `h(s)` uses horizontal progress. The +9-station density probe matches L127's root residuals exactly; across its 0.1 m samples, the +largest adjacent root change is 1.765 mm. + +Planning audits local curvature, nonlocal separation, 33,813 owner-world support probes, and +negative controls that inject a duplicate claim and a no-owner gap. The owner world has three +routes; tight joints are handled by mouth transitions and the body passes the support bound. A +pathological legacy two-strata fixture instead receives one full-route composite owner, keeping +its established field while retaining one slab claim. Such fallback routes do not use the body-slab +shortcut. The evaluator version is 226; no +owner route control points are changed. See +[`Standalone/Research/WorldAssemblerM3/Linux128/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux128/REPORT.md). + ## 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 56d375f..a59bfb3 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1352,6 +1352,17 @@ tunnel-slope version in `VoxelStrateDefinition.cpp`, plus the standalone `VoxelF in `VoxelForgeExploreCommandlet.cpp`. `GetStrateForChunk` still receives layout-chunk Z, not a Core tile index. See `Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md` for BuildHost evidence. +### 3.38 Prepared passage slab ownership — World Assembler M3 L128 + +| Symbol | Canonical file | Role | +|---|---|---| +| `FSlabReachAudit`, `AuditSlabReach`, `BuildSegments` | `Standalone/Core/include/VoxelForgeCore/PassageSpline.h` | Audits reach-bounded bend curvature, nonlocal route separation, single slab ownership, and injected duplicate/no-owner controls; chooses finite mouth-transition ranges. | +| `FPassageOwnershipSlab`, `PassageOwnershipSlabOwns`, `PassageOwnershipFloorT` | `Standalone/Core/src/ShapeWorldInternal.h` | Shares half-open slab-plane ownership between exact point queries and prepared tile sweeps; floor-profile `t` uses horizontal route progress independently of round distance, with a frame-plane fallback for vertical segments. Pathological legacy paths use one full-route composite owner. | +| `PassagePlanSlabAudit` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Fails layout planning when neither the normal slabs nor the validated whole-route composite can provide exactly one owner per support probe. | +| `WorldAssemblyEvaluatorVersion` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Bumped to 226 for slab ownership. | + +See ARCHITECTURE §8.56 and [`Standalone/Research/WorldAssemblerM3/Linux128/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux128/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h index 6b61bb9..93dbe2c 100644 --- a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h +++ b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h @@ -2,10 +2,14 @@ #include #include +#include #include #include +#include #include +#include #include +#include #include namespace VoxelForgeCore::PassageSpline @@ -23,6 +27,32 @@ namespace VoxelForgeCore::PassageSpline double plannedFloorA=0.0,plannedFloorB=0.0; }; + struct FSlabReachAudit + { + std::size_t upperMouthLastControlSpan=0; + std::size_t lowerMouthFirstControlSpan=0; + std::size_t upperMouthEndSegment=0; + std::size_t upperMouthTransitionEndSegment=0; + std::size_t lowerMouthStartSegment=0; + std::size_t lowerMouthTransitionStartSegment=0; + std::size_t upperTransitionLastControlSpan=0; + std::size_t tightMouthJoints=0; + std::size_t nonlocalMouthPairs=0; + std::size_t bodyCurvatureFailures=0; + std::size_t bodySelfOverlapFailures=0; + std::size_t ownershipTestSamples=0; + std::size_t zeroOwnerSamples=0,multipleOwnerSamples=0; + bool ownershipNegativeControlsPass=false; + bool usesWholeRouteCompositeSlab=false; + std::size_t firstZeroOwnerProbe=std::numeric_limits::max(); + std::size_t firstZeroOwnerSegment=std::numeric_limits::max(); + std::array firstZeroOwnerPoint{}; + double firstZeroOwnerStartSide=0.0,firstZeroOwnerEndSide=0.0; + double minimumBodyCurvatureRadiusMetres=std::numeric_limits::infinity(); + double minimumBodySelfGapMetres=std::numeric_limits::infinity(); + bool valid=false; + }; + inline double KnotSpan(const FPoint& a,const FPoint& b,double unitsPerMetre) { const double dx=(b[0]-a[0])/unitsPerMetre; @@ -252,4 +282,626 @@ namespace VoxelForgeCore::PassageSpline } return result; } + + inline FSlabReachAudit ComputeSlabReachAudit(std::span segments, + std::size_t routeEndControlIndex,double unitsPerMetre,double routeReachMetres, + double safetyMarginMetres=0.05) + { + FSlabReachAudit result; + if(segments.size()<2||!(unitsPerMetre>0.0)||!(routeReachMetres>=0.0))return result; + const std::size_t lastRouteSpan=std::min(routeEndControlIndex, + segments.back().controlSpan); + result.upperMouthLastControlSpan=0; + result.lowerMouthFirstControlSpan=lastRouteSpan; + struct FStation + { + std::array position{}; + double radius=0.0,along=0.0; + std::size_t controlSpan=0; + }; + std::vector stations; + stations.reserve(segments.size()+1); + const auto makeStation=[&](const FPoint& point,double along,std::size_t span) + { + FStation station; + for(std::size_t axis=0;axis<3;++axis) + station.position[axis]=point[axis]/unitsPerMetre; + station.radius=std::max(0.0,point[3]/unitsPerMetre); + station.along=along; + station.controlSpan=span; + return station; + }; + stations.push_back(makeStation(segments.front().a,segments.front().alongMetres, + segments.front().controlSpan)); + for(const auto& segment:segments) + stations.push_back(makeStation(segment.b, + segment.alongMetres+segment.lengthMetres,segment.controlSpan)); + double maximumSupport=0.0,maximumSegmentLength=0.0; + for(const auto& station:stations) + maximumSupport=std::max(maximumSupport,station.radius + +routeReachMetres+safetyMarginMetres); + for(const auto& segment:segments) + maximumSegmentLength=std::max(maximumSegmentLength,segment.lengthMetres); + + const auto distance=[](const FStation& a,const FStation& b) + { + double squared=0.0; + for(std::size_t axis=0;axis<3;++axis) + {const double delta=a.position[axis]-b.position[axis];squared+=delta*delta;} + return std::sqrt(squared); + }; + const auto curvatureRadius=[&](std::size_t index) + { + const auto& a=stations[index-1]; + const auto& b=stations[index]; + const auto& c=stations[index+1]; + const std::array ab={b.position[0]-a.position[0], + b.position[1]-a.position[1],b.position[2]-a.position[2]}; + const std::array bc={c.position[0]-b.position[0], + c.position[1]-b.position[1],c.position[2]-b.position[2]}; + const std::array cross={ab[1]*bc[2]-ab[2]*bc[1], + ab[2]*bc[0]-ab[0]*bc[2],ab[0]*bc[1]-ab[1]*bc[0]}; + const double twiceArea=std::sqrt(cross[0]*cross[0]+cross[1]*cross[1] + +cross[2]*cross[2]); + return twiceArea>1.0e-12 + ?distance(a,b)*distance(b,c)*distance(a,c)/(2.0*twiceArea) + :std::numeric_limits::infinity(); + }; + const auto normalize=[](std::array direction) + { + const double length=std::sqrt(direction[0]*direction[0] + +direction[1]*direction[1]+direction[2]*direction[2]); + if(length>1.0e-12) + for(double& component:direction)component/=length; + return direction; + }; + std::vector> frameTangents(stations.size()); + std::vector tangentSpacingTolerance(stations.size(),0.0); + for(std::size_t frame=0;frame incoming{},outgoing{},tangent{}; + if(frame>0) + for(std::size_t axis=0;axis<3;++axis) + incoming[axis]=stations[frame].position[axis] + -stations[frame-1].position[axis]; + if(frame+10&&frame+10) + tangentSpacingTolerance[frame]=0.5*(stations[frame].along + -stations[frame-1].along); + } + const auto isDoublyCritical=[&](std::size_t first,std::size_t second) + { + std::array delta{}; + for(std::size_t axis=0;axis<3;++axis) + delta[axis]=stations[second].position[axis] + -stations[first].position[axis]; + const auto alongTangent=[&](std::size_t frame) + { + return delta[0]*frameTangents[frame][0] + +delta[1]*frameTangents[frame][1] + +delta[2]*frameTangents[frame][2]; + }; + return std::abs(alongTangent(first))<=tangentSpacingTolerance[first] + &&std::abs(alongTangent(second))<=tangentSpacingTolerance[second]; + }; + // The signed coordinate to a route frame changes monotonically across a + // normal disk while kappa*S < 1, i.e. local support S is below curvature + // radius. Global uniqueness additionally requires separating doubly + // critical (mutually normal) centerline pairs by their support radii. + // Support S is bore radius plus the 4.335 m sweep/relief/corner reach. + const auto requiredAt=[&](std::size_t index) + { + return std::max({stations[index-1].radius,stations[index].radius, + stations[index+1].radius})+routeReachMetres+safetyMarginMetres; + }; + constexpr std::size_t MouthSearchControlSpans=8; + for(std::size_t index=1;index+1=requiredAt(index))continue; + ++result.tightMouthJoints; + const std::size_t span=std::min(stations[index].controlSpan, + routeEndControlIndex); + if(span<=MouthSearchControlSpans) + result.upperMouthLastControlSpan=std::max( + result.upperMouthLastControlSpan,span+1); + else if(span+MouthSearchControlSpans>=routeEndControlIndex) + result.lowerMouthFirstControlSpan=std::min( + result.lowerMouthFirstControlSpan,span>0?span-1:0); + } + + for(std::size_t first=0;first=0.0)continue; + const bool upperMouth=a.controlSpan<=MouthSearchControlSpans + &&b.controlSpan<=MouthSearchControlSpans; + const bool lowerMouth=a.controlSpan+MouthSearchControlSpans + >=routeEndControlIndex + &&b.controlSpan+MouthSearchControlSpans>=routeEndControlIndex; + if(upperMouth) + { + result.upperMouthLastControlSpan=std::max( + result.upperMouthLastControlSpan,b.controlSpan+1); + ++result.nonlocalMouthPairs; + } + else if(lowerMouth) + { + result.lowerMouthFirstControlSpan=std::min( + result.lowerMouthFirstControlSpan,a.controlSpan>0 + ?a.controlSpan-1:0); + ++result.nonlocalMouthPairs; + } + } + + if(result.upperMouthLastControlSpan>=result.lowerMouthFirstControlSpan) + { + result.upperMouthLastControlSpan=lastRouteSpan; + result.lowerMouthFirstControlSpan=lastRouteSpan; + } + for(std::size_t index=1;index+1=result.lowerMouthFirstControlSpan; + if(mouthOwned)continue; + const double radius=curvatureRadius(index); + result.minimumBodyCurvatureRadiusMetres=std::min( + result.minimumBodyCurvatureRadiusMetres,radius); + if(radius=required)continue; + if(!isDoublyCritical(first,second))continue; + const bool sameUpper=a.controlSpan<=result.upperMouthLastControlSpan + &&b.controlSpan<=result.upperMouthLastControlSpan; + const bool sameLower=a.controlSpan>=result.lowerMouthFirstControlSpan + &&b.controlSpan>=result.lowerMouthFirstControlSpan; + if(!sameUpper&&!sameLower)++result.bodySelfOverlapFailures; + } + struct FTestSlab + { + std::size_t first=0,end=0; + bool hasStartPlane=true,hasEndPlane=true; + }; + std::vector testSlabs; + const auto rebuildTestSlabs=[&]() + { + result.upperMouthEndSegment=std::min(result.upperMouthEndSegment, + segments.size()); + result.lowerMouthStartSegment=segments.size(); + for(std::size_t index=0;index=result.lowerMouthFirstControlSpan) + {result.lowerMouthStartSegment=index;break;} + result.upperMouthTransitionEndSegment=std::max( + result.upperMouthEndSegment,std::min(segments.size(), + result.upperMouthTransitionEndSegment)); + result.lowerMouthTransitionStartSegment=std::min( + result.lowerMouthStartSegment,std::max( + result.upperMouthTransitionEndSegment, + result.lowerMouthTransitionStartSegment)); + testSlabs.clear(); + if(result.upperMouthEndSegment>=result.lowerMouthStartSegment) + testSlabs.push_back({0,segments.size(),false,false}); + else + { + if(result.upperMouthEndSegment>0) + testSlabs.push_back({0,result.upperMouthEndSegment,false,true}); + if(result.upperMouthEndSegment& point) + { + if(slab.hasStartPlane) + { + const std::size_t frame=slab.first; + double side=0.0; + for(std::size_t axis=0;axis<3;++axis) + side+=(point[axis]-stations[frame].position[axis]) + *frameTangents[frame][axis]; + if(side<0.0)return false; + } + if(slab.hasEndPlane) + { + const std::size_t frame=slab.end; + double side=0.0; + for(std::size_t axis=0;axis<3;++axis) + side+=(point[axis]-stations[frame].position[axis]) + *frameTangents[frame][axis]; + if(side>=0.0)return false; + } + return true; + }; + const auto planeSide=[&](const std::array& point, + std::size_t frame) + { + double side=0.0; + for(std::size_t axis=0;axis<3;++axis) + side+=(point[axis]-stations[frame].position[axis]) + *frameTangents[frame][axis]; + return side; + }; + const auto influences=[&](const FTestSlab& slab, + const std::array& point) + { + for(std::size_t segment=slab.first;segment delta={b.position[0]-a.position[0], + b.position[1]-a.position[1],b.position[2]-a.position[2]}; + const double lengthSq=delta[0]*delta[0]+delta[1]*delta[1] + +delta[2]*delta[2]; + const std::array fromStart={point[0]-a.position[0], + point[1]-a.position[1],point[2]-a.position[2]}; + const double supportAtStart=a.radius+routeReachMetres+safetyMarginMetres; + const double supportDelta=b.radius-a.radius; + const auto supportsPoint=[&](double t) + { + const double dx=fromStart[0]-t*delta[0]; + const double dy=fromStart[1]-t*delta[1]; + const double dz=fromStart[2]-t*delta[2]; + const double support=supportAtStart+t*supportDelta; + return dx*dx+dy*dy+dz*dz<=support*support+1.0e-9; + }; + if(supportsPoint(0.0)||supportsPoint(1.0))return true; + const double quadratic=lengthSq-supportDelta*supportDelta; + if(quadratic>1.0e-12) + { + const double linear=-2.0*(fromStart[0]*delta[0] + +fromStart[1]*delta[1]+fromStart[2]*delta[2]) + -2.0*supportAtStart*supportDelta; + const double closest=std::clamp(-linear/(2.0*quadratic),0.0,1.0); + if(supportsPoint(closest))return true; + } + } + return false; + }; + const auto slabState=[&](const FTestSlab& slab, + const std::array& point) + { + const bool active=influences(slab,point); + return std::array{active,active&&owns(slab,point)}; + }; + const auto testOwnershipCrossSection=[&](const std::array& center, + const std::array& tangent,double boreRadius, + std::size_t probeIndex) + { + const double probeAlong=probeIndex reference=std::abs(tangent[2])<0.75 + ?std::array{0.0,0.0,1.0} + :std::array{0.0,1.0,0.0}; + std::array normalA={tangent[1]*reference[2]-tangent[2]*reference[1], + tangent[2]*reference[0]-tangent[0]*reference[2], + tangent[0]*reference[1]-tangent[1]*reference[0]}; + normalA=normalize(normalA); + const std::array normalB={tangent[1]*normalA[2]-tangent[2]*normalA[1], + tangent[2]*normalA[0]-tangent[0]*normalA[2], + tangent[0]*normalA[1]-tangent[1]*normalA[0]}; + const double support=boreRadius+routeReachMetres+safetyMarginMetres; + for(std::size_t ring=0;ring<3;++ring) + { + const double radius=support*static_cast(ring)*0.5; + const std::size_t directions=ring==0?1:8; + for(std::size_t directionIndex=0;directionIndex(directionIndex)/directions; + const double offsetA=radius*std::cos(angle); + const double offsetB=radius*std::sin(angle); + std::array point=center; + for(std::size_t axis=0;axis<3;++axis) + point[axis]+=offsetA*normalA[axis]+offsetB*normalB[axis]; + std::size_t claims=0; + std::size_t unownedSegment=std::numeric_limits::max(); + double unownedStartSide=0.0,unownedEndSide=0.0; + if(testSlabs.size()==1&&!testSlabs.front().hasStartPlane + &&!testSlabs.front().hasEndPlane) + { + // A whole-route composite has no ownership boundaries: the + // sampled point is within support of its center frame/segment, + // so that single unbounded slab owns it by construction. + claims=1; + } + else + { + const double firstAlong=probeAlong-arcWindow; + const double lastAlong=probeAlong+arcWindow; + const auto firstSlab=std::lower_bound(testSlabs.begin(), + testSlabs.end(),firstAlong,[&](const FTestSlab& slab, + double value) + { + return stations[slab.end].alongfirst].along<=lastAlong;++slab) + { + const auto state=slabState(*slab,point); + if(!state[0])continue; + if(state[1])++claims; + else if(unownedSegment==std::numeric_limits::max()) + { + unownedSegment=slab->first; + if(slab->hasStartPlane) + unownedStartSide=planeSide(point,slab->first); + if(slab->hasEndPlane) + unownedEndSide=planeSide(point,slab->end); + } + } + } + ++result.ownershipTestSamples; + if(claims==0)++result.zeroOwnerSamples; + else if(claims>1)++result.multipleOwnerSamples; + if(claims==0&&result.firstZeroOwnerProbe + ==std::numeric_limits::max()) + { + result.firstZeroOwnerProbe=probeIndex; + result.firstZeroOwnerPoint=point; + result.firstZeroOwnerSegment=unownedSegment; + result.firstZeroOwnerStartSide=unownedStartSide; + result.firstZeroOwnerEndSide=unownedEndSide; + } + } + } + }; + const std::size_t initialUpperMouthLastSpan=result.upperMouthLastControlSpan; + const std::size_t initialLowerMouthFirstSpan=result.lowerMouthFirstControlSpan; + result.upperTransitionLastControlSpan=initialUpperMouthLastSpan; + const auto segmentAfterSpan=[&](std::size_t span) + { + std::size_t end=0; + while(end=initialLowerMouthFirstSpan) + {result.lowerMouthTransitionStartSegment=segment;break;} + for(std::size_t attempt=0;attempt<=naturalUpperMouthEndSegment;++attempt) + { + result.upperMouthEndSegment=naturalUpperMouthEndSegment-attempt; + result.upperMouthLastControlSpan=result.upperMouthEndSegment>0 + ?segments[result.upperMouthEndSegment-1].controlSpan:0; + result.lowerMouthFirstControlSpan=initialLowerMouthFirstSpan; + result.ownershipTestSamples=0; + result.zeroOwnerSamples=0; + result.multipleOwnerSamples=0; + result.firstZeroOwnerProbe=std::numeric_limits::max(); + result.firstZeroOwnerSegment=std::numeric_limits::max(); + rebuildTestSlabs(); + for(std::size_t frame=0;frame center{},tangent{}; + for(std::size_t axis=0;axis<3;++axis) + { + center[axis]=(stations[segment].position[axis] + +stations[segment+1].position[axis])*0.5; + tangent[axis]=frameTangents[segment][axis] + +frameTangents[segment+1][axis]; + } + tangent=normalize(tangent); + if(tangent[0]==0.0&&tangent[1]==0.0&&tangent[2]==0.0) + tangent=frameTangents[segment]; + testOwnershipCrossSection(center,tangent, + 0.5*(stations[segment].radius+stations[segment+1].radius), + stations.size()+segment); + } + if(result.zeroOwnerSamples==0&&result.multipleOwnerSamples==0)break; + } + const std::size_t controlFrame=std::clamp( + result.upperMouthEndSegment,1,segments.size()-1); + std::array negativeControlPoint=stations[controlFrame].position; + for(std::size_t axis=0;axis<3;++axis) + negativeControlPoint[axis]-=0.1*frameTangents[controlFrame][axis]; + const std::vector duplicateControlSlabs={ + {0,controlFrame,false,true},{controlFrame,controlFrame+1,false,true}}; + const std::vector gapControlSlabs={ + {controlFrame,controlFrame+1,true,false}}; + const auto ownerCount=[&](const std::vector& slabs, + const std::array& point) + { + std::size_t claims=0; + for(const auto& slab:slabs) + { + const auto state=slabState(slab,point); + if(state[0]&&state[1])++claims; + } + return claims; + }; + result.ownershipNegativeControlsPass= + ownerCount(duplicateControlSlabs,negativeControlPoint)==2 + &&ownerCount(gapControlSlabs,negativeControlPoint)==0; + result.bodyCurvatureFailures=0; + result.bodySelfOverlapFailures=0; + result.minimumBodyCurvatureRadiusMetres= + std::numeric_limits::infinity(); + for(std::size_t index=1;index+1=result.lowerMouthFirstControlSpan)continue; + const double radius=curvatureRadius(index); + result.minimumBodyCurvatureRadiusMetres=std::min( + result.minimumBodyCurvatureRadiusMetres,radius); + if(radius=required + ||!isDoublyCritical(first,second))continue; + const bool sameUpper=a.controlSpan<=result.upperTransitionLastControlSpan + &&b.controlSpan<=result.upperTransitionLastControlSpan; + const bool sameLower=a.controlSpan>=result.lowerMouthFirstControlSpan + &&b.controlSpan>=result.lowerMouthFirstControlSpan; + if(!sameUpper&&!sameLower)++result.bodySelfOverlapFailures; + } + result.usesWholeRouteCompositeSlab=result.bodyCurvatureFailures>0 + ||result.bodySelfOverlapFailures>0||result.zeroOwnerSamples>0 + ||result.multipleOwnerSamples>0; + if(result.usesWholeRouteCompositeSlab) + { + // Pathological legacy routes can fold back across themselves. Keep their + // established union field by assigning the complete route to one owner; + // the normal path retains one slab per safe body segment. + testSlabs.assign(1,FTestSlab{0,segments.size(),false,false}); + result.ownershipTestSamples=0; + result.zeroOwnerSamples=0; + result.multipleOwnerSamples=0; + result.firstZeroOwnerProbe=std::numeric_limits::max(); + result.firstZeroOwnerSegment=std::numeric_limits::max(); + for(std::size_t frame=0;frame center{},tangent{}; + for(std::size_t axis=0;axis<3;++axis) + { + center[axis]=(stations[segment].position[axis] + +stations[segment+1].position[axis])*0.5; + tangent[axis]=frameTangents[segment][axis] + +frameTangents[segment+1][axis]; + } + tangent=normalize(tangent); + if(tangent[0]==0.0&&tangent[1]==0.0&&tangent[2]==0.0) + tangent=frameTangents[segment]; + testOwnershipCrossSection(center,tangent, + 0.5*(stations[segment].radius+stations[segment+1].radius), + stations.size()+segment); + } + } + if(result.upperMouthEndSegment>=result.lowerMouthStartSegment) + result.usesWholeRouteCompositeSlab=true; + result.valid=(result.usesWholeRouteCompositeSlab + ||(result.bodyCurvatureFailures==0 + &&result.bodySelfOverlapFailures==0)) + &&result.zeroOwnerSamples==0&&result.multipleOwnerSamples==0 + &&result.ownershipNegativeControlsPass; + return result; + } + + struct FSlabAuditKeyHash + { + std::size_t operator()(const std::vector& key) const noexcept + { + std::uint64_t hash=1469598103934665603ULL; + for(const std::uint64_t value:key) + { + hash^=value; + hash*=1099511628211ULL; + } + return static_cast(hash); + } + }; + + inline FSlabReachAudit AuditSlabReach(std::span segments, + std::size_t routeEndControlIndex,double unitsPerMetre,double routeReachMetres, + double safetyMarginMetres=0.05) + { + using FCache=std::unordered_map, + FSlabReachAudit,FSlabAuditKeyHash>; + static FCache cache; + static std::mutex cacheMutex; + std::vector key; + key.reserve(4+segments.size()*11); + key.push_back(static_cast(routeEndControlIndex)); + key.push_back(std::bit_cast(unitsPerMetre)); + key.push_back(std::bit_cast(routeReachMetres)); + key.push_back(std::bit_cast(safetyMarginMetres)); + for(const auto& segment:segments) + { + for(const double value:segment.a) + key.push_back(std::bit_cast(value)); + for(const double value:segment.b) + key.push_back(std::bit_cast(value)); + key.push_back(std::bit_cast(segment.alongMetres)); + key.push_back(std::bit_cast(segment.lengthMetres)); + key.push_back(static_cast(segment.controlSpan)); + } + { + std::lock_guard lock(cacheMutex); + if(const auto found=cache.find(key);found!=cache.end())return found->second; + } + + const FSlabReachAudit result=ComputeSlabReachAudit(segments, + routeEndControlIndex,unitsPerMetre,routeReachMetres,safetyMarginMetres); + { + std::lock_guard lock(cacheMutex); + constexpr std::size_t MaximumCachedRoutes=32; + if(cache.size()>=MaximumCachedRoutes)cache.clear(); + const auto [found,inserted]=cache.emplace(std::move(key),result); + if(!inserted)return found->second; + } + return result; + } } diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index dc4ca32..bdc3910 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -494,7 +494,8 @@ namespace VoxelForgeCore // Version 223 projects the floor onto one continuous arc-length profile and groups // dense passage samples into coarse tile candidates. Version 224 keeps the same // continuous floor while relaxing the spline chord tolerance and bounding shell work. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 225; + // Version 226 assigns route samples to reach-bounded slabs, with explicit mouth ownership. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 226; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 5fcb719..fc570c7 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -900,19 +900,22 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error) } const std::int64_t playerCenterAboveFloorQ=static_cast(std::llround( 1.13*WorldAssemblySubvoxelsPerVoxel*1000000.0/coreVoxelSizeMicrometres)); - for(auto& passage:passages) + const auto lowerMouthIndexFor=[&](const ResolvedLayoutPassageV1& passage) { - passage.lowerMouthControlIndex=passage.controlPoints.empty()?0:passage.controlPoints.size()-1; - for(std::size_t i=1;i issues; @@ -3239,6 +3242,34 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]=lowerMouth[2] +passage.controlPoints[lowerMouthIndex].radiusSubvoxels -playerCenterAboveFloorQ; + std::vector slabControls; + slabControls.reserve(passage.controlPoints.size()); + for(const auto& point:passage.controlPoints) + slabControls.push_back({static_cast(point.positionSubvoxels[0]), + static_cast(point.positionSubvoxels[1]), + static_cast(point.positionSubvoxels[2]), + static_cast(point.radiusSubvoxels)}); + constexpr double PassageSlabReachMetres=4.0+0.21+0.125; + const auto slabSegments=VoxelForgeCore::PassageSpline::BuildSegments( + slabControls,slabControls.size()-1,8.0,qPerMetre,{},0.100); + const auto slabAudit=VoxelForgeCore::PassageSpline::AuditSlabReach( + slabSegments,lowerMouthIndex,qPerMetre,PassageSlabReachMetres); + std::printf("[PassagePlanSlabAudit] id=%s frames=%zu reach_m=%.3f upper_mouth_spans=%zu lower_mouth_spans=%zu upper_end_segment=%zu lower_start_segment=%zu tight_mouth_joints=%zu nonlocal_mouth_pairs=%zu body_min_curvature_m=%.3f body_min_self_gap_m=%.3f body_curvature_failures=%zu body_self_overlap_failures=%zu ownership_test_samples=%zu zero_owners=%zu multiple_owners=%zu ownership_mode=%s negative_controls=%s status=%s\n", + passage.stableId.c_str(),slabSegments.size()+1,PassageSlabReachMetres, + slabAudit.upperMouthLastControlSpan+1, + slabAudit.lowerMouthFirstControlSpan<=lowerMouthIndex + ?lowerMouthIndex-slabAudit.lowerMouthFirstControlSpan+1:0, + slabAudit.upperMouthEndSegment,slabAudit.lowerMouthStartSegment, + slabAudit.tightMouthJoints,slabAudit.nonlocalMouthPairs, + slabAudit.minimumBodyCurvatureRadiusMetres, + slabAudit.minimumBodySelfGapMetres,slabAudit.bodyCurvatureFailures, + slabAudit.bodySelfOverlapFailures,slabAudit.ownershipTestSamples, + slabAudit.zeroOwnerSamples,slabAudit.multipleOwnerSamples, + slabAudit.usesWholeRouteCompositeSlab?"whole-route-composite":"per-segment", + slabAudit.ownershipNegativeControlsPass?"PASS":"FAIL", + slabAudit.valid?"PASS":"FAIL"); + if(!slabAudit.valid) + {error=passage.stableId+" route cannot form unique reach-bounded ownership slabs";return false;} layout.passages.push_back(std::move(passage)); } } @@ -3268,9 +3299,10 @@ ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3() auto base=ResolvedLayoutSnapshotV3{};base.worldIdentity="synthetic";base.ownerVoxelSizeMicrometres=125000; base.coreVoxelSizeMicrometres=250000;base.strates={make("a",0,10000000,0),make("b",1,-2000000,-12000000)}; ResolvedLayoutPassageConfigV1 passageConfig;passageConfig.upperStrateIndex=0;passageConfig.connections=1; - passageConfig.mouthRadiusSubvoxels=1280;passageConfig.midRadiusSubvoxels=1280; + passageConfig.style=0;passageConfig.mouthRadiusSubvoxels=1280; + passageConfig.midRadiusSubvoxels=1280; passageConfig.reachMinimumSubvoxels=2048;passageConfig.reachMaximumSubvoxels=2048; - passageConfig.distanceMinimumSubvoxels=1024;passageConfig.distanceMaximumSubvoxels=10240; + passageConfig.distanceMinimumSubvoxels=1024;passageConfig.distanceMaximumSubvoxels=64*1024; std::string planError; if(!PlanResolvedLayoutPassagesV1(base,19,{passageConfig},planError)) result.failures.push_back({"synthetic_passage_plan_failed",planError}); diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 517f375..229f57d 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -546,13 +546,13 @@ public: return it==ResolvedLayout->strates.end()?nullptr:&*it; } - void BindResolvedStratumFields() + bool BindResolvedStratumFields(std::string& Error) { BoundStratumFields.clear(); BoundManagerLayoutChunkWidthVoxels=Owner&&Owner->Manager ?Owner->Manager->GetStrateLayoutChunkWidthVoxels():0.0f; if(!ResolvedLayout||ResolvedLayout->strateLayoutChunkSizeMicrometres<=0) - {BuildPassageComposerConstants();return;} + return BuildPassageComposerConstants(Error); const int64 ChunkWidthMicrometres=ResolvedLayout->strateLayoutChunkSizeMicrometres; const auto FloorDiv64=[](int64 Value,int64 Divisor) { @@ -637,7 +637,7 @@ public: std::sort(BoundStratumFields.begin(),BoundStratumFields.end(), [](const auto& A,const auto& B){return A.BottomChunkZ(1,ResolvedLayout->coreVoxelSizeMicrometres); const double qx=static_cast(X)*Q,qy=static_cast(Y)*Q, qz=static_cast(Z)*Q; + const std::array passageQueryPoint={qx,qy,qz}; const double shellThickness=FloorShellThicknessMetres*qPerMetre; const auto* passageTileQueries=ActivePassageTileQueries; const bool passageTileScopeMatches=passageTileQueries @@ -1289,8 +1290,13 @@ public: double bestFloorCenterDistanceSq=std::numeric_limits::infinity(); const auto visitSegment=[&](std::size_t segmentIndex,int) { - if(passageProfile)++passageProfile->passageCarveSegmentVisits; const auto& segment=PassageSegmentData[passageIndex][segmentIndex]; + const auto& ownershipSlabs=PassageOwnershipSlabs[passageIndex]; + if(segment.ownershipSlabIndex>=ownershipSlabs.size() + ||!PassageOwnershipSlabOwns( + ownershipSlabs[segment.ownershipSlabIndex],passageQueryPoint)) + return; + if(passageProfile)++passageProfile->passageCarveSegmentVisits; if(segment.lengthSq<=0.0)return; const double projection=(qx-segment.a[0])*segment.delta[0] +(qy-segment.a[1])*segment.delta[1] @@ -1303,10 +1309,7 @@ public: const double ex=qx-cx,ey=qy-cy,ez=qz-cz; const double roundDistance=DeterministicSqrt( ex*ex+ey*ey+ez*ez)-radius; - const double floorT=segment.horizontalLengthSq>1.0e-12 - ?(((qx-segment.a[0])*segment.delta[0] - +(qy-segment.a[1])*segment.delta[1]) - /segment.horizontalLengthSq):t; + const double floorT=PassageOwnershipFloorT(segment,passageQueryPoint); const double floorTClamped=std::clamp(floorT,0.0,1.0); const double floorCenterDistanceSq=ex*ex+ey*ey+ez*ez; const double floorRadius=segment.radiusA @@ -1363,6 +1366,11 @@ public: const auto visitShellSegment=[&](std::size_t segmentIndex,int) { const auto& segment=PassageSegmentData[passageIndex][segmentIndex]; + const auto& ownershipSlabs=PassageOwnershipSlabs[passageIndex]; + if(segment.ownershipSlabIndex>=ownershipSlabs.size() + ||!PassageOwnershipSlabOwns( + ownershipSlabs[segment.ownershipSlabIndex],passageQueryPoint)) + return; if(segment.lengthSq<=0.0)return; const double projection=(qx-segment.a[0])*segment.delta[0] +(qy-segment.a[1])*segment.delta[1] @@ -1374,10 +1382,7 @@ public: const double radius=segment.radiusA+t*segment.radiusDelta; const double ex=qx-cx,ey=qy-cy,ez=qz-cz; const double roundDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius; - const double floorT=segment.horizontalLengthSq>1.0e-12 - ?(((qx-segment.a[0])*segment.delta[0] - +(qy-segment.a[1])*segment.delta[1]) - /segment.horizontalLengthSq):t; + const double floorT=PassageOwnershipFloorT(segment,passageQueryPoint); const double floorTClamped=std::clamp(floorT,0.0,1.0); const auto& segments=PassageSegmentData[passageIndex]; const double floorRadius=segment.radiusA @@ -2235,7 +2240,7 @@ public: Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels; bUseNativeManifestFields=true; UnbinnedSpatialPrimitiveIds.clear(); - BindResolvedStratumFields(); + if(!BindResolvedStratumFields(Error))return false; bAnalyticOnly=false; bProfileDetailBand=bInProfileDetailBand; bParallelSignEdgeRepair=bInParallelSignEdgeRepair; @@ -2441,7 +2446,7 @@ public: // Analytic pages retain their bounded owner bins for route bookkeeping. Once published, // the Core world binds each page to its base shape as the single density evaluator. bUseNativeManifestFields=true; - BindResolvedStratumFields(); + if(!BindResolvedStratumFields(Error))return false; RoundTunnelsEnabled=bInRoundTunnels;bAnalyticOnly=true;bProfileDetailBand=false; for(int32 A=0;A<3;++A) { @@ -2478,8 +2483,11 @@ public: { Owner=&World;WorldAssembly=InWorldAssembly;ResolvedLayout=InResolvedLayout; NativeOnlyMinimum=InMinimum;NativeOnlyMaximum=InMaximum; - bUseNativeManifestFields=true;bNativeOnly=true;bAnalyticOnly=true;Built=true; - BindResolvedStratumFields(); + bUseNativeManifestFields=true;bNativeOnly=true;bAnalyticOnly=true; + std::string ownershipError; + if(!BindResolvedStratumFields(ownershipError)) + {bNativeOnly=false;bAnalyticOnly=false;Built=false;return;} + Built=true; NativeOnlyViolations.store(0,std::memory_order_relaxed); } @@ -3422,6 +3430,9 @@ private: double floorDepthMetres=0.0,totalPathLengthMetres=0.0; double authoredTotalPathLengthMetres=0.0; std::size_t routeProfileSegmentCount=0; + std::size_t upperMouthEndSegment=0,upperMouthTransitionEndSegment=0; + std::size_t lowerMouthStartSegment=0,lowerMouthTransitionStartSegment=0; + bool wholeRouteCompositeSlab=false; double upperMouthX=0.0,upperMouthY=0.0; double upperMouthPlannedFloor=0.0; double upperMouthTangentX=0.0,upperMouthTangentY=0.0; @@ -3431,14 +3442,114 @@ private: struct FPassageSegmentData { std::array a{},b{},delta{}; + std::array frameStartTangent{},frameEndTangent{}; std::array sweepMinimumSubvoxels{},sweepMaximumSubvoxels{}; double authoredFloorZA=0.0,authoredFloorZDelta=0.0; double authoredAlongMetres=0.0,authoredLengthMetres=0.0; double radiusA=0.0,radiusB=0.0,radiusDelta=0.0; double lengthSq=0.0,horizontalLengthSq=0.0; double lengthSubvoxels=0.0,horizontalLength=0.0; + std::size_t controlSpan=0,ownershipSlabIndex=0; bool routeSegment=false; }; + struct FPassageOwnershipSlab + { + std::array startPosition{},endPosition{}; + std::array startTangent{},endTangent{}; + std::size_t firstSegment=0,endSegment=0; + bool hasStartPlane=true,hasEndPlane=true; + }; + static double PassageOwnershipPlaneSide( + const std::array& point,const std::array& origin, + const std::array& tangent) + { + return (point[0]-origin[0])*tangent[0] + +(point[1]-origin[1])*tangent[1] + +(point[2]-origin[2])*tangent[2]; + } + static bool PassageOwnershipSlabOwns(const FPassageOwnershipSlab& slab, + const std::array& point) + { + return (!slab.hasStartPlane + ||PassageOwnershipPlaneSide(point,slab.startPosition, + slab.startTangent)>=0.0) + &&(!slab.hasEndPlane + ||PassageOwnershipPlaneSide(point,slab.endPosition, + slab.endTangent)<0.0); + } + static bool PassageClipBoundsToOwnershipPlane(std::array& minimum, + std::array& maximum,const std::array& origin, + const std::array& tangent,bool keepPositive) + { + std::array,8> corners{}; + std::array sides{}; + for(std::size_t mask=0;mask clippedMinimum{ + std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + std::numeric_limits::infinity()}; + std::array clippedMaximum{ + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + bool hasPoint=false; + const auto include=[&](const std::array& point) + { + hasPoint=true; + for(std::size_t axis=0;axis<3;++axis) + { + clippedMinimum[axis]=std::min(clippedMinimum[axis],point[axis]); + clippedMaximum[axis]=std::max(clippedMaximum[axis],point[axis]); + } + }; + const auto inside=[&](double side) + {return keepPositive?side>=0.0:side<=0.0;}; + for(std::size_t corner=0;corner0.0) + ||(firstSide>0.0&&secondSide<0.0)))continue; + const double t=firstSide/(firstSide-secondSide); + std::array intersection{}; + for(std::size_t component=0;component<3;++component) + intersection[component]=std::lerp(corners[first][component], + corners[second][component],t); + include(intersection); + } + if(!hasPoint)return false; + minimum=clippedMinimum; + maximum=clippedMaximum; + return true; + } + static double PassageOwnershipFloorT(const FPassageSegmentData& segment, + const std::array& point) + { + // Route floors are authored against horizontal distance along the path. + // Ownership remains a 3D plane decision; use XY projection here so a + // downhill floor point's vertical offset cannot move h(s) along the route. + if(segment.horizontalLengthSq>1.0e-12) + return ((point[0]-segment.a[0])*segment.delta[0] + +(point[1]-segment.a[1])*segment.delta[1]) + /segment.horizontalLengthSq; + const double fromStart=PassageOwnershipPlaneSide(point,segment.a, + segment.frameStartTangent); + const double fromEnd=PassageOwnershipPlaneSide(point,segment.b, + segment.frameEndTangent); + const double denominator=fromStart-fromEnd; + if(std::abs(denominator)<=1.0e-12)return 0.0; + return fromStart/denominator; + } static double PassageFlatFloorDepthSubvoxels( const FPassageComposerConstants& constants,std::size_t segmentIndex,double t, double radiusSubvoxels,double qPerMetre) @@ -4587,13 +4698,14 @@ private: return result; } - void BuildPassageComposerConstants() + bool BuildPassageComposerConstants(std::string& Error) { const bool tracePreparation=VF_IsL98PassagePrepareTraceEnabled(); const auto preparationStart=tracePreparation ?std::chrono::steady_clock::now():std::chrono::steady_clock::time_point{}; PassageComposerConstants.clear(); PassageSegmentData.clear(); + PassageOwnershipSlabs.clear(); PassageSegmentOffsets.clear(); if(!ResolvedLayout) { @@ -4601,10 +4713,11 @@ private: std::printf("[L113PassagePrepare] passages=0 control_spans=0 segments=0 max_spacing_m=8.00 max_chord_error_m=0.025 bytes=0 prepare_ms=%.3f\n", std::chrono::duration( std::chrono::steady_clock::now()-preparationStart).count()); - return; + return true; } PassageComposerConstants.resize(ResolvedLayout->passages.size()); PassageSegmentData.resize(ResolvedLayout->passages.size()); + PassageOwnershipSlabs.resize(ResolvedLayout->passages.size()); PassageSegmentOffsets.resize(ResolvedLayout->passages.size()+1,0); constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double qPerMetre=Q*1000000.0/ @@ -4670,6 +4783,39 @@ private: const auto sampledSegments=VoxelForgeCore::PassageSpline::BuildSegments( controls,controls.size()-1,PassageSegmentSpacingMetres,qPerMetre, plannedFloorProfile,PassageSegmentChordErrorMetres); + constexpr double PassageSlabReachMetres=4.0+0.21+0.125; + const auto slabAudit=VoxelForgeCore::PassageSpline::AuditSlabReach( + sampledSegments,routeEnd,qPerMetre,PassageSlabReachMetres); + constants.upperMouthEndSegment=slabAudit.upperMouthEndSegment; + constants.upperMouthTransitionEndSegment= + slabAudit.upperMouthTransitionEndSegment; + constants.lowerMouthStartSegment=slabAudit.lowerMouthStartSegment; + constants.lowerMouthTransitionStartSegment= + slabAudit.lowerMouthTransitionStartSegment; + constants.wholeRouteCompositeSlab=slabAudit.usesWholeRouteCompositeSlab; + std::printf("[PassagePreparedSlabAudit] id=%s frames=%zu reach_m=%.3f upper_end_segment=%zu upper_transition_end_segment=%zu lower_transition_start_segment=%zu lower_start_segment=%zu tight_mouth_joints=%zu nonlocal_mouth_pairs=%zu body_min_curvature_m=%.3f body_min_self_gap_m=%.3f body_curvature_failures=%zu body_self_overlap_failures=%zu ownership_test_samples=%zu zero_owners=%zu multiple_owners=%zu ownership_mode=%s negative_controls=%s first_zero_probe=%zu first_zero_segment=%zu first_zero_point_m=(%.3f,%.3f,%.3f) first_zero_plane_side_m=(%.3f,%.3f) status=%s\n", + ResolvedLayout->passages[passageIndex].stableId.c_str(), + sampledSegments.size()+1,PassageSlabReachMetres, + slabAudit.upperMouthEndSegment,slabAudit.upperMouthTransitionEndSegment, + slabAudit.lowerMouthTransitionStartSegment,slabAudit.lowerMouthStartSegment, + slabAudit.tightMouthJoints,slabAudit.nonlocalMouthPairs, + slabAudit.minimumBodyCurvatureRadiusMetres, + slabAudit.minimumBodySelfGapMetres,slabAudit.bodyCurvatureFailures, + slabAudit.bodySelfOverlapFailures,slabAudit.ownershipTestSamples, + slabAudit.zeroOwnerSamples,slabAudit.multipleOwnerSamples, + slabAudit.usesWholeRouteCompositeSlab?"whole-route-composite":"per-segment", + slabAudit.ownershipNegativeControlsPass?"PASS":"FAIL", + slabAudit.firstZeroOwnerProbe,slabAudit.firstZeroOwnerSegment, + slabAudit.firstZeroOwnerPoint[0],slabAudit.firstZeroOwnerPoint[1], + slabAudit.firstZeroOwnerPoint[2],slabAudit.firstZeroOwnerStartSide, + slabAudit.firstZeroOwnerEndSide, + slabAudit.valid?"PASS":"FAIL"); + if(!slabAudit.valid) + { + Error=ResolvedLayout->passages[passageIndex].stableId + +" route does not have unique reach-bounded ownership slabs"; + return false; + } constexpr double MaximumFloorGradeDegrees=15.0; constexpr double DegreesPerRadian=180.0/3.14159265358979323846; double maximumFloorGradeDegrees=0.0; @@ -4739,6 +4885,7 @@ private: for(const auto& raw:rawSegments) { FPassageSegmentData data; + data.controlSpan=raw.controlSpan; data.authoredFloorZA=raw.plannedFloorA; data.authoredFloorZDelta=raw.plannedFloorB-raw.plannedFloorA; data.authoredAlongMetres=raw.alongMetres; @@ -4794,6 +4941,7 @@ private: preparedSegments*sizeof(FPassageSegmentData), std::chrono::duration( std::chrono::steady_clock::now()-preparationStart).count()); + return true; } void BuildPassageSpatialIndices() @@ -4823,6 +4971,78 @@ private: { const std::size_t segmentCount=PassageSegmentData[passageIndex].size(); if(segmentCount==0)continue; + auto& segments=PassageSegmentData[passageIndex]; + auto& slabs=PassageOwnershipSlabs[passageIndex]; + std::vector> frameTangents(segmentCount+1); + const auto normalized=[](std::array value) + { + const double length=std::sqrt(value[0]*value[0]+value[1]*value[1] + +value[2]*value[2]); + if(length>1.0e-12) + for(double& component:value)component/=length; + return value; + }; + for(std::size_t frame=0;frame<=segmentCount;++frame) + { + const std::array incoming=frame>0 + ?normalized(segments[frame-1].delta):std::array{}; + const std::array outgoing=frame{}; + std::array tangent{}; + if(frame==0)tangent=outgoing; + else if(frame==segmentCount)tangent=incoming; + else + for(std::size_t axis=0;axis<3;++axis) + tangent[axis]=incoming[axis]+outgoing[axis]; + tangent=normalized(tangent); + if(tangent[0]==0.0&&tangent[1]==0.0&&tangent[2]==0.0) + tangent=frame=end)return; + FPassageOwnershipSlab slab; + slab.firstSegment=first;slab.endSegment=end; + slab.hasStartPlane=first>0; + slab.hasEndPlane=end=lowerMouthStart) + addOwnershipSlab(0,segmentCount); + else + { + addOwnershipSlab(0,upperMouthEnd); + addOwnershipSlab(upperMouthEnd,upperTransitionEnd); + for(std::size_t segment=upperTransitionEnd; + segment segmentIds(segmentCount); for(std::size_t i=0;i1.0e-9) + { + std::array direction=prepared.delta; + const double inverseLength=1.0/prepared.lengthSubvoxels; + for(double& component:direction)component*=inverseLength; + const auto tangentCosine=[&](const std::array& tangent) + { + return std::clamp(direction[0]*tangent[0] + +direction[1]*tangent[1]+direction[2]*tangent[2],-1.0,1.0); + }; + const double startCos=tangentCosine(prepared.frameStartTangent); + const double endCos=tangentCosine(prepared.frameEndTangent); + if(startCos>1.0e-6&&endCos>1.0e-6) + { + const double alongExtra=localExtent*std::max( + std::sqrt(std::max(0.0,1.0-startCos*startCos))/startCos, + std::sqrt(std::max(0.0,1.0-endCos*endCos))/endCos); + for(std::size_t axis=0;axis<3;++axis) + { + axisExtent=localExtent*std::sqrt(std::max(0.0, + 1.0-direction[axis]*direction[axis])) + +alongExtra*std::abs(direction[axis])+0.11; + prepared.sweepMinimumSubvoxels[axis]= + static_cast(std::floor( + (localMinimum[axis]-axisExtent)*Q)); + prepared.sweepMaximumSubvoxels[axis]= + static_cast(std::ceil( + (localMaximum[axis]+axisExtent)*Q)); + } + hasTightSweepBounds=true; + } + } + if(!hasTightSweepBounds) + { + for(std::size_t axis=0;axis<3;++axis) + { + prepared.sweepMinimumSubvoxels[axis]=static_cast( + std::floor((localMinimum[axis]-localExtent)*Q)); + prepared.sweepMaximumSubvoxels[axis]=static_cast( + std::ceil((localMaximum[axis]+localExtent)*Q)); + } + } + const auto& slab=slabs[prepared.ownershipSlabIndex]; + std::array clippedMinimum{},clippedMaximum{}; for(std::size_t axis=0;axis<3;++axis) { - prepared.sweepMinimumSubvoxels[axis]=static_cast( - std::floor((localMinimum[axis]-localExtent)*Q)); - prepared.sweepMaximumSubvoxels[axis]=static_cast( - std::ceil((localMaximum[axis]+localExtent)*Q)); + clippedMinimum[axis]=static_cast( + prepared.sweepMinimumSubvoxels[axis]); + clippedMaximum[axis]=static_cast( + prepared.sweepMaximumSubvoxels[axis]); + } + bool clipped=true; + if(slab.hasStartPlane) + clipped=PassageClipBoundsToOwnershipPlane(clippedMinimum, + clippedMaximum,slab.startPosition,slab.startTangent,true); + if(clipped&&slab.hasEndPlane) + clipped=PassageClipBoundsToOwnershipPlane(clippedMinimum, + clippedMaximum,slab.endPosition,slab.endTangent,false); + if(!clipped) + { + prepared.sweepMinimumSubvoxels={1,1,1}; + prepared.sweepMaximumSubvoxels={0,0,0}; + } + else + { + for(std::size_t axis=0;axis<3;++axis) + { + prepared.sweepMinimumSubvoxels[axis]= + static_cast(std::floor(clippedMinimum[axis])); + prepared.sweepMaximumSubvoxels[axis]= + static_cast(std::ceil(clippedMaximum[axis])); + } } } const double routeExtent=maximumRadiusSubvoxels/Q+requiredReach+0.01; @@ -11433,6 +11724,7 @@ private: std::vector> PassageNearestNodes; std::vector PassageComposerConstants; std::vector> PassageSegmentData; + std::vector> PassageOwnershipSlabs; std::vector PassageSegmentOffsets; std::vector PassageProofBounds; std::vector PassageProofNodes; @@ -11507,6 +11799,7 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa passageSamples.samples.resize(gridSampleCount); const auto& constants=shape_->PassageComposerConstants[passageIndex]; const auto& segments=shape_->PassageSegmentData[passageIndex]; + const auto& ownershipSlabs=shape_->PassageOwnershipSlabs[passageIndex]; for(const auto& candidate:candidates) { if(candidate.segmentIndex>=segments.size())continue; @@ -11571,11 +11864,86 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa if(segmentFirst[0]>segmentLast[0] ||segmentFirst[1]>segmentLast[1] ||segmentFirst[2]>segmentLast[2])continue; - for(std::int32_t gz=segmentFirst[2];gz<=segmentLast[2];++gz) + if(segment.ownershipSlabIndex>=ownershipSlabs.size())continue; + const auto& ownershipSlab=ownershipSlabs[segment.ownershipSlabIndex]; + std::size_t scanAxis=0; + for(std::size_t axis=1;axis<3;++axis) { - for(std::int32_t gy=segmentFirst[1];gy<=segmentLast[1];++gy) - for(std::int32_t gx=segmentFirst[0];gx<=segmentLast[0];++gx) + const double currentScore=std::abs(ownershipSlab.startTangent[axis]) + +std::abs(ownershipSlab.endTangent[axis]); + const double bestScore=std::abs(ownershipSlab.startTangent[scanAxis]) + +std::abs(ownershipSlab.endTangent[scanAxis]); + if(currentScore>bestScore)scanAxis=axis; + } + const std::size_t rowAxisA=(scanAxis+1)%3; + const std::size_t rowAxisB=(scanAxis+2)%3; + for(std::int32_t rowB=segmentFirst[rowAxisB]; + rowB<=segmentLast[rowAxisB];++rowB) + { + for(std::int32_t rowA=segmentFirst[rowAxisA]; + rowA<=segmentLast[rowAxisA];++rowA) { + std::array gridCoordinates{}; + gridCoordinates[rowAxisA]=rowA; + gridCoordinates[rowAxisB]=rowB; + std::int32_t scanFirst=segmentFirst[scanAxis]; + std::int32_t scanLast=segmentLast[scanAxis]; + const auto constrainPlane=[&]( + const std::array& origin, + const std::array& tangent,bool keepPositive) + { + double intercept=-origin[scanAxis]*tangent[scanAxis]; + for(std::size_t axis=0;axis<3;++axis) + { + if(axis==scanAxis)continue; + const double coordinate=static_cast( + sweep->gridMinimumSubvoxels[axis] + +static_cast(gridCoordinates[axis]) + *sweep->stepSubvoxels); + intercept+=(coordinate-origin[axis])*tangent[axis]; + } + const double direction=tangent[scanAxis]; + if(direction==0.0) + { + if(keepPositive?intercept<0.0:intercept>=0.0) + {scanFirst=1;scanLast=0;} + return; + } + const double boundary=-intercept/direction; + const double relative=(boundary + -sweep->gridMinimumSubvoxels[scanAxis]) + /sweep->stepSubvoxels; + const double clampedRelative=std::clamp(relative,-1.0, + static_cast(sweep->dimension)+1.0); + if(keepPositive&&direction>0.0) + scanFirst=std::max(scanFirst, + static_cast( + std::ceil(clampedRelative))-1); + else if(keepPositive) + scanLast=std::min(scanLast, + static_cast( + std::floor(clampedRelative))+1); + else if(direction>0.0) + scanLast=std::min(scanLast, + static_cast( + std::ceil(clampedRelative))); + else + scanFirst=std::max(scanFirst, + static_cast( + std::floor(clampedRelative))); + }; + if(ownershipSlab.hasStartPlane) + constrainPlane(ownershipSlab.startPosition, + ownershipSlab.startTangent,true); + if(scanFirst<=scanLast&&ownershipSlab.hasEndPlane) + constrainPlane(ownershipSlab.endPosition, + ownershipSlab.endTangent,false); + for(std::int32_t scan=scanFirst;scan<=scanLast;++scan) + { + gridCoordinates[scanAxis]=scan; + const std::int32_t gx=gridCoordinates[0]; + const std::int32_t gy=gridCoordinates[1]; + const std::int32_t gz=gridCoordinates[2]; const double qx=static_cast(sweep->gridMinimumSubvoxels[0] +static_cast(gx)*sweep->stepSubvoxels); const double qy=static_cast(sweep->gridMinimumSubvoxels[1] @@ -11586,6 +11954,15 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa *static_cast(sweep->dimension)+static_cast(gy)) *static_cast(sweep->dimension)+static_cast(gx); auto& sample=passageSamples.samples[sampleIndex]; + // The scan interval was clipped against both ownership planes, with + // one grid-step guard. Only its ends can lie outside the exact slab; + // keep the exact predicate there and skip two plane dot products for + // every interior sample. The plan audit independently checks unique + // ownership throughout the full support radius. + if((scan==scanFirst||scan==scanLast) + &&!FStandaloneShapeWorld::PassageOwnershipSlabOwns( + ownershipSlabs[segment.ownershipSlabIndex],{qx,qy,qz})) + continue; const double projectionXY=(qx-segment.a[0])*segment.delta[0] +(qy-segment.a[1])*segment.delta[1]; const double t=std::clamp((projectionXY @@ -11599,8 +11976,8 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa ex*ex+ey*ey+ez*ez)-radius; if(profile&&signedDistanceinfluenceSubvoxels+1.0) ++contributingPairCount; - const double floorT=segment.horizontalLengthSq>1.0e-12 - ?projectionXY/segment.horizontalLengthSq:t; + const double floorT=FStandaloneShapeWorld::PassageOwnershipFloorT( + segment,{qx,qy,qz}); const double floorTClamped=std::clamp(floorT,0.0,1.0); const double floorCenterDistanceSq=ex*ex+ey*ey+ez*ez; const double floorRadius=segment.radiusA @@ -11612,20 +11989,32 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa constants,segments,segmentIndex,floorT,segment, qx,qy,floorDepth,qPerMetre); if(profile)++profileStats_->passageSweepSegmentSamplePairs; - if(signedDistance>sweep->influenceSubvoxels) + // A normal body slab contains one segment, so it owns the floor + // profile directly. Mouth transitions and pathological composite + // routes deliberately group segments into one slab and retain their + // established nearest-floor tie-break within that owner. + if(ownershipSlab.firstSegment==segmentIndex + &&ownershipSlab.endSegment==segmentIndex+1) { - if(profile)++profileStats_->passageSweepOutsideInfluencePairs; + sample.floorCenterZSubvoxels=floorCenterZ; + sample.floorDepthSubvoxels=floorDepth; + sample.floorT=floorT; + sample.floorSegmentIndex=static_cast(segmentIndex); + sample.floorCenterDistanceSq=floorCenterDistanceSq; } - if(floorCenterDistanceSq( - segmentIndex); + sample.floorSegmentIndex=static_cast(segmentIndex); + sample.floorCenterDistanceSq=floorCenterDistanceSq; + } + if(signedDistance>sweep->influenceSubvoxels) + { + if(profile)++profileStats_->passageSweepOutsideInfluencePairs; } // A negative shell distance is possible only when the flattened tube // distance lies in [0, shellThickness]. SmoothMaxDistance is at least @@ -11707,12 +12096,13 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa sample.hasWallRelief=false; sample.hasExactContribution=true; if(profile)++profileStats_->passageSweepMinimumUpdates; - } + } } } } } } + } for(auto& passageSamples:sweep->passages) { diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v226.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v226.baseline new file mode 100644 index 0000000..cab68d6 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v226.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173 +[PipelineWholeViewHash] tiles=2370 density=0x3ce77475b5908860 mesh=0x80d3ca4b215ddb16 payload=0xf838e9c8ece70933 topology=0xdf790ec894e60a20 +[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a +[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630 +[PipelineLODHash] lod=2 tiles=320 density=0xb3e6e0585b0fc92d mesh=0x16c9a82c59e0f3d3 payload=0x1b32fc2e695144de topology=0x89a1eddead8849c1 +[PipelineLODHash] lod=3 tiles=208 density=0xe7c3ce8c79544110 mesh=0xff01589508cbbe69 payload=0xe4370a5d39da57c3 topology=0x2ea4ffeae7c72463 +[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494 +[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573 +[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618 +[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b +[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04 +[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-owner4-surface-world-style1-v226.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-owner4-surface-world-style1-v226.baseline new file mode 100644 index 0000000..3af58f0 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-owner4-surface-world-style1-v226.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0x8351b0410e09bad1 mesh=0x065b56bf31023628 payload=0xa8f438d8c4247999 topology=0x9bb94e8edb2c8dce +[PipelineWholeViewHash] tiles=2578 density=0xd95e78cea6a82b28 mesh=0xa50dbd869848a6fe payload=0xc78599692cb2e67a topology=0x5274c97954eee2c8 +[PipelineLODHash] lod=0 tiles=512 density=0x1a13bdd0641f3c46 mesh=0x010f1ce532bc0bae payload=0x5c249f31a0e55494 topology=0x5fbad4f63b23eaab +[PipelineLODHash] lod=1 tiles=448 density=0xb1cdeec444ad72fa mesh=0x0d26924ba452ec23 payload=0xc93967164a6fd098 topology=0x86b8391e622498f0 +[PipelineLODHash] lod=2 tiles=320 density=0x9bca76e9112eb2ad mesh=0x4c4a0c509b59c7e6 payload=0xfb7985d6da664124 topology=0xae622b59ae115234 +[PipelineLODHash] lod=3 tiles=272 density=0x267227f5066ac775 mesh=0x4945616669ae8489 payload=0xf3e83d394410d28e topology=0x56b7e63096340e41 +[PipelineLODHash] lod=4 tiles=272 density=0xb743eb64967bddcd mesh=0xf6b5bd36b0ebfead payload=0xd1284c1540f50636 topology=0x4a6e197e2a1a9500 +[PipelineLODHash] lod=5 tiles=208 density=0x38706b628a74dd17 mesh=0x445b1c7b51489cbb payload=0x713efc66290768f7 topology=0x56486b919146bb1e +[PipelineLODHash] lod=6 tiles=144 density=0xd53849b2002a1faa mesh=0xaa210abc8498c415 payload=0xd109aef8639706d1 topology=0x61a4d75fbbc35268 +[PipelineLODHash] lod=7 tiles=96 density=0xb0ea9a140275c9cc mesh=0x3ffd8365c8b0ec29 payload=0x3577afb0a2e4a420 topology=0x97530aafde15b7e0 +[PipelineLODHash] lod=8 tiles=306 density=0x8b3d1816799d67a0 mesh=0x80bf554f17700e1e payload=0xccbe22c3333a9c34 topology=0xcb11b28f6b6bcc50 +[PipelineExactQueryHash] probes=9 density=0xf2df67b38b7802df unclamped=0x48b347986345b557 detail=0xefcba20c0c70c415 collision=0x137e8f6c0b03a624 path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v226.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v226.baseline new file mode 100644 index 0000000..1f591cf --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v226.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae +[PipelineWholeViewHash] tiles=2370 density=0xb91a65f2b5d23dd1 mesh=0xcb21ea0acbcf7482 payload=0x812c3d1f7ff8d3b8 topology=0xb18b9bc29e243ba2 +[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3 +[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100 +[PipelineLODHash] lod=2 tiles=320 density=0xe06c3509977d89df mesh=0x4d81f55877e78bde payload=0x4dff71b395ce032c topology=0x8af5aa065efee965 +[PipelineLODHash] lod=3 tiles=208 density=0x60940ca350c1110a mesh=0xd386632a7d9271c0 payload=0x02e312e98024ef89 topology=0x0a5335eabb257106 +[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08 +[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89 +[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64 +[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72 +[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335 +[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index 5fc1584..895ff60 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -122,11 +122,11 @@ RECIPE_RE = re.compile( def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]: if recipe == "surface-world" and style == 1: - return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v225.baseline", - "L125-v225-stamped-passage-sweep") + return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v226.baseline", + "L128-v226-slab-owned-passage-sweep") if recipe == "floating-islands" and style == 2: - return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v225.baseline", - "L125-v225-stamped-passage-sweep") + return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v226.baseline", + "L128-v226-slab-owned-passage-sweep") raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}")