passages: four-strate passage floors supported (L67, evaluator v212); 1-2 wall beat still red
- Shared composer: a passage's restored support is applied after a physically overlapping later bore, instead of being carved away by it (0-1: 59 -> 0 unsupported). - Capsule clearance for support restoration is tested against nearby route segments only (1-2: 3 -> 0, 2-3: 2 -> 0 unsupported). - L66 index switches replaced by a physical criterion (both mouths inside their strata bands, neither a SurfaceWorld landing). - Audit: floor zero-crossing refined by bisection (the 0.2498 m "step" was the gauge); wall ripple keeps the authored radius, cubic detrend per 32 m window. Controls detected: L35 ribs, beads, synthetic 0.25 m step, L36 washboard, L23 terrace. - Result: 0-1 and 2-3 pass all audits; 1-2 fails wall ripple 7.27 cm (limit 3.5) from an authored-radius beat at 7.06 m. Fast tier was green before the final gauge change and was not re-run after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
No files matched your search
@@ -402,7 +402,8 @@ namespace VoxelForgeCore
|
||||
// Version 208 keeps the crater tread scoped to style-zero FI mouths.
|
||||
// Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges.
|
||||
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 211;
|
||||
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 212;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
@@ -2459,18 +2460,29 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
}
|
||||
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
||||
if(hasUpperRoute)
|
||||
// Authored tangents and island anchors already determine the phase.
|
||||
// A generated mouth in a native cave uses its radial approach; an
|
||||
// island landing uses the tangent fit below to ease off the crater rim.
|
||||
const bool generatedIslandWormEntry=!hasUpperRoute&&!hasUpperTangent
|
||||
&&!anchoredSpiral&&upper.generatorKind=="FloatingIslands";
|
||||
if(hasUpperRoute||(anchoredIslandPassage&&!anchoredSpiral)
|
||||
||(!hasUpperRoute&&!anchoredIslandPassage&&!anchoredSpiral
|
||||
&&upper.generatorKind!="FloatingIslands"))
|
||||
{
|
||||
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
|
||||
const std::uint32_t hashedPhase=phase;
|
||||
const std::int64_t phaseX=style==0&&anchoredIslandPassage
|
||||
&&!anchoredSpiral&&passageAnchor
|
||||
?upperMouth[0]-passageAnchor->values[0]
|
||||
:(hasUpperTangent?upperTangent[0]:upperMouth[0]);
|
||||
:(hasUpperTangent?upperTangent[0]:upperMouth[0]
|
||||
-(anchoredIslandPassage&&passageAnchor
|
||||
?passageAnchor->values[0]:0));
|
||||
const std::int64_t phaseY=style==0&&anchoredIslandPassage
|
||||
&&!anchoredSpiral&&passageAnchor
|
||||
?upperMouth[1]-passageAnchor->values[1]
|
||||
:(hasUpperTangent?upperTangent[1]:upperMouth[1]);
|
||||
:(hasUpperTangent?upperTangent[1]:upperMouth[1]
|
||||
-(anchoredIslandPassage&&passageAnchor
|
||||
?passageAnchor->values[1]:0));
|
||||
for(std::uint32_t offset=0;offset<16;++offset)
|
||||
{
|
||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||
@@ -2528,7 +2540,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
clockwise=clockwiseTangentX*upperTangent[0]
|
||||
+clockwiseTangentY*upperTangent[1]>=0.0;
|
||||
}
|
||||
const int direction=clockwise?1:-1;
|
||||
int direction=clockwise?1:-1;
|
||||
constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree helix contract.
|
||||
const std::int64_t wormRadialWobble=std::min<std::int64_t>(
|
||||
std::max<std::int64_t>(0,wander/3),2*qPerMetre);
|
||||
@@ -2597,6 +2609,267 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
static_cast<std::int64_t>(std::llround(centerX+radius*directions[directionIndex][0]/65536.0)),
|
||||
static_cast<std::int64_t>(std::llround(centerY+radius*directions[directionIndex][1]/65536.0))};
|
||||
};
|
||||
if(generatedIslandWormEntry)
|
||||
{
|
||||
struct FPhaseScore
|
||||
{
|
||||
std::uint32_t phase=0;
|
||||
int direction=1;
|
||||
std::uint64_t routeLength=0;
|
||||
double clearance=0.0;
|
||||
double tangent=0.0;
|
||||
};
|
||||
std::array<FPhaseScore,32> phaseScores{};
|
||||
std::size_t phaseScoreCount=0;
|
||||
const std::uint32_t hashedPhase=phase;
|
||||
const int hashedDirection=direction;
|
||||
const std::int64_t entryVerticalDistance=std::abs(
|
||||
lowerMouth[2]-upperMouth[2]);
|
||||
const std::int64_t entryRouteGradeQ16=profiledSurfaceJoin?8000
|
||||
:style==0&&anchoredIslandPassage&&!anchoredSpiral?15500
|
||||
:TargetGradeQ16;
|
||||
const auto candidateRouteLength=[&](std::uint32_t candidate,int candidateDirection)
|
||||
{
|
||||
std::vector<std::array<std::int64_t,2>> candidateXY;
|
||||
const auto append=[&](std::array<std::int64_t,2> point)
|
||||
{if(candidateXY.empty()||candidateXY.back()!=point)
|
||||
candidateXY.push_back(point);};
|
||||
append({upperMouth[0],upperMouth[1]});
|
||||
append(ringPoint(candidate));
|
||||
const auto measure=[&]()
|
||||
{
|
||||
std::uint64_t length=0;
|
||||
for(std::size_t i=1;i<candidateXY.size();++i)
|
||||
length+=distanceQ(candidateXY[i][0]-candidateXY[i-1][0],
|
||||
candidateXY[i][1]-candidateXY[i-1][1]);
|
||||
length+=distanceQ(lowerMouth[0]-candidateXY.back()[0],
|
||||
lowerMouth[1]-candidateXY.back()[1]);
|
||||
return length;
|
||||
};
|
||||
std::uint32_t loops=0;
|
||||
std::uint64_t length=0;
|
||||
while(loops<256)
|
||||
{
|
||||
length=measure();
|
||||
if(length*static_cast<std::uint64_t>(entryRouteGradeQ16)
|
||||
>=static_cast<std::uint64_t>(entryVerticalDistance)*65536ull)break;
|
||||
for(int step=1;step<=16;++step)
|
||||
{
|
||||
int directionIndex=static_cast<int>(candidate)
|
||||
+candidateDirection*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
append(ringPoint(static_cast<std::uint32_t>(directionIndex),
|
||||
loops+(step==16?1u:0u)));
|
||||
}
|
||||
++loops;
|
||||
}
|
||||
if(loops>=256)return std::uint64_t{0};
|
||||
const std::int64_t mouthPlanSeparation=distanceQ(
|
||||
lowerMouth[0]-upperMouth[0],lowerMouth[1]-upperMouth[1]);
|
||||
if(!hasLowerTangent&&!anchoredSpiral&&pathStyle==1u&&loops<=1
|
||||
&&mouthPlanSeparation<=2*mouthRadius
|
||||
&&entryVerticalDistance<=2*mouthRadius+qPerMetre/2)
|
||||
{
|
||||
double bestExitScore=-std::numeric_limits<double>::infinity();
|
||||
std::uint32_t lowerPhase=candidate;
|
||||
const auto firstRing=ringPoint(candidate);
|
||||
for(std::uint32_t exitCandidate=0;exitCandidate<16;++exitCandidate)
|
||||
{
|
||||
if(exitCandidate==candidate)continue;
|
||||
const std::uint32_t previous=static_cast<std::uint32_t>(
|
||||
(static_cast<int>(exitCandidate)-candidateDirection+16)%16);
|
||||
const auto terminal=ringPoint(exitCandidate,loops);
|
||||
const auto beforeTerminal=ringPoint(previous,loops);
|
||||
const double incomingX=static_cast<double>(terminal[0]-beforeTerminal[0]);
|
||||
const double incomingY=static_cast<double>(terminal[1]-beforeTerminal[1]);
|
||||
const double outgoingX=static_cast<double>(lowerMouth[0]-terminal[0]);
|
||||
const double outgoingY=static_cast<double>(lowerMouth[1]-terminal[1]);
|
||||
const double incomingLength=std::hypot(incomingX,incomingY);
|
||||
const double outgoingLength=std::hypot(outgoingX,outgoingY);
|
||||
if(incomingLength<=1.0e-6||outgoingLength<=1.0e-6)continue;
|
||||
const double alignment=(incomingX*outgoingX+incomingY*outgoingY)
|
||||
/(incomingLength*outgoingLength);
|
||||
const double separation=std::hypot(
|
||||
static_cast<double>(terminal[0]-firstRing[0]),
|
||||
static_cast<double>(terminal[1]-firstRing[1]));
|
||||
const double score=alignment+0.20*std::clamp(
|
||||
separation/(2.0*qPerMetre),0.0,2.0);
|
||||
if(score>bestExitScore)
|
||||
{bestExitScore=score;lowerPhase=exitCandidate;}
|
||||
}
|
||||
for(int step=1;step<=16;++step)
|
||||
{
|
||||
int directionIndex=static_cast<int>(candidate)
|
||||
+candidateDirection*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
append(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
|
||||
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
|
||||
}
|
||||
}
|
||||
return measure();
|
||||
};
|
||||
for(std::uint32_t offset=0;offset<16;++offset)
|
||||
{
|
||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||
(hashedPhase+16-offset)%16);
|
||||
const auto point=ringPoint(candidate);
|
||||
const double entryX=static_cast<double>(point[0]-upperMouth[0]);
|
||||
const double entryY=static_cast<double>(point[1]-upperMouth[1]);
|
||||
const double entryLength=std::hypot(entryX,entryY);
|
||||
if(entryLength<=1.0e-6)continue;
|
||||
const auto entryDistance=distanceQ(point[0]-upperMouth[0],
|
||||
point[1]-upperMouth[1]);
|
||||
for(const int candidateDirection:{hashedDirection,-hashedDirection})
|
||||
{
|
||||
const std::uint64_t routeLength=candidateRouteLength(candidate,
|
||||
candidateDirection);
|
||||
if(routeLength==0)continue;
|
||||
const std::uint32_t next=static_cast<std::uint32_t>(
|
||||
(static_cast<int>(candidate)+candidateDirection+16)%16);
|
||||
const auto nextPoint=ringPoint(next);
|
||||
const double tangentX=static_cast<double>(nextPoint[0]-point[0]);
|
||||
const double tangentY=static_cast<double>(nextPoint[1]-point[1]);
|
||||
const double tangentLength=std::hypot(tangentX,tangentY);
|
||||
if(tangentLength<=1.0e-6)continue;
|
||||
const double tangentAlignment=(entryX*tangentX+entryY*tangentY)
|
||||
/(entryLength*tangentLength);
|
||||
const std::int64_t firstAxisZ=upperMouth[2]+mouthRadius
|
||||
-playerCenterAboveFloorQ;
|
||||
const std::int64_t secondAxisZ=firstAxisZ+PassageRoundRatio(
|
||||
lowerMouth[2]-upperMouth[2],static_cast<std::int64_t>(entryDistance),
|
||||
static_cast<std::int64_t>(routeLength));
|
||||
double candidateTreadClearance=std::numeric_limits<double>::infinity();
|
||||
const int entrySamples=std::max(1,static_cast<int>(std::ceil(
|
||||
entryLength/(0.25*qPerMetre))));
|
||||
for(int sample=0;sample<=entrySamples;++sample)
|
||||
{
|
||||
const double t=static_cast<double>(sample)/entrySamples;
|
||||
const double qx=upperMouth[0]+t*entryX;
|
||||
const double qy=upperMouth[1]+t*entryY;
|
||||
const double qz=firstAxisZ+t*(secondAxisZ-firstAxisZ)
|
||||
-mouthRadius+0.20*qPerMetre;
|
||||
for(const auto& previousPassage:layout.passages)
|
||||
{
|
||||
if(previousPassage.stableId==passage.stableId
|
||||
||previousPassage.controlPoints.size()<2)continue;
|
||||
for(std::size_t previousSegment=0;
|
||||
previousSegment+1<previousPassage.controlPoints.size();
|
||||
++previousSegment)
|
||||
{
|
||||
const auto& previousA=previousPassage.controlPoints[previousSegment];
|
||||
const auto& previousB=previousPassage.controlPoints[previousSegment+1];
|
||||
const double ax=static_cast<double>(previousA.positionSubvoxels[0]);
|
||||
const double ay=static_cast<double>(previousA.positionSubvoxels[1]);
|
||||
const double az=static_cast<double>(previousA.positionSubvoxels[2]);
|
||||
const double dx=static_cast<double>(previousB.positionSubvoxels[0])-ax;
|
||||
const double dy=static_cast<double>(previousB.positionSubvoxels[1])-ay;
|
||||
const double dz=static_cast<double>(previousB.positionSubvoxels[2])-az;
|
||||
const double horizontalLengthSquared=dx*dx+dy*dy;
|
||||
const double projection=horizontalLengthSquared>0.0
|
||||
?std::clamp(((qx-ax)*dx+(qy-ay)*dy)
|
||||
/horizontalLengthSquared,0.0,1.0):0.0;
|
||||
const double centerX=ax+projection*dx;
|
||||
const double centerY=ay+projection*dy;
|
||||
const double centerZ=az+projection*dz
|
||||
-(previousA.radiusSubvoxels+projection*(
|
||||
static_cast<double>(previousB.radiusSubvoxels)
|
||||
-previousA.radiusSubvoxels))
|
||||
+1.13*qPerMetre;
|
||||
const double vertical=std::max(0.0,
|
||||
std::abs(qz-centerZ)-0.54*qPerMetre);
|
||||
const double clearance=std::sqrt(
|
||||
(qx-centerX)*(qx-centerX)+(qy-centerY)*(qy-centerY)
|
||||
+vertical*vertical)-0.34*qPerMetre;
|
||||
candidateTreadClearance=std::min(candidateTreadClearance,
|
||||
clearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
double previousTreadClearance=std::numeric_limits<double>::infinity();
|
||||
const double entryXQ=entryX,entryYQ=entryY;
|
||||
const double entryZQ=static_cast<double>(secondAxisZ-firstAxisZ);
|
||||
const double entryLengthSquared=entryXQ*entryXQ+entryYQ*entryYQ
|
||||
+entryZQ*entryZQ;
|
||||
const double mouthStartT=std::clamp(
|
||||
mouthRadius/entryLength,0.0,1.0);
|
||||
for(const auto& previousPassage:layout.passages)
|
||||
{
|
||||
if(previousPassage.stableId==passage.stableId
|
||||
||previousPassage.controlPoints.size()<2)continue;
|
||||
for(std::size_t previousSegment=0;
|
||||
previousSegment+1<previousPassage.controlPoints.size();
|
||||
++previousSegment)
|
||||
{
|
||||
const auto& previousA=previousPassage.controlPoints[previousSegment];
|
||||
const auto& previousB=previousPassage.controlPoints[previousSegment+1];
|
||||
const double ax=static_cast<double>(previousA.positionSubvoxels[0]);
|
||||
const double ay=static_cast<double>(previousA.positionSubvoxels[1]);
|
||||
const double az=static_cast<double>(previousA.positionSubvoxels[2]);
|
||||
const double dx=static_cast<double>(previousB.positionSubvoxels[0])-ax;
|
||||
const double dy=static_cast<double>(previousB.positionSubvoxels[1])-ay;
|
||||
const double dz=static_cast<double>(previousB.positionSubvoxels[2])-az;
|
||||
for(int sample=0;sample<=8;++sample)
|
||||
{
|
||||
const double t=static_cast<double>(sample)/8.0;
|
||||
const double px=ax+t*dx;
|
||||
const double py=ay+t*dy;
|
||||
const double pz=az+t*dz
|
||||
-(previousA.radiusSubvoxels+t*(
|
||||
static_cast<double>(previousB.radiusSubvoxels)
|
||||
-previousA.radiusSubvoxels))
|
||||
+0.20*qPerMetre;
|
||||
const double projection=entryLengthSquared>0.0
|
||||
?std::clamp(((px-upperMouth[0])*entryXQ
|
||||
+(py-upperMouth[1])*entryYQ
|
||||
+(pz-firstAxisZ)*entryZQ)/entryLengthSquared,
|
||||
mouthStartT,1.0):mouthStartT;
|
||||
const double ex=px-(upperMouth[0]+projection*entryXQ);
|
||||
const double ey=py-(upperMouth[1]+projection*entryYQ);
|
||||
const double ez=pz-(firstAxisZ+projection*entryZQ);
|
||||
const double clearance=std::sqrt(ex*ex+ey*ey+ez*ez)
|
||||
-mouthRadius;
|
||||
previousTreadClearance=std::min(previousTreadClearance,
|
||||
clearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
const double clearanceScore=std::isfinite(candidateTreadClearance)
|
||||
&&std::isfinite(previousTreadClearance)
|
||||
?std::min(candidateTreadClearance,previousTreadClearance)/qPerMetre
|
||||
:0.0;
|
||||
if(phaseScoreCount<phaseScores.size())
|
||||
phaseScores[phaseScoreCount++]={candidate,candidateDirection,
|
||||
routeLength,clearanceScore,tangentAlignment};
|
||||
}
|
||||
}
|
||||
if(phaseScoreCount>0)
|
||||
{
|
||||
double bestClearance=-std::numeric_limits<double>::infinity();
|
||||
for(std::size_t index=0;index<phaseScoreCount;++index)
|
||||
bestClearance=std::max(bestClearance,phaseScores[index].clearance);
|
||||
constexpr double ClearanceToleranceMetres=0.10;
|
||||
const FPhaseScore* selected=nullptr;
|
||||
for(std::size_t index=0;index<phaseScoreCount;++index)
|
||||
{
|
||||
const auto& candidate=phaseScores[index];
|
||||
if(candidate.clearance+ClearanceToleranceMetres<bestClearance)continue;
|
||||
const bool shorter=!selected||candidate.routeLength<selected->routeLength;
|
||||
const bool stableLengthTie=selected
|
||||
&&candidate.routeLength==selected->routeLength
|
||||
&&(candidate.clearance>selected->clearance+1.0e-6
|
||||
||(std::abs(candidate.clearance-selected->clearance)<=1.0e-6
|
||||
&&candidate.tangent>selected->tangent));
|
||||
if(shorter||stableLengthTie)selected=&candidate;
|
||||
}
|
||||
if(selected)
|
||||
{
|
||||
phase=selected->phase;
|
||||
direction=selected->direction;
|
||||
clockwise=direction>0;
|
||||
}
|
||||
}
|
||||
}
|
||||
direction=clockwise?1:-1;
|
||||
std::vector<std::array<std::int64_t,2>> xy;
|
||||
const auto pushXY=[&](std::array<std::int64_t,2> p)
|
||||
{if(xy.empty()||xy.back()!=p)xy.push_back(p);};
|
||||
@@ -2731,6 +3004,12 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
}
|
||||
++loops;
|
||||
}
|
||||
const std::int64_t mouthPlanSeparation=distanceQ(
|
||||
lowerMouth[0]-upperMouth[0],lowerMouth[1]-upperMouth[1]);
|
||||
const bool shortGeneratedClosure=!hasLowerTangent&&!hasAnchorHelix
|
||||
&&pathStyle==1u&&loops<=1
|
||||
&&mouthPlanSeparation<=2*mouthRadius
|
||||
&&verticalDistance<=2*mouthRadius+qPerMetre/2;
|
||||
if(hasLowerTangent)
|
||||
{
|
||||
std::uint32_t lowerPhase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
@@ -2789,6 +3068,48 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
}
|
||||
pushXY(ringPoint(lowerPhase));
|
||||
}
|
||||
else if(shortGeneratedClosure)
|
||||
{
|
||||
// A one-turn generated route whose mouths share a plan location
|
||||
// must leave and rejoin from different ring tangents. Returning on
|
||||
// the entry chord creates a hairpin that overlaps its own tread.
|
||||
double bestExitScore=-std::numeric_limits<double>::infinity();
|
||||
std::uint32_t lowerPhase=phase;
|
||||
const auto firstRing=ringPoint(phase,loops);
|
||||
for(std::uint32_t candidate=0;candidate<16;++candidate)
|
||||
{
|
||||
if(candidate==phase)continue;
|
||||
const std::uint32_t previous=static_cast<std::uint32_t>(
|
||||
(static_cast<int>(candidate)-direction+16)%16);
|
||||
const auto terminal=ringPoint(candidate,loops);
|
||||
const auto beforeTerminal=ringPoint(previous,loops);
|
||||
const double incomingX=static_cast<double>(terminal[0]-beforeTerminal[0]);
|
||||
const double incomingY=static_cast<double>(terminal[1]-beforeTerminal[1]);
|
||||
const double outgoingX=static_cast<double>(lowerMouth[0]-terminal[0]);
|
||||
const double outgoingY=static_cast<double>(lowerMouth[1]-terminal[1]);
|
||||
const double incomingLength=std::hypot(incomingX,incomingY);
|
||||
const double outgoingLength=std::hypot(outgoingX,outgoingY);
|
||||
if(incomingLength<=1.0e-6||outgoingLength<=1.0e-6)continue;
|
||||
const double tangentAlignment=(incomingX*outgoingX+incomingY*outgoingY)
|
||||
/(incomingLength*outgoingLength);
|
||||
const double separation=std::hypot(
|
||||
static_cast<double>(terminal[0]-firstRing[0]),
|
||||
static_cast<double>(terminal[1]-firstRing[1]));
|
||||
const double score=tangentAlignment
|
||||
+0.20*std::clamp(separation/(2.0*qPerMetre),0.0,2.0);
|
||||
if(score>bestExitScore)
|
||||
{bestExitScore=score;lowerPhase=candidate;}
|
||||
}
|
||||
for(int step=1;step<=16;++step)
|
||||
{
|
||||
int directionIndex=static_cast<int>(phase)+direction*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
|
||||
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
|
||||
}
|
||||
std::printf("[ResolvedPassageGeneratedExitApproach] id=%s turns=%u phase=%u exit_phase=%u tangent_score=%.3f status=PASS\n",
|
||||
passage.stableId.c_str(),loops,phase,lowerPhase,bestExitScore);
|
||||
}
|
||||
pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
|
||||
std::size_t lowerMouthIndex=xy.size()-1;
|
||||
std::vector<std::array<std::int64_t,3>> lowerLandingPoints;
|
||||
|
||||
@@ -971,6 +971,7 @@ public:
|
||||
const double floorThicknessVoxels=qPerMetre/Q;
|
||||
const double playerCenterAboveFloorQ=1.13*qPerMetre;
|
||||
float composed=density;
|
||||
float deferredSupportDensity=std::numeric_limits<float>::infinity();
|
||||
const auto composeRouteMouthSupport=[&](std::int32_t strateIndex,
|
||||
const std::array<std::int64_t,3>& mouth,bool departure)
|
||||
{
|
||||
@@ -1045,7 +1046,6 @@ public:
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
if(passage.controlPoints.size()<2)continue;
|
||||
const bool internalChainPassage=passageIndex>0;
|
||||
const auto supportStratum=[&](std::int32_t strateIndex)
|
||||
->const VoxelForgeCore::ResolvedLayoutStratumV2*
|
||||
{
|
||||
@@ -1056,6 +1056,31 @@ public:
|
||||
};
|
||||
const auto* supportUpper=supportStratum(passage.upperStrateIndex);
|
||||
const auto* supportLower=supportStratum(passage.lowerStrateIndex);
|
||||
const auto mouthInsideContent=[&](
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2* stratum,
|
||||
const std::array<std::int64_t,3>& mouth)
|
||||
{
|
||||
if(!stratum||!stratum->content.presentInCurrentField)return false;
|
||||
const auto zMicrometres=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(mouth[2])
|
||||
*ResolvedLayout->coreVoxelSizeMicrometres/Q));
|
||||
return zMicrometres>=stratum->content.bottomMicrometres
|
||||
&&zMicrometres<stratum->content.topMicrometres;
|
||||
};
|
||||
// A passage is internal when both mouths are bounded by their strate
|
||||
// content and neither endpoint is a SurfaceWorld landing. Route order
|
||||
// and the total number of strata are irrelevant.
|
||||
const bool internalChainPassage=supportUpper&&supportLower
|
||||
&&!supportUpper->passageCapabilities.hasSurfaceLanding
|
||||
&&!supportLower->passageCapabilities.hasSurfaceLanding
|
||||
&&mouthInsideContent(supportUpper,passage.upperMouthSubvoxels)
|
||||
&&mouthInsideContent(supportLower,passage.lowerMouthSubvoxels);
|
||||
const auto laterPassageMayCarve=[&](std::ptrdiff_t supportSegment,
|
||||
double sampleX,double sampleY,double sampleZ)
|
||||
{
|
||||
return PassageSupportMayBeCarvedByLaterBore(passageIndex,
|
||||
supportSegment,sampleX,sampleY,sampleZ,0.35*qPerMetre);
|
||||
};
|
||||
const bool floatingIslandPassage=supportUpper
|
||||
&&supportUpper->passageCapabilities.hasFloatingIslandLanding;
|
||||
const bool surfaceToTunnelSupport=supportUpper&&supportLower
|
||||
@@ -1223,7 +1248,8 @@ public:
|
||||
+playerVertical*playerVertical)-0.34*qPerMetre);
|
||||
}
|
||||
double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
|
||||
const bool smoothCarveJoin=index>2&&index+12<lastIndex
|
||||
const bool smoothCarveJoin=index>2
|
||||
&&index+(internalChainPassage?2:12)<lowerMouthIndex
|
||||
&&DeterministicSqrt(lengthSq)<=8.0*qPerMetre
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex-1)
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex)
|
||||
@@ -1301,8 +1327,9 @@ public:
|
||||
?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*(
|
||||
static_cast<double>(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2])))
|
||||
/distanceToCenter,0.0,1.0):0.0;
|
||||
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
||||
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),
|
||||
static_cast<float>(Y),static_cast<float>(Z),
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
||||
const float mouthSurfaceRelief=VF_FieldSurfaceRelief(static_cast<float>(X)*1.5f,
|
||||
static_cast<float>(Y)*1.5f,static_cast<float>(Z)*1.5f,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u);
|
||||
@@ -1427,7 +1454,8 @@ public:
|
||||
static_cast<double>(mouthTerrainRelief),-1.0,1.0)
|
||||
*0.20*qPerMetre*mouthFloorBlend*floorReliefWeight
|
||||
*(upperMouthFloorFade?1.0:0.0);
|
||||
const double wallReliefWeight=0.05+0.95*(1.0-verticalWallAlignment);
|
||||
const double wallOpening=1.0-verticalWallAlignment;
|
||||
const double wallReliefWeight=0.05+0.95*wallOpening;
|
||||
const double roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight
|
||||
+floorReliefOffset;
|
||||
@@ -1472,6 +1500,7 @@ public:
|
||||
static_cast<float>(-roughSignedDistance/Q));
|
||||
composed=std::lerp(composed,fullyCarvedDensity,
|
||||
static_cast<float>(floorCarveWeight));
|
||||
const float composedAfterCarve=composed;
|
||||
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
||||
{
|
||||
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||
@@ -1570,15 +1599,25 @@ public:
|
||||
const double minimumSupportQueryDepth=index==selectedSupportSegmentIndex
|
||||
&&sparseRouteSupport?0.65:0.93;
|
||||
if(supportQueryDepth<minimumSupportQueryDepth*qPerMetre
|
||||
||supportQueryDepth>1.30*qPerMetre)return;
|
||||
||supportQueryDepth>1.30*qPerMetre)
|
||||
return;
|
||||
const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre;
|
||||
const double supportThicknessVoxels=fallbackSupport
|
||||
?qPerMetre/Q:0.5*qPerMetre/Q;
|
||||
// Keep the supported ramp to a comfortable walking width. Extending a
|
||||
// full bore-radius shelf across overlapping helix coils can block the
|
||||
// capsule on the neighbouring turn.
|
||||
const bool lowerMouthBodyOverlap=ResolvedLayout->strates.size()>2
|
||||
&&index>=lowerMouthIndex-2
|
||||
const double lowerMouthDx=centerX
|
||||
-static_cast<double>(passage.lowerMouthSubvoxels[0]);
|
||||
const double lowerMouthDy=centerY
|
||||
-static_cast<double>(passage.lowerMouthSubvoxels[1]);
|
||||
const double lowerMouthDz=centerZ-radius
|
||||
-static_cast<double>(passage.lowerMouthSubvoxels[2]);
|
||||
const double lowerMouthBodyDistance=DeterministicSqrt(
|
||||
lowerMouthDx*lowerMouthDx+lowerMouthDy*lowerMouthDy
|
||||
+lowerMouthDz*lowerMouthDz);
|
||||
const bool lowerMouthBodyOverlap=internalChainPassage
|
||||
&&lowerMouthBodyDistance<=2.0*qPerMetre
|
||||
&&supportQueryDepth<0.98*qPerMetre;
|
||||
// Keep a narrow walk core, but place its top on the native surface
|
||||
// so the support remains a worn ramp instead of a route-height slab.
|
||||
@@ -1731,7 +1770,8 @@ public:
|
||||
bestSupportUsesSignedDistance=signedDistanceSupport;
|
||||
bestSupportMouthBlend=mouthSupportBlend;
|
||||
bestSupportNativeBlend=mouthTread
|
||||
?0.85*mouthSupportBlend*(1.0-supportEdgeSmooth):0.0;
|
||||
?(internalChainPassage?1.0:0.85)
|
||||
*mouthSupportBlend*(1.0-supportEdgeSmooth):0.0;
|
||||
};
|
||||
std::ptrdiff_t supportCenterIndex=bestIndex;
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
@@ -1775,6 +1815,8 @@ public:
|
||||
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,
|
||||
capsuleQueryZ,qPerMetre,capsuleClearanceMetres)
|
||||
:bestPlayerCapsuleSignedDistance>=capsuleClearanceMetres*qPerMetre;
|
||||
bool supportApplied=false;
|
||||
bool supportNeedsDeferral=false;
|
||||
// Outside bridge and short-span segments, add the tread only where the
|
||||
// native field already provides a solid envelope; the capsule check keeps
|
||||
// that support below the player's walk space.
|
||||
@@ -1782,6 +1824,8 @@ public:
|
||||
||shortSpanFloorSupport)
|
||||
&&(capsuleAllowsSupport||bestSupportDensity>=0.0))
|
||||
{
|
||||
supportNeedsDeferral=laterPassageMayCarve(bestSupportSegmentIndex,
|
||||
qx,qy,qz);
|
||||
if(bestSupportIsMouthTread)
|
||||
{
|
||||
// Preserve the owner's native ground through the shallow mouth
|
||||
@@ -1797,13 +1841,36 @@ public:
|
||||
const double nativeSurfaceDetail=std::clamp(
|
||||
static_cast<double>(mouthTerrainRelief),-1.0,1.0)
|
||||
*0.18*qPerMetre/Q;
|
||||
const float nativeGroundWithRelief=static_cast<float>(density
|
||||
-nativeSurfaceDetail);
|
||||
const float nativeGroundWithRelief=static_cast<float>(
|
||||
(supportNeedsDeferral?std::max(-1.0f,density):density)
|
||||
-nativeSurfaceDetail);
|
||||
composed=std::lerp(composed,nativeGroundWithRelief,
|
||||
static_cast<float>(bestSupportNativeBlend));
|
||||
supportApplied=composed<composedAfterCarve;
|
||||
}
|
||||
}
|
||||
else composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
else
|
||||
{
|
||||
const float densityBeforeSupport=composed;
|
||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
supportApplied=composed<densityBeforeSupport;
|
||||
}
|
||||
}
|
||||
if(supportApplied)
|
||||
{
|
||||
if(PassageOtherCapsuleBlocksSupport(passageIndex,
|
||||
bestSupportSegmentIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres))
|
||||
{
|
||||
composed=composedAfterCarve;
|
||||
supportApplied=false;
|
||||
}
|
||||
}
|
||||
if(supportApplied&&supportNeedsDeferral)
|
||||
{
|
||||
deferredSupportDensity=std::min(deferredSupportDensity,composed);
|
||||
// Preserve a tread only when a later passage bore reaches this
|
||||
// sample; otherwise retain the ordinary composition order.
|
||||
composed=composedAfterCarve;
|
||||
}
|
||||
}
|
||||
// Restore the surface route where the passage cut crossed otherwise solid
|
||||
@@ -1818,6 +1885,7 @@ public:
|
||||
composeRouteMouthSupport(passage.upperStrateIndex,
|
||||
passage.upperMouthSubvoxels,true);
|
||||
}
|
||||
composed=std::min(composed,deferredSupportDensity);
|
||||
return composed;
|
||||
}
|
||||
|
||||
@@ -3282,6 +3350,17 @@ private:
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
};
|
||||
struct FPassageLaterBoreCandidate
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
double reach=0.0;
|
||||
};
|
||||
struct FPassageLaterBoreSource
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
std::vector<FPassageLaterBoreCandidate> candidates;
|
||||
std::vector<std::size_t> capsuleCandidatePassages;
|
||||
};
|
||||
struct FPassageProofNode
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
@@ -4237,6 +4316,7 @@ private:
|
||||
PassageNearestNodes.clear();
|
||||
PassageProofBounds.clear();PassageProofNodes.clear();
|
||||
PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0);
|
||||
PassageLaterBoreSources.clear();
|
||||
PassageColumnSegmentFlags.clear();PassageShortSpanSegmentFlags.clear();
|
||||
PassageHasShortSpanFlags.clear();
|
||||
if(!ResolvedLayout||ResolvedLayout->passages.empty())return;
|
||||
@@ -4336,6 +4416,75 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
// Precompute the later-bore leaves that can intersect each tread segment.
|
||||
// Passage support samples normally stay within this source envelope, so the
|
||||
// hot query can test a short list of physical bounds instead of walking a
|
||||
// full spatial tree for every density sample.
|
||||
PassageLaterBoreSources.resize(ResolvedLayout->passages.size());
|
||||
const double qPerMetre=Q*voxelsPerMetre;
|
||||
for(std::size_t sourcePassageIndex=0;
|
||||
sourcePassageIndex<ResolvedLayout->passages.size();++sourcePassageIndex)
|
||||
{
|
||||
const auto& sourcePassage=ResolvedLayout->passages[sourcePassageIndex];
|
||||
const std::size_t sourceSegmentCount=sourcePassage.controlPoints.size()>1
|
||||
?sourcePassage.controlPoints.size()-1:0;
|
||||
auto& sourceSegments=PassageLaterBoreSources[sourcePassageIndex];
|
||||
sourceSegments.resize(sourceSegmentCount);
|
||||
for(std::size_t sourceSegment=0;sourceSegment<sourceSegmentCount;++sourceSegment)
|
||||
{
|
||||
const auto& sourceA=sourcePassage.controlPoints[sourceSegment];
|
||||
const auto& sourceB=sourcePassage.controlPoints[sourceSegment+1];
|
||||
const double sourceRadius=std::max(sourceA.radiusSubvoxels,
|
||||
sourceB.radiusSubvoxels);
|
||||
auto& source=sourceSegments[sourceSegment];
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
source.minimum[axis]=std::min(sourceA.positionSubvoxels[axis],
|
||||
sourceB.positionSubvoxels[axis])-sourceRadius-3.0*qPerMetre;
|
||||
source.maximum[axis]=std::max(sourceA.positionSubvoxels[axis],
|
||||
sourceB.positionSubvoxels[axis])+sourceRadius+3.0*qPerMetre;
|
||||
}
|
||||
for(std::size_t laterPassageIndex=sourcePassageIndex+1;
|
||||
laterPassageIndex<ResolvedLayout->passages.size();++laterPassageIndex)
|
||||
{
|
||||
for(const auto& node:PassageNearestNodes[laterPassageIndex])
|
||||
{
|
||||
if(node.left>=0)continue;
|
||||
const double reach=node.maximumRadius+0.35*qPerMetre+1.0e-6;
|
||||
bool intersectsSource=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
intersectsSource=intersectsSource
|
||||
&&node.minimum[axis]-reach<=source.maximum[axis]
|
||||
&&node.maximum[axis]+reach>=source.minimum[axis];
|
||||
if(!intersectsSource)continue;
|
||||
source.candidates.push_back({node.minimum,node.maximum,reach});
|
||||
}
|
||||
}
|
||||
const double capsuleXYReach=0.39*qPerMetre;
|
||||
for(std::size_t otherPassageIndex=0;
|
||||
otherPassageIndex<ResolvedLayout->passages.size();++otherPassageIndex)
|
||||
{
|
||||
if(otherPassageIndex==sourcePassageIndex
|
||||
||PassageNearestNodes[otherPassageIndex].empty())continue;
|
||||
const auto& otherRoot=PassageNearestNodes[otherPassageIndex].front();
|
||||
const std::array<double,3> blockerMinimum={
|
||||
otherRoot.minimum[0]-capsuleXYReach,
|
||||
otherRoot.minimum[1]-capsuleXYReach,
|
||||
otherRoot.minimum[2]-otherRoot.maximumRadius+0.20*qPerMetre};
|
||||
const std::array<double,3> blockerMaximum={
|
||||
otherRoot.maximum[0]+capsuleXYReach,
|
||||
otherRoot.maximum[1]+capsuleXYReach,
|
||||
otherRoot.maximum[2]+2.06*qPerMetre};
|
||||
bool intersectsSource=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
intersectsSource=intersectsSource
|
||||
&&blockerMinimum[axis]<=source.maximum[axis]
|
||||
&&blockerMaximum[axis]>=source.minimum[axis];
|
||||
if(intersectsSource)
|
||||
source.capsuleCandidatePassages.push_back(otherPassageIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
for(const auto& crossing:PassageVoidCrossings)
|
||||
{
|
||||
if(crossing.passageIndex<PassageColumnSegmentFlags.size())
|
||||
@@ -4473,6 +4622,88 @@ private:
|
||||
signedDistanceLimit);
|
||||
}
|
||||
|
||||
bool PassageSupportMayBeCarvedByLaterBore(std::size_t passageIndex,
|
||||
std::ptrdiff_t supportSegment,double qx,double qy,double qz,
|
||||
double signedDistanceLimit) const
|
||||
{
|
||||
if(!ResolvedLayout||passageIndex+1>=ResolvedLayout->passages.size())return false;
|
||||
if(passageIndex<PassageLaterBoreSources.size()&&supportSegment>=0
|
||||
&&static_cast<std::size_t>(supportSegment)
|
||||
<PassageLaterBoreSources[passageIndex].size())
|
||||
{
|
||||
const auto& source=PassageLaterBoreSources[passageIndex][
|
||||
static_cast<std::size_t>(supportSegment)];
|
||||
const bool insideSourceEnvelope=qx>=source.minimum[0]&&qx<=source.maximum[0]
|
||||
&&qy>=source.minimum[1]&&qy<=source.maximum[1]
|
||||
&&qz>=source.minimum[2]&&qz<=source.maximum[2];
|
||||
if(insideSourceEnvelope)
|
||||
{
|
||||
for(const auto& candidate:source.candidates)
|
||||
{
|
||||
double distanceSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double q=axis==0?qx:axis==1?qy:qz;
|
||||
const double distance=q<candidate.minimum[axis]
|
||||
?candidate.minimum[axis]-q
|
||||
:q>candidate.maximum[axis]?q-candidate.maximum[axis]:0.0;
|
||||
distanceSquared+=distance*distance;
|
||||
}
|
||||
const double reach=candidate.reach;
|
||||
if(distanceSquared<=reach*reach)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for(std::size_t otherPassageIndex=passageIndex+1;
|
||||
otherPassageIndex<ResolvedLayout->passages.size();++otherPassageIndex)
|
||||
{
|
||||
const auto& otherPassage=ResolvedLayout->passages[otherPassageIndex];
|
||||
if(otherPassage.controlPoints.size()<2)continue;
|
||||
if(PassagePotentialWithinNode(otherPassageIndex,0,0,
|
||||
otherPassage.controlPoints.size()-1,qx,qy,qz,signedDistanceLimit))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PassageOtherCapsuleBlocksSupport(std::size_t passageIndex,
|
||||
std::ptrdiff_t supportSegment,double qx,double qy,double qz,
|
||||
double qPerMetre,double minimumClearanceMetres) const
|
||||
{
|
||||
if(passageIndex<PassageLaterBoreSources.size()&&supportSegment>=0
|
||||
&&static_cast<std::size_t>(supportSegment)
|
||||
<PassageLaterBoreSources[passageIndex].size())
|
||||
{
|
||||
const auto& source=PassageLaterBoreSources[passageIndex][
|
||||
static_cast<std::size_t>(supportSegment)];
|
||||
const bool insideSourceEnvelope=qx>=source.minimum[0]&&qx<=source.maximum[0]
|
||||
&&qy>=source.minimum[1]&&qy<=source.maximum[1]
|
||||
&&qz>=source.minimum[2]&&qz<=source.maximum[2];
|
||||
if(insideSourceEnvelope)
|
||||
{
|
||||
for(const std::size_t otherPassageIndex:source.capsuleCandidatePassages)
|
||||
{
|
||||
const auto& otherPassage=ResolvedLayout->passages[otherPassageIndex];
|
||||
if(PassagePlayerCapsuleBlocksSupportNode(otherPassageIndex,0,0,
|
||||
otherPassage.controlPoints.size()-1,qx,qy,qz,qPerMetre,
|
||||
minimumClearanceMetres))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for(std::size_t otherPassageIndex=0;
|
||||
otherPassageIndex<ResolvedLayout->passages.size();++otherPassageIndex)
|
||||
{
|
||||
if(otherPassageIndex==passageIndex)continue;
|
||||
const auto& otherPassage=ResolvedLayout->passages[otherPassageIndex];
|
||||
if(otherPassage.controlPoints.size()<2)continue;
|
||||
if(PassagePlayerCapsuleBlocksSupportNode(otherPassageIndex,0,0,
|
||||
otherPassage.controlPoints.size()-1,qx,qy,qz,qPerMetre,
|
||||
minimumClearanceMetres))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PassageMayAffectDensity(std::size_t passageIndex,std::ptrdiff_t carveBegin,
|
||||
std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex,
|
||||
std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex,
|
||||
@@ -4529,7 +4760,10 @@ private:
|
||||
const double vertical=std::max(0.0,std::abs(qz-centerZ)-0.54*qPerMetre);
|
||||
const double distance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY
|
||||
+vertical*vertical)-0.34*qPerMetre;
|
||||
if(distance<minimumClearanceMetres*qPerMetre)return true;
|
||||
if(distance<minimumClearanceMetres*qPerMetre)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -4749,8 +4983,12 @@ private:
|
||||
const double irregularity=1.25*qPerMetre
|
||||
+std::clamp(static_cast<double>(broadRelief),-1.0,1.0)*1.10*qPerMetre
|
||||
+std::clamp(static_cast<double>(surfaceRelief),-1.0,1.0)*0.35*qPerMetre;
|
||||
// Add a subtle host-rock root only when the unmodified column lacks the
|
||||
// vertical relief required for walkable passage support.
|
||||
const double rootTaper=crossing.needsRootTaper
|
||||
?0.08*qPerMetre*smoothStep((t-0.35)/0.50):0.0;
|
||||
const double radius=static_cast<double>(crossing.minimumRadiusSubvoxels)
|
||||
+std::max(0.15*qPerMetre,irregularity)+baseFlare+ceilingFlare;
|
||||
+std::max(0.15*qPerMetre,irregularity)+rootTaper+baseFlare+ceilingFlare;
|
||||
const double radialDistanceField=radialDistance-radius;
|
||||
const double topDistance=qz-crossing.topZSubvoxels;
|
||||
const double bottomDistance=crossing.bottomZSubvoxels-qz;
|
||||
@@ -4780,6 +5018,67 @@ private:
|
||||
return ApplyResolvedStrateBoundaryBand(density,X,Y,Z);
|
||||
}
|
||||
|
||||
double PassageColumnNaturalReliefMetres(
|
||||
const FStandalonePassageVoidCrossing& crossing,double qPerMetre) const
|
||||
{
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
constexpr std::array<double,6> HeightFractions{{0.25,0.35,0.45,0.55,0.65,0.75}};
|
||||
constexpr std::size_t DirectionCount=16;
|
||||
const auto smoothStep=[](double value)
|
||||
{
|
||||
value=std::clamp(value,0.0,1.0);
|
||||
return value*value*(3.0-2.0*value);
|
||||
};
|
||||
std::array<double,HeightFractions.size()> meanRadii{};
|
||||
std::size_t heightCount=0;
|
||||
const auto seed=static_cast<std::uint32_t>(Owner?Owner->Seed:0);
|
||||
const double height=static_cast<double>(crossing.topZSubvoxels
|
||||
-crossing.bottomZSubvoxels);
|
||||
if(height<=0.0)return 0.0;
|
||||
for(const double fraction:HeightFractions)
|
||||
{
|
||||
const double qz=crossing.topZSubvoxels-height*fraction;
|
||||
const float z=static_cast<float>(qz/Q);
|
||||
const float scaledZ=z*0.12f;
|
||||
const float driftX=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ,seed^0x434f4c58u);
|
||||
const float driftY=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ,seed^0x434f4c59u);
|
||||
const double axisX=crossing.centerXSubvoxels
|
||||
+std::clamp(static_cast<double>(driftX),-1.0,1.0)*0.35*qPerMetre;
|
||||
const double axisY=crossing.centerYSubvoxels
|
||||
+std::clamp(static_cast<double>(driftY),-1.0,1.0)*0.35*qPerMetre;
|
||||
const double t=(crossing.topZSubvoxels-qz)/height;
|
||||
const double baseFlare=6.0*qPerMetre*smoothStep((t-0.82)/0.18);
|
||||
const double ceilingFlare=2.0*qPerMetre*(1.0-smoothStep(t/0.075));
|
||||
double meanRadius=0.0;
|
||||
for(std::size_t direction=0;direction<DirectionCount;++direction)
|
||||
{
|
||||
const double angle=2.0*3.14159265358979323846*direction/DirectionCount;
|
||||
double radius=crossing.minimumRadiusSubvoxels+1.25*qPerMetre
|
||||
+baseFlare+ceilingFlare;
|
||||
for(int iteration=0;iteration<8;++iteration)
|
||||
{
|
||||
const float sampleX=static_cast<float>((axisX+radius*std::cos(angle))/Q);
|
||||
const float sampleY=static_cast<float>((axisY+radius*std::sin(angle))/Q);
|
||||
const float broadRelief=VF_FieldSurfaceRelief(sampleX*0.12f,
|
||||
sampleY*0.12f,scaledZ,seed^0x434f4c52u);
|
||||
const float surfaceRelief=VF_FieldSurfaceRelief(sampleX,sampleY,z,
|
||||
seed^0x434f4c53u);
|
||||
const double irregularity=1.25*qPerMetre
|
||||
+std::clamp(static_cast<double>(broadRelief),-1.0,1.0)*1.10*qPerMetre
|
||||
+std::clamp(static_cast<double>(surfaceRelief),-1.0,1.0)*0.35*qPerMetre;
|
||||
radius=crossing.minimumRadiusSubvoxels
|
||||
+std::max(0.15*qPerMetre,irregularity)+baseFlare+ceilingFlare;
|
||||
}
|
||||
meanRadius+=radius;
|
||||
}
|
||||
meanRadii[heightCount++]=meanRadius/DirectionCount/qPerMetre;
|
||||
}
|
||||
if(heightCount<2)return 0.0;
|
||||
const auto begin=meanRadii.begin();
|
||||
const auto end=begin+static_cast<std::ptrdiff_t>(heightCount);
|
||||
return *std::max_element(begin,end)-*std::min_element(begin,end);
|
||||
}
|
||||
|
||||
void BuildPassageVoidCrossingPlan()
|
||||
{
|
||||
BuildResolvedBoundaryZIntervals();
|
||||
@@ -4789,7 +5088,8 @@ private:
|
||||
{BuildPassageSpatialIndices();return;}
|
||||
auto& cache=*Owner->PassageVoidCrossingCache;
|
||||
std::lock_guard<std::mutex> lock(cache.mutex);
|
||||
const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v1";
|
||||
const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v2-seed-"
|
||||
+std::to_string(Owner->Seed);
|
||||
const auto cached=cache.byLayout.find(cacheKey);
|
||||
if(cached!=cache.byLayout.end())
|
||||
{PassageVoidCrossings=cached->second;BuildPassageSpatialIndices();return;}
|
||||
@@ -4968,6 +5268,12 @@ private:
|
||||
maximumCrossingZ+5.0*qPerMetre));
|
||||
crossing.bottomZSubvoxels=static_cast<std::int64_t>(std::llround(
|
||||
minimumCrossingZ-6.0*qPerMetre));
|
||||
crossing.naturalSurfaceReliefMetres=PassageColumnNaturalReliefMetres(
|
||||
crossing,qPerMetre);
|
||||
// The support-column audit requires 20 cm of vertical relief.
|
||||
// Add a shallow root flare only when the existing field profile
|
||||
// falls short, so naturally varied legacy columns remain intact.
|
||||
crossing.needsRootTaper=crossing.naturalSurfaceReliefMetres<0.20;
|
||||
}
|
||||
PassageVoidCrossings.push_back(crossing);
|
||||
if(crossing.needsColumn)
|
||||
@@ -4975,7 +5281,7 @@ private:
|
||||
const double coreRadius=crossing.nominalRadiusSubvoxels/qPerMetre;
|
||||
const double minimumWall=coreRadius-crossing.maximumRouteOffsetMetres
|
||||
-crossing.maximumBoreRadiusMetres;
|
||||
std::printf("[PassageVoidColumnPlan] id=%s segments=%zu..%zu unsupported_samples=%llu span_m=%.3f threshold_m=%.3f connected=%d/%d axis_m=(%.3f,%.3f) z_m=%.3f..%.3f route_radius_m=%.3f bore_radius_m=%.3f min_radius_m=%.3f nominal_radius_m=%.3f max_radius_m=%.3f nominal_wall_m=%.3f flare_m=6.000 status=%s\n",
|
||||
std::printf("[PassageVoidColumnPlan] id=%s segments=%zu..%zu unsupported_samples=%llu span_m=%.3f threshold_m=%.3f connected=%d/%d axis_m=(%.3f,%.3f) z_m=%.3f..%.3f route_radius_m=%.3f bore_radius_m=%.3f min_radius_m=%.3f nominal_radius_m=%.3f max_radius_m=%.3f nominal_wall_m=%.3f natural_relief_m=%.3f root_taper=%d flare_m=6.000 status=%s\n",
|
||||
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
||||
static_cast<unsigned long long>(crossing.unsupportedSamples),gapLength,
|
||||
shortSpanMaximumMetres,static_cast<int>(crossing.connectedAtStart),
|
||||
@@ -4985,6 +5291,7 @@ private:
|
||||
crossing.maximumRouteOffsetMetres,crossing.maximumBoreRadiusMetres,
|
||||
crossing.minimumRadiusSubvoxels/qPerMetre,coreRadius,
|
||||
crossing.maximumRadiusSubvoxels/qPerMetre,minimumWall,
|
||||
crossing.naturalSurfaceReliefMetres,static_cast<int>(crossing.needsRootTaper),
|
||||
crossing.connectedAtStart&&crossing.connectedAtEnd?"PLANNED":"UNATTACHED");
|
||||
}
|
||||
else
|
||||
@@ -10801,6 +11108,7 @@ private:
|
||||
std::vector<FPassageFeatureNode> PassageFeatureNodes;
|
||||
bool PassageFeatureBoundsFailOpen=false;
|
||||
std::vector<std::vector<FPassageNearestNode>> PassageNearestNodes;
|
||||
std::vector<std::vector<FPassageLaterBoreSource>> PassageLaterBoreSources;
|
||||
std::vector<FPassageProofBound> PassageProofBounds;
|
||||
std::vector<FPassageProofNode> PassageProofNodes;
|
||||
std::vector<std::vector<std::uint8_t>> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags;
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render arrival, departure, and mid-tunnel views for the four-strate owner plan."""
|
||||
import json
|
||||
import importlib.util
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l67/captures-run"
|
||||
BIN = ROOT / "Saved/Linux/l67/bin-final"
|
||||
CAPTURES = Path(__file__).resolve().parent / "captures"
|
||||
HEADLESS = BIN / "VoxelForgeCoreHeadless-l67-final-clang"
|
||||
RENDERER = BIN / "VoxelForgeOffscreenCapture-l67-final-clang"
|
||||
PNG_DIR = OUT / "capture-png"
|
||||
RECIPE_INPUTS = (
|
||||
("passage-0-1", ROOT / "Standalone/data/owner_world.json",
|
||||
"surface-world", "SurfaceWorld", "upper"),
|
||||
("passage-1-2", ROOT / "Standalone/data/owner_world.json",
|
||||
"surface-world", "FloatingIslands", "upper"),
|
||||
("passage-2-3", ROOT / "Standalone/data/owner_world.json",
|
||||
"surface-world", "TunnelNetwork", "upper"),
|
||||
)
|
||||
SUBVOXELS_PER_VOXEL = 256.0
|
||||
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||
ring_profile = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ring_profile)
|
||||
|
||||
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
ENV = os.environ.copy()
|
||||
ENV["LP_NUM_THREADS"] = "12"
|
||||
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||
PARSER.add_argument("--view", choices=("arrival-mouth", "departure-mouth", "mid-tunnel"))
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"capture-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L35CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
return log
|
||||
|
||||
|
||||
def add(a, b):
|
||||
return tuple(x + y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(x - y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def mul(a, scale):
|
||||
return tuple(x * scale for x in a)
|
||||
|
||||
|
||||
def norm(a):
|
||||
length = math.sqrt(sum(x * x for x in a))
|
||||
if length <= 1.0e-9:
|
||||
raise ValueError("passage has a degenerate camera tangent")
|
||||
return tuple(x / length for x in a)
|
||||
|
||||
|
||||
def interpolate_route(points, distance):
|
||||
remaining = max(0.0, distance)
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length = math.sqrt(sum(x * x for x in delta))
|
||||
if length <= 1.0e-9:
|
||||
continue
|
||||
if remaining <= length:
|
||||
t = remaining / length
|
||||
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||
radius = a[1] + (b[1] - a[1]) * t
|
||||
return pos, radius
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
|
||||
def route_frame(points, distance):
|
||||
center, radius = interpolate_route(points, distance)
|
||||
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||
after, _ = interpolate_route(points, distance + 0.25)
|
||||
tangent = norm(sub(after, before))
|
||||
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||
side = (-horizontal[1], horizontal[0], 0.0)
|
||||
return center, radius, tangent, side
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
delta = sub(target, camera)
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||
return yaw, pitch
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def load_reference_cameras():
|
||||
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
pattern = re.compile(
|
||||
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||
values = {}
|
||||
for name, camera, target, clip in pattern.findall(
|
||||
log.read_text(encoding="utf-8", errors="replace")):
|
||||
parse = lambda value: tuple(float(part) for part in value.split(","))
|
||||
values[name] = (parse(camera), parse(target), None if clip == "none" else parse(clip))
|
||||
return values
|
||||
|
||||
|
||||
REFERENCE_CAMERAS = {}
|
||||
|
||||
|
||||
def terrain_surface_near(mesh, x, y, expected_height):
|
||||
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||
_, triangles, _ = ring_profile.read_scene(
|
||||
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
hits = []
|
||||
for dy in range(-4, 5):
|
||||
for dx in range(-4, 5):
|
||||
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||
preferred_height=expected_height)
|
||||
if hit is not None:
|
||||
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||
if not hits:
|
||||
return None
|
||||
distance, _, sample_x, sample_y, height = min(hits)
|
||||
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||
return None
|
||||
return sample_x, sample_y, height
|
||||
|
||||
|
||||
def load_layout(name, source, recipe):
|
||||
path = OUT / f"layout-{name}.json"
|
||||
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def select_passage(layout, mouth_generator, preferred_side):
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
preferred_candidates = []
|
||||
fallback_candidates = []
|
||||
for passage in layout["passages"]:
|
||||
upper = strata[int(passage["upper_strate_index"])]
|
||||
lower = strata[int(passage["lower_strate_index"])]
|
||||
sides = []
|
||||
if upper["generator_kind"] == mouth_generator:
|
||||
sides.append("upper")
|
||||
if lower["generator_kind"] == mouth_generator:
|
||||
sides.append("lower")
|
||||
if preferred_side in sides:
|
||||
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||
elif sides:
|
||||
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||
candidates = preferred_candidates or fallback_candidates
|
||||
if not candidates:
|
||||
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||
|
||||
|
||||
def make_views(layout, passage, reverse):
|
||||
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||
controls = [
|
||||
(tuple(value * q_to_m for value in point["position_q"]),
|
||||
point["radius_q"] * q_to_m)
|
||||
for point in passage["control_points"]
|
||||
]
|
||||
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||
path = controls[:lower_index + 1]
|
||||
if len(path) < 2:
|
||||
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||
if reverse:
|
||||
path.reverse()
|
||||
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||
for a, b in zip(path, path[1:])]
|
||||
total = sum(lengths)
|
||||
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||
entry_distance = min(6.0, total * 0.25)
|
||||
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||
|
||||
def exterior_view(prefix, center, radius, outward, target):
|
||||
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||
eye_height = center[2] - radius + 1.6
|
||||
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||
center[1] + horizontal_outward[1] * 5.5,
|
||||
eye_height)
|
||||
yaw, pitch = camera_angles(eye, target)
|
||||
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||
|
||||
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||
ground["focus"] = entrance
|
||||
ground["outside_camera"] = True
|
||||
|
||||
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||
center[2] - radius + 1.6)
|
||||
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||
target = (target_pos[0], target_pos[1],
|
||||
target_pos[2] - target_radius + 1.6)
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
plane = (side[0], side[1], 0.0,
|
||||
-(side[0] * center[0] + side[1] * center[1]))
|
||||
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||
"focus": center, "mouth": center}
|
||||
|
||||
# Place each interface view at its true route endpoint. The endpoint camera
|
||||
# is a side cutaway so the solid terrain in front of an underground mouth
|
||||
# cannot hide the floor-to-tunnel connection.
|
||||
arrival_distance = 0.0
|
||||
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||
path, arrival_distance)
|
||||
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||
arrival_tangent, arrival_side, arrival_distance,
|
||||
min(total, arrival_distance + 12.0))
|
||||
arrival["camera_location"] = "passage-air"
|
||||
arrival["focus_radius"] = 20.0
|
||||
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||
arrival["camera"], arrival["target"])
|
||||
|
||||
entry_camera_distance = min(8.0, total * 0.1)
|
||||
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||
path, entry_camera_distance)
|
||||
# Player-height view from just inside the passage, aimed through its actual
|
||||
# interface mouth so the floor, bore, and opening share one frame.
|
||||
entry_camera = (entry_center[0], entry_center[1],
|
||||
entry_center[2] - entry_radius + 1.6)
|
||||
entry_target_pos, entry_target_radius = interpolate_route(
|
||||
path, min(total, entry_distance + 14.0))
|
||||
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||
entry_camera[2] - 2.5)
|
||||
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||
"clip": None, "camera_xy": entry_camera[:2],
|
||||
"focus": entry_center, "mouth": entrance, "outside_camera": False,
|
||||
"camera_location": "passage-air", "focus_radius": 20.0,
|
||||
"eye_height_z": entry_camera[2]}
|
||||
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||
ground["focus_radius"] = 20.0
|
||||
middle_view["focus_radius"] = 20.0
|
||||
middle_camera, _ = interpolate_route(path, max(0.0, middle_distance - 6.0))
|
||||
middle_view["camera"] = middle_camera
|
||||
middle_target_pos, _ = interpolate_route(path, min(total, middle_distance + 9.0))
|
||||
middle_view["target"] = (middle_target_pos[0], middle_target_pos[1], middle[2])
|
||||
# The camera sits in the passage air. Remove the near wall to open a
|
||||
# longitudinal section and cap the remaining rock at that cut.
|
||||
middle_cut_point = (middle[0] - middle_side[0] * 2.5,
|
||||
middle[1] - middle_side[1] * 2.5, middle[2])
|
||||
middle_view["clip"] = (*middle_side,
|
||||
-sum(middle_side[axis] * middle_cut_point[axis] for axis in range(3)))
|
||||
middle_view["camera_location"] = "passage-air"
|
||||
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||
middle_view["camera"], middle_view["target"])
|
||||
return (ground, entry_view, middle_view, arrival), total, lower_index
|
||||
|
||||
|
||||
def export_capture(scene_name, source, recipe, passage, endpoint, view):
|
||||
stem = f"{passage['stable_id'].replace('/', '-')}-{endpoint}"
|
||||
view["name"] = stem
|
||||
mesh = OUT / f"{stem}.vfmesh"
|
||||
png = PNG_DIR / f"{stem}.png"
|
||||
jpg = CAPTURES / f"{stem}.jpg"
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
camera = view["camera"]
|
||||
export_camera = view.get("focus", camera)
|
||||
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||
render_clip = view["clip"]
|
||||
surface = None
|
||||
if view.get("outside_camera", False):
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
if surface is None:
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1], view["eye_height_z"])
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
elif render_clip is not None:
|
||||
camera = view["camera"]
|
||||
yaw, pitch = camera_angles(camera, view["target"])
|
||||
print(f"[L67CaptureCamera] view={stem} camera_m=({fmt(camera)}) "
|
||||
f"target_m=({fmt(view['target'])}) "
|
||||
f"clip={fmt(render_clip) if render_clip is not None else 'none'} fov_deg=80",
|
||||
flush=True)
|
||||
render = [RENDERER, mesh, png, "--size=1600x900", "--fov-deg=80",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=off", "--shade=readable", "--headlamp=on", "--height-tint=on",
|
||||
f"--height-center-m={view['target'][2]:.6f}", "--height-range-m=8", "--tile-edges=off"]
|
||||
if view.get("focus_radius", 24.0) is not None:
|
||||
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||
if render_clip is not None:
|
||||
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||
run(f"{stem}-render", render)
|
||||
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||
size = jpg.stat().st_size if jpg.exists() else 0
|
||||
print(f"[L67CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
if not 0 < size <= 300_000:
|
||||
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||
|
||||
|
||||
for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS:
|
||||
if OPTIONS.scene and scene_name != OPTIONS.scene:
|
||||
continue
|
||||
layout = load_layout(scene_name, source, recipe)
|
||||
passage, _ = select_passage(layout, mouth_generator, preferred_side)
|
||||
forward_views, route_length, lower_index = make_views(layout, passage, False)
|
||||
reverse_views, _, _ = make_views(layout, passage, True)
|
||||
print(f"[L67CapturePassage] interface={scene_name.removeprefix('passage-')} "
|
||||
f"id={passage['stable_id']} route_m={route_length:.3f} "
|
||||
f"control_points={lower_index + 1}", flush=True)
|
||||
endpoint_views = (
|
||||
("arrival-mouth", forward_views[0] if scene_name == "passage-0-1" else forward_views[1]),
|
||||
("departure-mouth", reverse_views[3]),
|
||||
)
|
||||
for endpoint, view in endpoint_views:
|
||||
if OPTIONS.view and endpoint != OPTIONS.view:
|
||||
continue
|
||||
export_capture(scene_name, source, recipe, passage, endpoint, view)
|
||||
if scene_name == "passage-1-2" and (OPTIONS.view is None or OPTIONS.view == "mid-tunnel"):
|
||||
export_capture(scene_name, source, recipe, passage, "mid-tunnel", forward_views[2])
|
||||
@@ -0,0 +1,81 @@
|
||||
# L67 — four-strate passage walkability
|
||||
|
||||
## Cause and fix
|
||||
|
||||
The FI, TN, and VS paths use the compiled analytic base evaluator and then the shared strate composer. The “native” 5/5 result was the same composed field with resolved passages removed; it was not a raw-generator probe. The band stage is therefore not the difference. The passage carve and the support/capsule gates are.
|
||||
|
||||
- **0–1 SW→FI:** at the failed probe `(21.366, -11.480, -726.367)`, passage 0 first restored support, then passage 1’s bore changed density to `+1.613` (`carve_sdf=-0.292`), removing it. The shared composer now defers that support only when a physically overlapping later bore can reach it, then applies it after the later carve. The column’s natural relief is predictor-driven and tapers at the root; relief rose from `0.172 m` to `0.219 m`.
|
||||
- **1–2 FI→TN:** at `(2.419, -13.433, -735.870)`, support restoration was rejected by an adjacent route segment’s player-capsule test (`clearance=-0.245 m`, required `0.045 m`). The segment-specific overlap candidates now keep that test local to physically relevant route geometry. The reported `0.2498 m` step came from the audit switching to a neighboring zero crossing on its `0.25 m` scan. Refining and following the same root shows a continuous floor; the step audit now reports `0.000 m`.
|
||||
- **2–3 TN→VS:** at `(-1.365, -15.673, -897.088)`, the same capsule gate rejected support next to route segment 81 (`clearance=-0.329 m`). The shared segment-overlap handling fixes this link too.
|
||||
|
||||
The L66 `passageIndex>0` / `strates.size()>2` switches are gone. “Internal chain” now means both mouths lie inside their endpoint strata content bands and neither endpoint is a SurfaceWorld landing. Lower-mouth body overlap uses actual 3D distance to the mouth and tread, independent of plan length or passage index.
|
||||
|
||||
## Audit
|
||||
|
||||
All existing thresholds are unchanged. The L66 baseline binary reproduced the original failures.
|
||||
|
||||
| Passage | L66 negative control | L67 result |
|
||||
|---|---|---|
|
||||
| 0–1 SW→FI | 59 unsupported; column relief `0.172 m < 0.20 m` | 0 unsupported / 0 blocked / 0 grade failures; wall/floor ripple `1.49 / 0.06 cm`; column relief `0.219 m`; wall relief `0.267 m` |
|
||||
| 1–2 FI→TN | 3 unsupported; wall/floor ripple `6.78 / 2.90 cm`; apparent `0.2498 m` step | 0 / 0 / 0; wall/floor ripple `2.29 / 1.23 cm`; refined step `0.000 m`; wall relief `0.257 m` |
|
||||
| 2–3 TN→VS | 2 unsupported | 0 / 0 / 0; wall/floor ripple `1.48 / 0.47 cm`; wall relief `0.248 m` |
|
||||
|
||||
All three now pass support, blocked, grade, mouth-binding, ripple, and naturalness gates. The detailed final audit is in [`owner-four-strata-route4.log`](../../../../Saved/Linux/l67/logs/owner-four-strata-route4.log); the measured composition trace is in [`l66-root-refined-audit.log`](../../../../Saved/Linux/l67/logs/l66-root-refined-audit.log).
|
||||
|
||||
## Controls, hashes, and builds
|
||||
|
||||
- The L66 build remains a negative control: it reproduces `59 / 3 / 2` unsupported and the `0.172 m` column relief failure. The expected audit exit is 10.
|
||||
- The pinned fast tier passes: **269 PASS, 5 diagnostic, overall PASS**. The prior L52 surface-world hash comparison is diagnostic because the route output changed; the changed style-1 baseline is [pinned at v212](hash-baselines/owner-hash-surface-world-style1-v212.log). See the [fast summary](../../../../Saved/Linux/l67/verify/fast-summary.json).
|
||||
- Evaluator version is **212** (from 211), required because shared tread composition and route selection changed generated output. Source stamp: `d49aa8d941fb390818569243d7783f3e2799795244a8d7ca48e3e640d4a2876f`.
|
||||
- All four strata produced 12 equal hash rows at each of W1/W8/W12 ([determinism summary](../../../../Saved/Linux/l67/verify/owner-w1-w8-w12-determinism.json)). Clang and GCC builds both pass.
|
||||
- Changed implementation files are `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/src/ShapeWorldInternal.h`, `Standalone/Source/OwnerWorldLoader.h`, `Standalone/Source/StreamBenchmarkPipeline.h`, and `Standalone/Tools/verify/verify.py`. No Unreal `Source/` files changed.
|
||||
|
||||
## Cost
|
||||
|
||||
Three interleaved whole-view pairs per strate, L66→L67 median seconds ([summary](../../../../Saved/Linux/l67/cost/cost-summary.json)):
|
||||
|
||||
| Strate | L66 | L67 | Change |
|
||||
|---:|---:|---:|---:|
|
||||
| 0 | 1.101 | 1.121 | +1.8% |
|
||||
| 1 | 1.575 | 1.552 | −1.5% |
|
||||
| 2 | 0.581 | 0.602 | +3.6% |
|
||||
| 3 | 0.216 | 0.212 | −1.9% |
|
||||
|
||||
Strate 2’s samples overlap (`0.582–0.600 s`) and paired results are within observed run variation. The route chooser now selects the shortest candidate within `0.10 m` of maximum sampled tread clearance, with tangent alignment as a tie-breaker. It also skips an extra closure turn when mouth separation exceeds the tread envelope; the FI→TN route is `114.231 m` rather than the earlier `165.799 m` candidate.
|
||||
|
||||
## Captures
|
||||
|
||||
Seven JPEGs are in `captures/`; each is under 300 KB. The TN→VS endpoint meshes are sparse in these offscreen views, so those two frames show the nearby cave/strate surface more clearly than the tread.
|
||||
|
||||
- 0–1 arrival [26,342 B](captures/passage-0-1-0-arrival-mouth.jpg), departure [40,012 B](captures/passage-0-1-0-departure-mouth.jpg)
|
||||
- 1–2 arrival [34,298 B](captures/passage-1-2-0-arrival-mouth.jpg), departure [42,450 B](captures/passage-1-2-0-departure-mouth.jpg)
|
||||
- 2–3 arrival [23,937 B](captures/passage-2-3-0-arrival-mouth.jpg), departure [23,380 B](captures/passage-2-3-0-departure-mouth.jpg)
|
||||
- Mid-tunnel, 1–2 [22,392 B](captures/passage-1-2-0-mid-tunnel.jpg)
|
||||
|
||||
## Failed runs
|
||||
|
||||
- The expected L66 negative-control audit exited 10 after reproducing the failures.
|
||||
- One p0 clipped arrival-camera trial exited 3 because the clip plane left two open contours. It was replaced by the successful uncut SurfaceWorld entrance view.
|
||||
- A p2 clipped arrival-camera trial also exited 3 on open contours. The final endpoint image uses the no-clip interior view. Both cap failures are recorded in [`failed-capture-trials.log`](../../../../Saved/Linux/l67/logs/failed-capture-trials.log).
|
||||
- No final capture, Clang/GCC build, determinism run, fast-tier gate, or cost run failed. No run exceeded 30 minutes.
|
||||
|
||||
## Audit controls (follow-up)
|
||||
|
||||
This section supersedes the earlier wall-ripple values; the support, blocked, grade, mouth, and column results remain as reported. The residual-only gauge omitted `pose.radius` and hid an authored-radius beat:
|
||||
|
||||
| Field / gauge | Old gauge (authored radius, linear detrend) | Residual-only gauge |
|
||||
|---|---:|---:|
|
||||
| L66 four-strata field | 6.78 cm | 2.21 cm |
|
||||
| L67 four-strata field | 14.52 cm | 2.29 cm |
|
||||
|
||||
On the old gauge, the geometry change raised 1–2 by 7.74 cm. On L67 geometry, the residual-only gauge then removed 12.23 cm; the reported 6.78→2.29 cm drop therefore masked the radius beat. The final gauge keeps `sampleRadius + 0.5*(left+right)` and cubic-detrends each 32 m window. The 0.18 m is the probe depth inside the authored radius; its former standalone subtraction is removed. The cubic baseline leaves the 0–1 and 2–3 taper profiles below 0.01 cm, while 1–2 retains a 4.77 cm authored-radius beat at 7.06 m. Final ripple results are 1.45 / 7.27 / 0.92 cm: 1–2 fails the unchanged 3.5 cm wall limit. Support, blocked, grade, and mouth-binding checks still pass for all three; overall passage audit is 2/3. See [`audit-control-l67-final-owner.log`](../../../../Saved/Linux/l67/logs/audit-control-l67-final-owner.log) and [`audit-control-authored-radius-all.log`](../../../../Saved/Linux/l67/logs/audit-control-authored-radius-all.log).
|
||||
|
||||
| Negative control under the updated gauge | Measured | Result |
|
||||
|---|---:|---|
|
||||
| L35 wall ribs | 9.75 cm at 6.50 m; limit 3.50 cm | Detected |
|
||||
| Synthetic wall spheres / floor beads | 5.98 / 5.98 cm; limits 3.50 / 2.50 cm | Detected |
|
||||
| Synthetic signed-distance floor step | 0.25 m step; 2.97 cm ripple vs 2.50 cm limit | Detected |
|
||||
| L36 washboard field, five-step root refinement | 6.46 cm; limit 2.50 cm | Detected |
|
||||
| L23 terrace profile | 2.00 m flat then 0.25 m step | Detected |
|
||||
|
||||
No control passed, so no old audit behavior was restored. Logs: [L35 ribs](../../../../Saved/Linux/l67/logs/audit-control-l35-wall-ribs-cubic-audit.log), [L36 washboard](../../../../Saved/Linux/l67/logs/audit-control-l36-washboard-l35field-bisection.log). The L67 synthetic controls are in the final owner log above. One direct link of the current headless code against archived Core libraries failed route setup and was discarded; the control runs used source/Core-matched L35 builds. The follow-up changed only `Standalone/Source/StreamBenchmarkPipeline.h`; Clang Linux build succeeded. No timing run, commit, push, or stash was made.
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure three interleaved L66/L67 whole-view pairs for each owner strate."""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l67/cost"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BASELINE = ROOT / "Saved/Linux/l67/bin-l66-baseline/VoxelForgeCoreHeadless-l67-l66-baseline-clang"
|
||||
CANDIDATE = ROOT / "Saved/Linux/l67/bin-final/VoxelForgeCoreHeadless-l67-final-clang"
|
||||
CAMERAS = {
|
||||
0: "0.000000,0.000000,-374.939453,0.000000,0.000000",
|
||||
1: "25.836914,3.199219,-737.000000,0.000000,0.000000",
|
||||
2: "19.166016,-14.200195,-753.374023,0.000000,0.000000",
|
||||
3: "-20.062500,-5.068359,-900.353516,0.000000,0.000000",
|
||||
}
|
||||
ORDER = ("l66", "l67", "l67", "l66", "l66", "l67")
|
||||
PREFIXES = (
|
||||
"[PipelineFullDetailHash]", "[PipelineWholeViewHash]",
|
||||
"[PipelineLODHash]", "[PipelineExactQueryHash]",
|
||||
)
|
||||
ELAPSED_RE = re.compile(r"\belapsed_ms=([0-9.]+)")
|
||||
WHOLE_VIEW_RE = re.compile(r"^\[StreamPipeline\].*?whole_view_ready=([0-9.]+)s", re.M)
|
||||
ENV = os.environ.copy()
|
||||
|
||||
|
||||
def run(strate, repetition, variant):
|
||||
binary = BASELINE if variant == "l66" else CANDIDATE
|
||||
name = f"strate{strate}-pair{repetition // 2 + 1}-{variant}"
|
||||
log_path = OUT / f"{name}.log"
|
||||
layout_path = OUT / f"{name}.json"
|
||||
command = [
|
||||
str(binary), str(DATA), "--workers=1", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||
"--clip-radius=1", "--max-clip-level=8", "--max-level=0",
|
||||
"--recipe=surface-world", "--stream-hash", "--max-clip-level=0",
|
||||
"--render-distance-chunks=0", f"--camera={CAMERAS[strate]}",
|
||||
f"--resolved-layout-output={layout_path}",
|
||||
]
|
||||
wrapped = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *command,
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log_path.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - start
|
||||
lines = log_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
hashes = [line for line in lines if line.startswith(PREFIXES)]
|
||||
pipeline_elapsed = [float(value) for line in lines
|
||||
if line.startswith("[PipelineWholeViewHash]")
|
||||
for value in ELAPSED_RE.findall(line)]
|
||||
whole_view_match = WHOLE_VIEW_RE.search("\n".join(lines))
|
||||
record = {
|
||||
"strate": strate, "pair_order": repetition, "variant": variant,
|
||||
"elapsed_seconds": elapsed, "pipeline_elapsed_ms": pipeline_elapsed,
|
||||
"whole_view_seconds": float(whole_view_match.group(1))
|
||||
if whole_view_match else None,
|
||||
"exit_code": result.returncode, "hash_rows": len(hashes),
|
||||
"log": str(log_path.relative_to(ROOT)),
|
||||
}
|
||||
print(json.dumps(record, sort_keys=True), flush=True)
|
||||
return record
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
results = []
|
||||
for strate in range(4):
|
||||
for repetition, variant in enumerate(ORDER):
|
||||
record = run(strate, repetition, variant)
|
||||
results.append(record)
|
||||
if record["exit_code"] != 0 or record["hash_rows"] != 12:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
strata = {}
|
||||
for strate in range(4):
|
||||
values = {variant: [row["whole_view_seconds"] for row in results
|
||||
if row["strate"] == strate and row["variant"] == variant
|
||||
and row["whole_view_seconds"] is not None]
|
||||
for variant in ("l66", "l67")}
|
||||
if all(len(values[variant]) == 3 for variant in ("l66", "l67")):
|
||||
l66 = sorted(values["l66"])[1]
|
||||
l67 = sorted(values["l67"])[1]
|
||||
strata[str(strate)] = {"l66_seconds": values["l66"],
|
||||
"l67_seconds": values["l67"], "l66_median_seconds": l66,
|
||||
"l67_median_seconds": l67,
|
||||
"change_percent": (l67 / l66 - 1.0) * 100.0}
|
||||
summary = {"pairs_per_strate": 3, "order": ORDER, "strata": strata,
|
||||
"runs": results,
|
||||
"complete": len(results) == 24
|
||||
and all(x["exit_code"] == 0 and x["hash_rows"] == 12
|
||||
for x in results)}
|
||||
(OUT / "cost-summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
print(f"[L67CostSummary] complete={summary['complete']} runs={len(results)}",
|
||||
flush=True)
|
||||
return 0 if summary["complete"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare owner four-strata W12 hashes with the pinned W1/W8 cameras."""
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l67"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARY = OUT / "bin-final/VoxelForgeCoreHeadless-l67-final-clang"
|
||||
HASH_RE = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
CAMERA_RE = re.compile(r"camera_m=\(([-0-9.]+),([-0-9.]+),([-0-9.]+)\)")
|
||||
RESULTS = []
|
||||
|
||||
|
||||
def hashes(path):
|
||||
return HASH_RE.findall(path.read_text(encoding="utf-8", errors="replace"))
|
||||
|
||||
|
||||
def main():
|
||||
logs = OUT / "logs"
|
||||
layouts = OUT / "layouts"
|
||||
logs.mkdir(parents=True, exist_ok=True)
|
||||
layouts.mkdir(parents=True, exist_ok=True)
|
||||
for strate in range(4):
|
||||
w1_path = OUT / "verify" / f"owner-new-plan-clang-strate{strate}-w1.log"
|
||||
w8_path = OUT / "verify" / f"owner-new-plan-clang-strate{strate}-w8.log"
|
||||
match = CAMERA_RE.search(w1_path.read_text(encoding="utf-8", errors="replace"))
|
||||
if not match:
|
||||
raise RuntimeError(f"camera not found in {w1_path}")
|
||||
camera = ",".join(match.groups()) + ",0.000000,0.000000"
|
||||
log_path = logs / f"owner-new-plan-clang-strate{strate}-w12-final.log"
|
||||
layout_path = layouts / f"owner-new-plan-clang-strate{strate}-w12-final.json"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", str(BINARY), str(DATA), "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=1", "--max-clip-level=8",
|
||||
"--max-level=0", "--recipe=surface-world", "--stream-hash",
|
||||
"--max-clip-level=0", "--render-distance-chunks=0", f"--camera={camera}",
|
||||
f"--resolved-layout-output={layout_path}",
|
||||
]
|
||||
with log_path.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
current = hashes(log_path)
|
||||
w1 = hashes(w1_path)
|
||||
w8 = hashes(w8_path)
|
||||
passed = result.returncode == 0 and len(current) == 12 \
|
||||
and current == w1 == w8
|
||||
row = {"strate": strate, "exit_code": result.returncode,
|
||||
"w1_hashes": len(w1), "w8_hashes": len(w8),
|
||||
"w12_hashes": len(current), "w1_w8_w12_equal": passed,
|
||||
"log": str(log_path.relative_to(ROOT))}
|
||||
RESULTS.append(row)
|
||||
print(json.dumps(row, sort_keys=True), flush=True)
|
||||
summary = {"status": "PASS" if all(row["w1_w8_w12_equal"] for row in RESULTS)
|
||||
else "FAIL", "strata": RESULTS}
|
||||
(OUT / "verify" / "owner-w1-w8-w12-determinism.json").write_text(
|
||||
json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L67OwnerW1W8W12] status={summary['status']}", flush=True)
|
||||
return 0 if summary["status"] == "PASS" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -43,8 +43,10 @@ struct FStandalonePassageVoidCrossing
|
||||
std::int64_t minimumRadiusSubvoxels=0;
|
||||
std::int64_t nominalRadiusSubvoxels=0;
|
||||
std::int64_t maximumRadiusSubvoxels=0;
|
||||
double naturalSurfaceReliefMetres=0.0;
|
||||
double maximumRouteOffsetMetres=0.0;
|
||||
double maximumBoreRadiusMetres=0.0;
|
||||
bool needsRootTaper=false;
|
||||
};
|
||||
|
||||
struct FStandalonePassageVoidCrossingCache
|
||||
|
||||
@@ -2357,19 +2357,22 @@ namespace StandaloneStreamBenchmark
|
||||
{{0.0,-supportProbeRadiusMetres}}}};
|
||||
std::uint32_t supportedProbes=0;
|
||||
std::uint32_t nativeSupportedProbes=0;
|
||||
double firstUnsupportedX=x,firstUnsupportedY=y,lastUnsupportedDensity=0.0;
|
||||
double firstUnsupportedX=x,firstUnsupportedY=y,
|
||||
firstUnsupportedZ=footZ,lastUnsupportedDensity=0.0;
|
||||
std::array<bool,5> supportMask{};
|
||||
for(std::size_t offsetIndex=0;offsetIndex<supportOffsets.size();++offsetIndex)
|
||||
{
|
||||
const auto& offset=supportOffsets[offsetIndex];
|
||||
const double supportX=x+offset[0],supportY=y+offset[1];
|
||||
bool supported=false;
|
||||
double offsetSampleZ=footZ;
|
||||
for(double depth=supportStartMetres;
|
||||
depth<=supportMaximumMetres+1.0e-6;depth+=supportStepMetres)
|
||||
{
|
||||
++audit.supportSamples;
|
||||
offsetSampleZ=(footZ-depth)/coreMetres;
|
||||
lastUnsupportedDensity=Field.densityAt({supportX/coreMetres,
|
||||
supportY/coreMetres,(footZ-depth)/coreMetres});
|
||||
supportY/coreMetres,offsetSampleZ});
|
||||
if(lastUnsupportedDensity<0.0)
|
||||
{
|
||||
supported=true;break;
|
||||
@@ -2384,7 +2387,11 @@ namespace StandaloneStreamBenchmark
|
||||
{++nativeSupportedProbes;break;}
|
||||
}
|
||||
if(supported){++supportedProbes;supportMask[offsetIndex]=true;}
|
||||
else{firstUnsupportedX=supportX;firstUnsupportedY=supportY;}
|
||||
else
|
||||
{
|
||||
firstUnsupportedX=supportX;firstUnsupportedY=supportY;
|
||||
firstUnsupportedZ=offsetSampleZ*coreMetres;
|
||||
}
|
||||
}
|
||||
if(nativeField&&segmentAudit
|
||||
&&nativeSupportedProbes<minimumSupportProbes)
|
||||
@@ -2414,17 +2421,18 @@ namespace StandaloneStreamBenchmark
|
||||
{
|
||||
audit.hasDeepUnsupported=true;
|
||||
audit.firstDeepUnsupported={firstUnsupportedX,firstUnsupportedY,
|
||||
footZ,lastUnsupportedDensity};
|
||||
firstUnsupportedZ,lastUnsupportedDensity};
|
||||
audit.firstDeepSupportMask=supportMask;
|
||||
}
|
||||
if(audit.firstUnsupportedCount<audit.firstUnsupported.size())
|
||||
{
|
||||
const std::size_t sample=audit.firstUnsupportedCount++;
|
||||
audit.firstUnsupported[sample]={
|
||||
firstUnsupportedX,firstUnsupportedY,footZ,lastUnsupportedDensity};
|
||||
firstUnsupportedX,firstUnsupportedY,firstUnsupportedZ,
|
||||
lastUnsupportedDensity};
|
||||
audit.firstSupportMasks[sample]=supportMask;
|
||||
std::printf("[ResolvedWalkDispatch] kind=unsupported xyz_m=(%.3f,%.3f,%.3f) support_probes=%u/5 mask=%d%d%d%d%d path=%s\n",
|
||||
firstUnsupportedX,firstUnsupportedY,footZ,supportedProbes,
|
||||
firstUnsupportedX,firstUnsupportedY,firstUnsupportedZ,supportedProbes,
|
||||
supportMask[0],supportMask[1],supportMask[2],supportMask[3],supportMask[4],
|
||||
Field.densityDispatchPathAt({firstUnsupportedX/coreMetres,
|
||||
firstUnsupportedY/coreMetres,footZ/coreMetres}).c_str());
|
||||
@@ -2467,6 +2475,8 @@ namespace StandaloneStreamBenchmark
|
||||
std::uint64_t floorSamples=0,floorExpectedSamples=0;
|
||||
bool controlDetected=false,passed=false;
|
||||
bool floorControlDetected=false;
|
||||
double floorStepControlAmplitudeMetres=0.0;
|
||||
bool floorStepControlDetected=false;
|
||||
};
|
||||
constexpr double MaximumFlatRunMetres=8.0;
|
||||
constexpr double MinimumWallReliefMetres=0.20;
|
||||
@@ -2859,21 +2869,55 @@ namespace StandaloneStreamBenchmark
|
||||
double startDistance,std::vector<std::array<double,2>>& output)
|
||||
{
|
||||
if(values.size()<64)return;
|
||||
// Remove a smooth mouth taper across each 32 m window while
|
||||
// preserving the authored radius, where regular wall ribs can live.
|
||||
// A cubic baseline tracks the broad flare without fitting the
|
||||
// 0.5–8 m periods that this audit is meant to detect.
|
||||
constexpr std::size_t Degree=3;
|
||||
std::array<std::array<double,Degree+2>,Degree+1> normal{};
|
||||
const double meanIndex=0.5*static_cast<double>(values.size()-1);
|
||||
double mean=0.0,xy=0.0,xx=0.0;
|
||||
for(double value:values)mean+=value;
|
||||
mean/=static_cast<double>(values.size());
|
||||
const double halfSpan=std::max(1.0,meanIndex);
|
||||
for(std::size_t i=0;i<values.size();++i)
|
||||
{
|
||||
const double x=(static_cast<double>(i)-meanIndex)*SampleSpacingMetres;
|
||||
xy+=x*(values[i]-mean);xx+=x*x;
|
||||
const double x=(static_cast<double>(i)-meanIndex)/halfSpan;
|
||||
std::array<double,2*Degree+1> powers{};
|
||||
powers[0]=1.0;
|
||||
for(std::size_t power=1;power<powers.size();++power)
|
||||
powers[power]=powers[power-1]*x;
|
||||
for(std::size_t row=0;row<=Degree;++row)
|
||||
{
|
||||
for(std::size_t column=0;column<=Degree;++column)
|
||||
normal[row][column]+=powers[row+column];
|
||||
normal[row][Degree+1]+=values[i]*powers[row];
|
||||
}
|
||||
}
|
||||
for(std::size_t column=0;column<=Degree;++column)
|
||||
{
|
||||
std::size_t pivot=column;
|
||||
for(std::size_t row=column+1;row<=Degree;++row)
|
||||
if(std::abs(normal[row][column])>
|
||||
std::abs(normal[pivot][column]))pivot=row;
|
||||
if(std::abs(normal[pivot][column])<=1.0e-12)return;
|
||||
if(pivot!=column)std::swap(normal[pivot],normal[column]);
|
||||
const double divisor=normal[column][column];
|
||||
for(std::size_t value=column;value<=Degree+1;++value)
|
||||
normal[column][value]/=divisor;
|
||||
for(std::size_t row=0;row<=Degree;++row)
|
||||
{
|
||||
if(row==column)continue;
|
||||
const double scale=normal[row][column];
|
||||
for(std::size_t value=column;value<=Degree+1;++value)
|
||||
normal[row][value]-=scale*normal[column][value];
|
||||
}
|
||||
}
|
||||
const double slope=xx>0.0?xy/xx:0.0;
|
||||
for(std::size_t i=0;i<values.size();++i)
|
||||
{
|
||||
const double x=(static_cast<double>(i)-meanIndex)*SampleSpacingMetres;
|
||||
const double x=(static_cast<double>(i)-meanIndex)/halfSpan;
|
||||
double trend=normal[Degree][Degree+1];
|
||||
for(std::size_t coefficient=Degree;coefficient-->0;)
|
||||
trend=trend*x+normal[coefficient][Degree+1];
|
||||
output.push_back({startDistance+static_cast<double>(i)*SampleSpacingMetres,
|
||||
values[i]-mean-slope*x});
|
||||
values[i]-trend});
|
||||
}
|
||||
};
|
||||
const auto appendDetrendedPairs=[](
|
||||
@@ -2915,6 +2959,50 @@ namespace StandaloneStreamBenchmark
|
||||
audit.floorControlAmplitudeMetres=audit.controlAmplitudeMetres;
|
||||
audit.floorControlDetected=audit.floorControlAmplitudeMetres
|
||||
>=FloorRippleAmplitudeLimitMetres;
|
||||
// A signed-distance floor with a real 0.25 m ledge must still trip
|
||||
// the same floor-ripple gauge after the crossing-refinement change.
|
||||
std::vector<std::array<double,2>> floorStepResiduals;
|
||||
floorStepResiduals.reserve(static_cast<std::size_t>(WindowLengthMetres/
|
||||
SampleSpacingMetres)+1);
|
||||
constexpr double FloorStepControlProbeSpacingMetres=0.25;
|
||||
for(std::size_t sample=0;
|
||||
sample<=static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres);
|
||||
++sample)
|
||||
{
|
||||
const double distance=static_cast<double>(sample)*SampleSpacingMetres;
|
||||
const double stepHeight=distance>=16.0?0.25:0.0;
|
||||
double previousDensity=0.0,previousZ=0.0;
|
||||
bool havePrevious=false,haveFloor=false;
|
||||
double bestFloor=0.0,bestDistance=std::numeric_limits<double>::infinity();
|
||||
for(int vertical=0;vertical<7;++vertical)
|
||||
{
|
||||
const double z=-1.0+vertical*FloorStepControlProbeSpacingMetres;
|
||||
const double density=z-stepHeight;
|
||||
if(havePrevious&&previousDensity<0.0&&density>=0.0)
|
||||
{
|
||||
double lowZ=previousZ,highZ=z;
|
||||
for(int refine=0;refine<5;++refine)
|
||||
{
|
||||
const double middleZ=(lowZ+highZ)*0.5;
|
||||
if(middleZ-stepHeight<0.0)lowZ=middleZ;
|
||||
else highZ=middleZ;
|
||||
}
|
||||
const double crossing=(lowZ+highZ)*0.5;
|
||||
const double candidateDistance=std::abs(crossing);
|
||||
if(candidateDistance<bestDistance)
|
||||
{bestDistance=candidateDistance;bestFloor=crossing;haveFloor=true;}
|
||||
}
|
||||
previousDensity=density;previousZ=z;havePrevious=true;
|
||||
}
|
||||
if(haveFloor)floorStepResiduals.push_back({distance,bestFloor});
|
||||
}
|
||||
std::vector<std::array<double,2>> floorStepSamples;
|
||||
appendDetrendedPairs(floorStepResiduals,floorStepSamples);
|
||||
double floorStepPeriod=0.0;
|
||||
audit.floorStepControlAmplitudeMetres=sinusoidAmplitude(
|
||||
floorStepSamples,floorStepPeriod);
|
||||
audit.floorStepControlDetected=audit.floorStepControlAmplitudeMetres
|
||||
>=FloorRippleAmplitudeLimitMetres;
|
||||
const std::array<double,5> windowStarts{{0.03,0.31,0.457,0.61,0.91}};
|
||||
std::vector<std::array<double,2>> coherentSamples;
|
||||
std::vector<std::array<double,2>> coherentFloorSamples;
|
||||
@@ -2955,8 +3043,27 @@ namespace StandaloneStreamBenchmark
|
||||
if(havePrevious&&previousDensity<0.0&&density>=0.0)
|
||||
{
|
||||
const double denominator=density-previousDensity;
|
||||
const double crossing=denominator>1.0e-12
|
||||
double crossing=denominator>1.0e-12
|
||||
?previousZ+(z-previousZ)*(-previousDensity)/denominator:z;
|
||||
// Density can change sharply at a composed floor. Refine
|
||||
// the sign bracket instead of assuming it is linear over
|
||||
// an entire Core voxel; linear interpolation can report a
|
||||
// one-voxel step even when the zero crossing is nearby.
|
||||
double lowZ=previousZ,highZ=z;
|
||||
double lowDensity=previousDensity,highDensity=density;
|
||||
for(int refine=0;refine<5;++refine)
|
||||
{
|
||||
const double middleZ=(lowZ+highZ)*0.5;
|
||||
const double middleDensity=static_cast<double>(Field.densityAt({
|
||||
pose.x/coreMetres,pose.y/coreMetres,middleZ/coreMetres}))
|
||||
*coreMetres;
|
||||
if(middleDensity<0.0)
|
||||
{lowZ=middleZ;lowDensity=middleDensity;}
|
||||
else
|
||||
{highZ=middleZ;highDensity=middleDensity;}
|
||||
}
|
||||
(void)lowDensity;(void)highDensity;
|
||||
crossing=(lowZ+highZ)*0.5;
|
||||
const double candidateDistance=std::abs(crossing-expectedFloor);
|
||||
if(candidateDistance<bestDistance)
|
||||
{bestDistance=candidateDistance;bestFloor=crossing;haveFloor=true;}
|
||||
@@ -2985,6 +3092,9 @@ namespace StandaloneStreamBenchmark
|
||||
{x0-nx*offset,y0-ny*offset,z0}))*coreMetres;
|
||||
if(!std::isfinite(left)||!std::isfinite(right)
|
||||
||left< -0.75||left>0.75||right< -0.75||right>0.75)continue;
|
||||
// The probes sit 0.18 m inside the authored radius. Add that
|
||||
// radius back to the signed wall distance so radius-carried ribs
|
||||
// remain visible; the cubic detrend removes the smooth mouth flare.
|
||||
radii.push_back(sampleRadius+0.5*(left+right));
|
||||
++audit.samples;
|
||||
}
|
||||
@@ -3052,7 +3162,8 @@ namespace StandaloneStreamBenchmark
|
||||
?static_cast<double>(audit.floorSamples)/audit.floorExpectedSamples:0.0;
|
||||
audit.passed=audit.controlDetected&&coverage>=0.75
|
||||
&&audit.localPeakAmplitudeMetres<RippleAmplitudeLimitMetres
|
||||
&&audit.floorControlDetected&&floorCoverage>=0.75
|
||||
&&audit.floorControlDetected&&audit.floorStepControlDetected
|
||||
&&floorCoverage>=0.75
|
||||
&&audit.floorAmplitudeMetres<FloorRippleAmplitudeLimitMetres;
|
||||
return audit;
|
||||
};
|
||||
@@ -3546,6 +3657,12 @@ namespace StandaloneStreamBenchmark
|
||||
meanRadiusByHeight.begin()+heightSamples)
|
||||
-*std::min_element(meanRadiusByHeight.begin(),
|
||||
meanRadiusByHeight.begin()+heightSamples):0.0;
|
||||
std::printf("[PassageColumnReliefProfile] id=%s heights=%zu radii_m=",
|
||||
passage.stableId.c_str(),heightSamples);
|
||||
for(std::size_t height=0;height<heightSamples;++height)
|
||||
std::printf("%s%.3f",height?",":"",
|
||||
meanRadiusByHeight[height]);
|
||||
std::printf("\n");
|
||||
audit.minimumSurfaceRadiusMetres=std::min(audit.minimumSurfaceRadiusMetres,
|
||||
crossingMinimumSurfaceRadius);
|
||||
audit.maximumSurfaceRadiusMetres=std::max(audit.maximumSurfaceRadiusMetres,
|
||||
@@ -3675,7 +3792,7 @@ namespace StandaloneStreamBenchmark
|
||||
};
|
||||
std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0;
|
||||
std::uint64_t mouthProfilePasses=0,ripplePasses=0,rippleControlsDetected=0;
|
||||
std::uint64_t floorRippleControlsDetected=0;
|
||||
std::uint64_t floorRippleControlsDetected=0,floorStepRippleControlsDetected=0;
|
||||
std::uint64_t openAirViolations=0,openAirLegacyViolations=0,openAirSamples=0;
|
||||
std::uint64_t openAirAudits=0,openAirColumnSamples=0,openAirShortSpanSamples=0;
|
||||
std::uint64_t columnCrossings=0,shortGroundedSpans=0,columnConnectedEnds=0;
|
||||
@@ -3767,6 +3884,7 @@ namespace StandaloneStreamBenchmark
|
||||
ripplePasses+=rippleAudit.passed;
|
||||
rippleControlsDetected+=rippleAudit.controlDetected;
|
||||
floorRippleControlsDetected+=rippleAudit.floorControlDetected;
|
||||
floorStepRippleControlsDetected+=rippleAudit.floorStepControlDetected;
|
||||
passagePasses+=passageAudit.passed&&mouthsInRadius&&mouthsInContent
|
||||
&&naturalAudit.passed&&mouthProfileAudit.passed&&rippleAudit.passed;
|
||||
std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n",
|
||||
@@ -3794,7 +3912,7 @@ namespace StandaloneStreamBenchmark
|
||||
mouthProfileAudit.wallFlatStepRunMetres,mouthProfileAudit.wallStepMetres,
|
||||
PassageProfileSpacingMetres,PassageProfilePlateauMinimumMetres,
|
||||
PassageProfileStepThresholdMetres,mouthProfileAudit.passed?"PASS":"FAIL");
|
||||
std::printf("[PassageRippleAudit] id=%s window_m=32.000 spacing_m=0.125 period_m=%.3f amplitude_m=%.4f amplitude_cm=%.2f coherent_period_m=%.3f coherent_amplitude_m=%.4f coherent_amplitude_cm=%.2f local_peak_amplitude_m=%.4f coverage=%llu/%llu threshold_m=0.035 synthetic_spheres_period_m=1.500 synthetic_spheres_amplitude_m=%.4f synthetic_spheres_detected=%s floor_period_m=%.3f floor_amplitude_m=%.4f floor_amplitude_cm=%.2f floor_coherent_period_m=%.3f floor_coherent_amplitude_m=%.4f floor_coverage=%llu/%llu floor_threshold_m=0.025 floor_beaded_control_amplitude_m=%.4f floor_beaded_control_detected=%s status=%s\n",
|
||||
std::printf("[PassageRippleAudit] id=%s window_m=32.000 spacing_m=0.125 period_m=%.3f amplitude_m=%.4f amplitude_cm=%.2f coherent_period_m=%.3f coherent_amplitude_m=%.4f coherent_amplitude_cm=%.2f local_peak_amplitude_m=%.4f coverage=%llu/%llu threshold_m=0.035 synthetic_spheres_period_m=1.500 synthetic_spheres_amplitude_m=%.4f synthetic_spheres_detected=%s floor_period_m=%.3f floor_amplitude_m=%.4f floor_amplitude_cm=%.2f floor_coherent_period_m=%.3f floor_coherent_amplitude_m=%.4f floor_coverage=%llu/%llu floor_threshold_m=0.025 floor_beaded_control_amplitude_m=%.4f floor_beaded_control_detected=%s floor_step_control_amplitude_m=%.4f floor_step_control_detected=%s status=%s\n",
|
||||
passage.stableId.c_str(),rippleAudit.periodMetres,
|
||||
rippleAudit.amplitudeMetres,rippleAudit.amplitudeMetres*100.0,
|
||||
rippleAudit.coherentPeriodMetres,rippleAudit.coherentAmplitudeMetres,
|
||||
@@ -3810,7 +3928,12 @@ namespace StandaloneStreamBenchmark
|
||||
static_cast<unsigned long long>(rippleAudit.floorExpectedSamples),
|
||||
rippleAudit.floorControlAmplitudeMetres,
|
||||
rippleAudit.floorControlDetected?"true":"false",
|
||||
rippleAudit.floorStepControlAmplitudeMetres,
|
||||
rippleAudit.floorStepControlDetected?"true":"false",
|
||||
rippleAudit.passed?"PASS":"FAIL");
|
||||
std::printf("[PassageFloorStepNegativeControl] id=%s source=synthetic_signed_distance_step height_m=0.250 threshold_m=0.025 amplitude_m=%.4f detected=%s\n",
|
||||
passage.stableId.c_str(),rippleAudit.floorStepControlAmplitudeMetres,
|
||||
rippleAudit.floorStepControlDetected?"true":"false");
|
||||
for(std::size_t sample=0;sample<rippleAudit.floorPeakProfile.size();sample+=4)
|
||||
{
|
||||
const auto& value=rippleAudit.floorPeakProfile[sample];
|
||||
@@ -4134,11 +4257,12 @@ namespace StandaloneStreamBenchmark
|
||||
PassageProfilePlateauMinimumMetres,PassageProfileStepThresholdMetres,
|
||||
l23TerraceControlDetected?"DETECTED":"MISSED",
|
||||
mouthProfilePasses==passageCount&&l23TerraceControlDetected?"PASS":"FAIL");
|
||||
std::printf("[PassageRippleAuditSummary] passed=%llu/%llu synthetic_spheres_negative_control=%s max_amplitude_m=0.035 floor_beaded_negative_control=%s max_floor_amplitude_m=0.015 status=%s\n",
|
||||
std::printf("[PassageRippleAuditSummary] passed=%llu/%llu synthetic_spheres_negative_control=%s max_amplitude_m=0.035 floor_beaded_negative_control=%s max_floor_amplitude_m=0.025 floor_step_negative_control=%s status=%s\n",
|
||||
static_cast<unsigned long long>(ripplePasses),
|
||||
static_cast<unsigned long long>(passageCount),
|
||||
rippleControlsDetected==passageCount?"DETECTED":"MISSED",
|
||||
floorRippleControlsDetected==passageCount?"DETECTED":"MISSED",
|
||||
floorStepRippleControlsDetected==passageCount?"DETECTED":"MISSED",
|
||||
ripplePasses==passageCount?"PASS":"FAIL");
|
||||
const bool auditPassed=passageCount>0&&passagePasses==passageCount
|
||||
&&naturalPasses==passageCount&&l23SmoothWallDetected
|
||||
|
||||
@@ -29,6 +29,7 @@ LINUX54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54"
|
||||
LINUX56 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux56"
|
||||
LINUX58 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux58"
|
||||
LINUX60 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux60"
|
||||
LINUX67 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux67"
|
||||
OWNER = ROOT / "Standalone/data/regression/owner_world_2strate_l61.json"
|
||||
OWNER_NEW = ROOT / "Standalone/data/owner_world.json"
|
||||
OWNER_2STRATE_SHA256 = "7b095f9bcc6b90133d5a463f99f0b571133b2c549ce1ecf2d64fe9175e41e183"
|
||||
@@ -623,9 +624,25 @@ def run_hash_pairs(runner: VerificationRunner,
|
||||
l52_baseline = ROOT / "Saved/Linux/l52" / f"owner-hash-{recipe}-style{style}-w12.log"
|
||||
l52_hashes = tuple(HASH_RE.findall(l52_baseline.read_text(
|
||||
encoding="utf-8", errors="replace")))
|
||||
same_l52 = hashes[12] == l52_hashes and len(l52_hashes) == 12
|
||||
runner.require(f"owner-hash-{recipe}-w12-matches-L52", same_l52,
|
||||
f"current={len(hashes[12])} L52={len(l52_hashes)} exact canonical hash rows")
|
||||
if recipe == "surface-world":
|
||||
expected_baseline = LINUX67 / "hash-baselines" / (
|
||||
"owner-hash-surface-world-style1-v212.log")
|
||||
expected_label = "L67-v212"
|
||||
else:
|
||||
expected_baseline = l52_baseline
|
||||
expected_label = "L52"
|
||||
expected_hashes = tuple(HASH_RE.findall(expected_baseline.read_text(
|
||||
encoding="utf-8", errors="replace")))
|
||||
same_expected = hashes[12] == expected_hashes and len(expected_hashes) == 12
|
||||
runner.require(f"owner-hash-{recipe}-w12-matches-{expected_label}", same_expected,
|
||||
f"current={len(hashes[12])} {expected_label}={len(expected_hashes)} exact canonical hash rows")
|
||||
if recipe == "surface-world":
|
||||
diagnostic = {"name": "owner-hash-surface-world-matches-L52-diagnostic",
|
||||
"status": "DIAGNOSTIC",
|
||||
"detail": f"matches={hashes[12] == l52_hashes} L52 rows={len(l52_hashes)}; "
|
||||
"the v212 generated-route change has its own pinned baseline"}
|
||||
runner.steps.append(diagnostic)
|
||||
print(f"[{runner.tier}] {diagnostic['name']} {diagnostic['detail']}", flush=True)
|
||||
baseline = ROOT / "Saved/Linux/l46" / f"owner-hash-{recipe}-style{style}-w12.log"
|
||||
old_hashes = tuple(HASH_RE.findall(baseline.read_text(encoding="utf-8", errors="replace")))
|
||||
same_l46 = hashes[12] == old_hashes and len(old_hashes) == 12
|
||||
|
||||