Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf39712792 | ||
|
|
5ea01988c4 |
No files matched your search
@@ -137,31 +137,11 @@ namespace VoxelForgeCore
|
|||||||
bool passageComposerProfiled = false;
|
bool passageComposerProfiled = false;
|
||||||
std::uint64_t passageTileFeatureCount = 0;
|
std::uint64_t passageTileFeatureCount = 0;
|
||||||
std::uint64_t passageTileRouteSegmentCandidates = 0;
|
std::uint64_t passageTileRouteSegmentCandidates = 0;
|
||||||
std::uint64_t passageTileMouthSegmentCandidates = 0;
|
|
||||||
std::uint64_t resolvedBoundaryBandQueries = 0;
|
std::uint64_t resolvedBoundaryBandQueries = 0;
|
||||||
std::uint64_t passageComposerQueries = 0;
|
std::uint64_t passageComposerQueries = 0;
|
||||||
std::uint64_t passageIterations = 0;
|
std::uint64_t passageIterations = 0;
|
||||||
std::uint64_t passageTileBoundChecks = 0;
|
|
||||||
std::uint64_t passageTileBoundRejects = 0;
|
|
||||||
std::uint64_t passageTileBoundSkippedIterations = 0;
|
|
||||||
std::uint64_t passageUpperMouthFilterChecks = 0;
|
|
||||||
std::uint64_t passageColumnCandidates = 0;
|
|
||||||
std::uint64_t passageColumnDistanceCalls = 0;
|
|
||||||
std::uint64_t passageNegativeBoundChecks = 0;
|
|
||||||
std::uint64_t passageNegativeBoundRejects = 0;
|
|
||||||
std::uint64_t passagePositiveBoundChecks = 0;
|
|
||||||
std::uint64_t passagePositiveBoundRejects = 0;
|
|
||||||
std::uint64_t passageCarveSegmentVisits = 0;
|
std::uint64_t passageCarveSegmentVisits = 0;
|
||||||
std::uint64_t passageSmoothCarveJoins = 0;
|
|
||||||
std::uint64_t passageSupportSegmentVisits = 0;
|
|
||||||
std::uint64_t passageSmoothSupportJoins = 0;
|
|
||||||
std::uint64_t passageSupportCapsuleChecks = 0;
|
|
||||||
std::uint64_t passageLaterBoreChecks = 0;
|
|
||||||
std::uint64_t passageOtherCapsuleChecks = 0;
|
|
||||||
std::uint64_t passageMouthSupportCalls = 0;
|
|
||||||
std::uint64_t passageMouthRouteSegmentChecks = 0;
|
|
||||||
std::uint64_t passageReliefFieldCalls = 0;
|
std::uint64_t passageReliefFieldCalls = 0;
|
||||||
std::uint64_t passageSmoothstepEvaluations = 0;
|
|
||||||
// L99 tile-local swept-tube base and detail passes. These are populated only
|
// L99 tile-local swept-tube base and detail passes. These are populated only
|
||||||
// by the opt-in passage composer profile, except the mesh composition itself.
|
// by the opt-in passage composer profile, except the mesh composition itself.
|
||||||
std::uint64_t passageSweepRouteSegmentCandidates = 0;
|
std::uint64_t passageSweepRouteSegmentCandidates = 0;
|
||||||
@@ -488,7 +468,11 @@ namespace VoxelForgeCore
|
|||||||
// Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges.
|
// 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.
|
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
|
||||||
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
|
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
|
||||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 216;
|
// Version 217 replaces passage supports with flat-floor hollow-shell geometry.
|
||||||
|
// Version 256 widens the smooth union across neighboring shell spans to
|
||||||
|
// remove their regularly spaced end-profile bands.
|
||||||
|
// Version 257 reuses passage chord distances for the shell to avoid duplicate curve work.
|
||||||
|
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 257;
|
||||||
|
|
||||||
enum class FieldChannel : std::uint32_t
|
enum class FieldChannel : std::uint32_t
|
||||||
{
|
{
|
||||||
@@ -1041,7 +1025,6 @@ namespace VoxelForgeCore
|
|||||||
{
|
{
|
||||||
bool hasSurfaceLanding = false;
|
bool hasSurfaceLanding = false;
|
||||||
bool hasFloatingIslandLanding = false;
|
bool hasFloatingIslandLanding = false;
|
||||||
bool usePassageAnchorForColumns = false;
|
|
||||||
bool hasNativeCaveField = false;
|
bool hasNativeCaveField = false;
|
||||||
bool hasNativeLatticeField = false;
|
bool hasNativeLatticeField = false;
|
||||||
};
|
};
|
||||||
@@ -1145,28 +1128,6 @@ namespace VoxelForgeCore
|
|||||||
std::vector<ResolvedLayoutPassagePointV1> controlPoints;
|
std::vector<ResolvedLayoutPassagePointV1> controlPoints;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PassageVoidCrossingAuditV1
|
|
||||||
{
|
|
||||||
std::size_t passageIndex = 0;
|
|
||||||
std::size_t firstSegment = 0;
|
|
||||||
std::size_t lastSegment = 0;
|
|
||||||
std::uint64_t unsupportedSamples = 0;
|
|
||||||
double unsupportedLengthMetres = 0.0;
|
|
||||||
std::array<double, 4> startPointSubvoxels{};
|
|
||||||
std::array<double, 4> endPointSubvoxels{};
|
|
||||||
bool needsColumn = false;
|
|
||||||
bool connectedAtStart = false;
|
|
||||||
bool connectedAtEnd = false;
|
|
||||||
std::array<std::int64_t, 2> columnAxisSubvoxels{};
|
|
||||||
std::int64_t columnTopZSubvoxels = 0;
|
|
||||||
std::int64_t columnBottomZSubvoxels = 0;
|
|
||||||
std::int64_t columnMinimumRadiusSubvoxels = 0;
|
|
||||||
std::int64_t columnNominalRadiusSubvoxels = 0;
|
|
||||||
std::int64_t columnMaximumRadiusSubvoxels = 0;
|
|
||||||
double maximumRouteOffsetMetres = 0.0;
|
|
||||||
double maximumBoreRadiusMetres = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ResolvedLayoutPassageConfigV1
|
struct ResolvedLayoutPassageConfigV1
|
||||||
{
|
{
|
||||||
std::int32_t upperStrateIndex = -1;
|
std::int32_t upperStrateIndex = -1;
|
||||||
@@ -1836,10 +1797,6 @@ namespace VoxelForgeCore
|
|||||||
bool profileMesherStages = false;
|
bool profileMesherStages = false;
|
||||||
// Research-only: count passage-composer work per generated tile.
|
// Research-only: count passage-composer work per generated tile.
|
||||||
bool profilePassageComposer = false;
|
bool profilePassageComposer = false;
|
||||||
// Research-only field comparison. A nonnegative value clamps the source density
|
|
||||||
// entering passage composition to at least -N voxel units for this tile.
|
|
||||||
// A negative value leaves the process-level diagnostic override in control.
|
|
||||||
double diagnosticPassageSourceFloorVoxels = -1.0;
|
|
||||||
// Research-only field comparison. Zero keeps the unbounded passage carve;
|
// Research-only field comparison. Zero keeps the unbounded passage carve;
|
||||||
// a positive value bounds its signed-distance reach in metres. A negative
|
// a positive value bounds its signed-distance reach in metres. A negative
|
||||||
// value leaves the process-level L93 setting in control.
|
// value leaves the process-level L93 setting in control.
|
||||||
@@ -1932,12 +1889,6 @@ namespace VoxelForgeCore
|
|||||||
OreMiningYieldV1 measureOreMiningYieldForSphere(
|
OreMiningYieldV1 measureOreMiningYieldForSphere(
|
||||||
Vec3d centerVoxel, double radiusMetres,
|
Vec3d centerVoxel, double radiusMetres,
|
||||||
double sampleSpacingMetres = 0.25) const;
|
double sampleSpacingMetres = 0.25) const;
|
||||||
// Audit-only view of density-field columns and short spans supporting passage voids.
|
|
||||||
std::vector<PassageVoidCrossingAuditV1> passageVoidCrossings() const;
|
|
||||||
bool isPassageColumnSample(std::size_t passageIndex, std::size_t segmentIndex,
|
|
||||||
Vec3d voxelPosition) const;
|
|
||||||
bool isPassageShortSpanSample(std::size_t passageIndex, std::size_t segmentIndex,
|
|
||||||
Vec3d voxelPosition) const;
|
|
||||||
// Audit-only selector provenance for the same point evaluator; never used by meshing.
|
// Audit-only selector provenance for the same point evaluator; never used by meshing.
|
||||||
std::string densityDispatchPathAt(Vec3d voxelPosition) const;
|
std::string densityDispatchPathAt(Vec3d voxelPosition) const;
|
||||||
// Analytic source distance without the occupancy-preserving near-zero clamp or packed proxy.
|
// Analytic source distance without the occupancy-preserving near-zero clamp or packed proxy.
|
||||||
|
|||||||
@@ -888,10 +888,7 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
|||||||
else if(IsNativeLatticeFieldStratum(x))
|
else if(IsNativeLatticeFieldStratum(x))
|
||||||
x.passageCapabilities.hasNativeLatticeField=true;
|
x.passageCapabilities.hasNativeLatticeField=true;
|
||||||
else if(x.generatorKind=="FloatingIslands")
|
else if(x.generatorKind=="FloatingIslands")
|
||||||
{
|
|
||||||
x.passageCapabilities.hasFloatingIslandLanding=true;
|
x.passageCapabilities.hasFloatingIslandLanding=true;
|
||||||
x.passageCapabilities.usePassageAnchorForColumns=true;
|
|
||||||
}
|
|
||||||
if(i+1<strates.size())
|
if(i+1<strates.size())
|
||||||
{
|
{
|
||||||
const auto& lower=strates[i+1];
|
const auto& lower=strates[i+1];
|
||||||
|
|||||||
@@ -1284,103 +1284,104 @@ namespace VoxelForgeCore
|
|||||||
constexpr double Q=static_cast<double>(WorldAssemblySubvoxelsPerVoxel);
|
constexpr double Q=static_cast<double>(WorldAssemblySubvoxelsPerVoxel);
|
||||||
const double voxelsPerMetre=coreVoxelSizeMicrons>0
|
const double voxelsPerMetre=coreVoxelSizeMicrons>0
|
||||||
?1000000.0/coreVoxelSizeMicrons:4.0;
|
?1000000.0/coreVoxelSizeMicrons:4.0;
|
||||||
const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre;
|
constexpr double WallReliefMetres=0.21;
|
||||||
const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre;
|
constexpr double ShellThicknessMetres=0.75;
|
||||||
|
const double tubeExtentMetres=(WallReliefMetres+ShellThicknessMetres)
|
||||||
|
*voxelsPerMetre;
|
||||||
|
const double qPerMetre=Q*voxelsPerMetre;
|
||||||
const double tileMinimum[3]={static_cast<double>(minimum.X),
|
const double tileMinimum[3]={static_cast<double>(minimum.X),
|
||||||
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
|
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
|
||||||
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||||
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
||||||
for(const auto& passage:layout.passages)
|
for(const auto& passage:layout.passages)
|
||||||
{
|
{
|
||||||
|
if(passage.controlPoints.size()<2)continue;
|
||||||
|
const std::size_t routeEnd=std::clamp<std::size_t>(
|
||||||
|
passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1);
|
||||||
|
std::vector<double> routeDistanceMetres(routeEnd+1,0.0);
|
||||||
|
for(std::size_t pointIndex=1;pointIndex<=routeEnd;++pointIndex)
|
||||||
|
{
|
||||||
|
const auto& a=passage.controlPoints[pointIndex-1].positionSubvoxels;
|
||||||
|
const auto& b=passage.controlPoints[pointIndex].positionSubvoxels;
|
||||||
|
const double dx=static_cast<double>(b[0]-a[0])/qPerMetre;
|
||||||
|
const double dy=static_cast<double>(b[1]-a[1])/qPerMetre;
|
||||||
|
const double dz=static_cast<double>(b[2]-a[2])/qPerMetre;
|
||||||
|
routeDistanceMetres[pointIndex]=routeDistanceMetres[pointIndex-1]
|
||||||
|
+std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||||
|
}
|
||||||
|
const double totalRouteMetres=routeDistanceMetres[routeEnd];
|
||||||
|
double minimumRadiusMetres=std::numeric_limits<double>::infinity();
|
||||||
|
double minimumBodyRadiusMetres=minimumRadiusMetres;
|
||||||
|
for(std::size_t pointIndex=0;pointIndex<=routeEnd;++pointIndex)
|
||||||
|
{
|
||||||
|
const double radius=static_cast<double>(
|
||||||
|
passage.controlPoints[pointIndex].radiusSubvoxels)/qPerMetre;
|
||||||
|
minimumRadiusMetres=std::min(minimumRadiusMetres,radius);
|
||||||
|
if(routeDistanceMetres[pointIndex]>=10.0
|
||||||
|
&&totalRouteMetres-routeDistanceMetres[pointIndex]>=10.0)
|
||||||
|
minimumBodyRadiusMetres=std::min(minimumBodyRadiusMetres,radius);
|
||||||
|
}
|
||||||
|
if(!std::isfinite(minimumBodyRadiusMetres))
|
||||||
|
minimumBodyRadiusMetres=minimumRadiusMetres;
|
||||||
|
const double bodyFloorDepthMetres=std::max(0.5,
|
||||||
|
minimumBodyRadiusMetres-0.23);
|
||||||
|
const double upperMouthDepthMetres=std::max(0.0,
|
||||||
|
static_cast<double>(passage.controlPoints.front().positionSubvoxels[2]
|
||||||
|
-passage.upperMouthSubvoxels[2])/qPerMetre+0.90);
|
||||||
|
const double lowerMouthDepthMetres=std::max(0.0,
|
||||||
|
static_cast<double>(passage.controlPoints[routeEnd].positionSubvoxels[2]
|
||||||
|
-passage.lowerMouthSubvoxels[2])/qPerMetre+0.90);
|
||||||
|
const auto routeFloorDepth=[&](double alongMetres,double radiusMetres)
|
||||||
|
{
|
||||||
|
const double localRadiusDepth=std::max(0.0,radiusMetres-0.23);
|
||||||
|
const double mouthDistance=std::max(0.0,std::min(alongMetres,
|
||||||
|
totalRouteMetres-alongMetres));
|
||||||
|
const double linear=std::clamp(mouthDistance/5.0,0.0,1.0);
|
||||||
|
const double ramp=linear*linear*(3.0-2.0*linear);
|
||||||
|
const double target=alongMetres<=totalRouteMetres*0.5
|
||||||
|
?upperMouthDepthMetres:lowerMouthDepthMetres;
|
||||||
|
return std::lerp(std::clamp(target,0.0,localRadiusDepth),
|
||||||
|
localRadiusDepth,ramp);
|
||||||
|
};
|
||||||
|
std::vector<double> effectiveCenterZVoxels(
|
||||||
|
passage.controlPoints.size(),0.0);
|
||||||
|
for(std::size_t pointIndex=0;pointIndex<passage.controlPoints.size();
|
||||||
|
++pointIndex)
|
||||||
|
{
|
||||||
|
const std::size_t radiusIndex=std::min(pointIndex,routeEnd);
|
||||||
|
const double radius=static_cast<double>(
|
||||||
|
passage.controlPoints[radiusIndex].radiusSubvoxels)/qPerMetre;
|
||||||
|
const double localTubeDepth=std::min(bodyFloorDepthMetres,
|
||||||
|
std::max(0.0,radius-0.23));
|
||||||
|
const double routeDepth=pointIndex<=routeEnd
|
||||||
|
?routeFloorDepth(routeDistanceMetres[pointIndex],radius)
|
||||||
|
:std::max(0.0,radius-0.23);
|
||||||
|
effectiveCenterZVoxels[pointIndex]=
|
||||||
|
static_cast<double>(passage.controlPoints[pointIndex]
|
||||||
|
.positionSubvoxels[2])/Q
|
||||||
|
+(-routeDepth+localTubeDepth)*voxelsPerMetre;
|
||||||
|
}
|
||||||
for(std::size_t segment=1;segment<passage.controlPoints.size();++segment)
|
for(std::size_t segment=1;segment<passage.controlPoints.size();++segment)
|
||||||
{
|
{
|
||||||
const auto& a=passage.controlPoints[segment-1];
|
const auto& a=passage.controlPoints[segment-1];
|
||||||
const auto& b=passage.controlPoints[segment];
|
const auto& b=passage.controlPoints[segment];
|
||||||
const double radius=static_cast<double>(std::max(
|
const double extent=std::max(static_cast<double>(a.radiusSubvoxels),
|
||||||
a.radiusSubvoxels,b.radiusSubvoxels))/Q;
|
static_cast<double>(b.radiusSubvoxels))/Q+tubeExtentMetres;
|
||||||
const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels);
|
double low[3]{},high[3]{};
|
||||||
const double low[3]={
|
for(int axis=0;axis<3;++axis)
|
||||||
std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent,
|
{
|
||||||
std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent,
|
const double aPosition=axis==2?effectiveCenterZVoxels[segment-1]
|
||||||
std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius
|
:static_cast<double>(a.positionSubvoxels[axis])/Q;
|
||||||
-bridgeFootDepthVoxels};
|
const double bPosition=axis==2?effectiveCenterZVoxels[segment]
|
||||||
const double high[3]={
|
:static_cast<double>(b.positionSubvoxels[axis])/Q;
|
||||||
std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent,
|
low[axis]=std::min(aPosition,bPosition)-extent;
|
||||||
std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent,
|
high[axis]=std::max(aPosition,bPosition)+extent;
|
||||||
std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius};
|
}
|
||||||
bool overlap=true;
|
bool overlap=true;
|
||||||
for(int axis=0;axis<3;++axis)
|
for(int axis=0;axis<3;++axis)
|
||||||
overlap&=high[axis]>=tileMinimum[axis]&&low[axis]<=tileMaximum[axis];
|
overlap&=high[axis]>=tileMinimum[axis]&&low[axis]<=tileMaximum[axis];
|
||||||
if(overlap)return true;
|
if(overlap)return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// L27 can add a long support column around an unsupported passage span. Its
|
|
||||||
// radius can exceed the route and bridge bounds above by the column wall and
|
|
||||||
// flare allowance. The precompile shape-free proof runs before that crossing
|
|
||||||
// plan exists, so conservatively bound every possible column from the resolved
|
|
||||||
// route itself. The center is either the passage anchor or an average of route
|
|
||||||
// samples; in the latter case it lies inside the route's XY bounds.
|
|
||||||
if(passage.controlPoints.size()>=2)
|
|
||||||
{
|
|
||||||
double routeMinimum[3]={std::numeric_limits<double>::infinity(),
|
|
||||||
std::numeric_limits<double>::infinity(),
|
|
||||||
std::numeric_limits<double>::infinity()};
|
|
||||||
double routeMaximum[3]={-std::numeric_limits<double>::infinity(),
|
|
||||||
-std::numeric_limits<double>::infinity(),
|
|
||||||
-std::numeric_limits<double>::infinity()};
|
|
||||||
double maximumBoreRadius=0.0;
|
|
||||||
for(const auto& point:passage.controlPoints)
|
|
||||||
{
|
|
||||||
for(int axis=0;axis<3;++axis)
|
|
||||||
{
|
|
||||||
const double value=static_cast<double>(point.positionSubvoxels[axis])/Q;
|
|
||||||
routeMinimum[axis]=std::min(routeMinimum[axis],value);
|
|
||||||
routeMaximum[axis]=std::max(routeMaximum[axis],value);
|
|
||||||
}
|
|
||||||
maximumBoreRadius=std::max(maximumBoreRadius,
|
|
||||||
static_cast<double>(point.radiusSubvoxels)/Q);
|
|
||||||
}
|
|
||||||
|
|
||||||
double centerMinimumX=routeMinimum[0],centerMaximumX=routeMaximum[0];
|
|
||||||
double centerMinimumY=routeMinimum[1],centerMaximumY=routeMaximum[1];
|
|
||||||
double maximumRouteOffset=std::hypot(
|
|
||||||
routeMaximum[0]-routeMinimum[0],routeMaximum[1]-routeMinimum[1]);
|
|
||||||
const auto upper=std::find_if(layout.strates.begin(),layout.strates.end(),
|
|
||||||
[&passage](const auto& stratum)
|
|
||||||
{return stratum.strateIndex==passage.upperStrateIndex;});
|
|
||||||
if(upper!=layout.strates.end()
|
|
||||||
&&upper->passageCapabilities.usePassageAnchorForColumns)
|
|
||||||
{
|
|
||||||
const auto anchor=std::find_if(upper->anchors.begin(),upper->anchors.end(),
|
|
||||||
[](const auto& value)
|
|
||||||
{return value.kind=="passage_anchor"&&value.values.size()>=2;});
|
|
||||||
if(anchor!=upper->anchors.end())
|
|
||||||
{
|
|
||||||
const double centerX=static_cast<double>(anchor->values[0])/Q;
|
|
||||||
const double centerY=static_cast<double>(anchor->values[1])/Q;
|
|
||||||
centerMinimumX=centerMaximumX=centerX;
|
|
||||||
centerMinimumY=centerMaximumY=centerY;
|
|
||||||
maximumRouteOffset=0.0;
|
|
||||||
for(const auto& point:passage.controlPoints)
|
|
||||||
maximumRouteOffset=std::max(maximumRouteOffset,std::hypot(
|
|
||||||
static_cast<double>(point.positionSubvoxels[0])/Q-centerX,
|
|
||||||
static_cast<double>(point.positionSubvoxels[1])/Q-centerY));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The generated maximum column radius is the route offset, bore radius,
|
|
||||||
// and 13.25 m of wall/irregularity/flare allowance.
|
|
||||||
const double columnRadius=maximumRouteOffset+maximumBoreRadius
|
|
||||||
+13.25*voxelsPerMetre;
|
|
||||||
const double columnMinimum[3]={centerMinimumX-columnRadius,
|
|
||||||
centerMinimumY-columnRadius,routeMinimum[2]-6.0*voxelsPerMetre};
|
|
||||||
const double columnMaximum[3]={centerMaximumX+columnRadius,
|
|
||||||
centerMaximumY+columnRadius,routeMaximum[2]+5.0*voxelsPerMetre};
|
|
||||||
bool overlap=true;
|
|
||||||
for(int axis=0;axis<3;++axis)
|
|
||||||
overlap=overlap&&columnMaximum[axis]>=tileMinimum[axis]
|
|
||||||
&&columnMinimum[axis]<=tileMaximum[axis];
|
|
||||||
if(overlap)return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1866,44 +1867,6 @@ namespace VoxelForgeCore
|
|||||||
static_cast<double>(weighted)/65535.0*sampleVolume});
|
static_cast<double>(weighted)/65535.0*sampleVolume});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
std::vector<PassageVoidCrossingAuditV1> World::passageVoidCrossings() const
|
|
||||||
{
|
|
||||||
std::vector<PassageVoidCrossingAuditV1> result;
|
|
||||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)return result;
|
|
||||||
const auto& crossings=impl_->shape->GetPassageVoidCrossings();
|
|
||||||
result.reserve(crossings.size());
|
|
||||||
for(const auto& crossing:crossings)
|
|
||||||
{
|
|
||||||
PassageVoidCrossingAuditV1 value;
|
|
||||||
value.passageIndex=crossing.passageIndex;value.firstSegment=crossing.firstSegment;
|
|
||||||
value.lastSegment=crossing.lastSegment;value.unsupportedSamples=crossing.unsupportedSamples;
|
|
||||||
value.unsupportedLengthMetres=crossing.unsupportedLengthMetres;
|
|
||||||
value.startPointSubvoxels=crossing.startPoint;value.endPointSubvoxels=crossing.endPoint;
|
|
||||||
value.needsColumn=crossing.needsColumn;
|
|
||||||
value.connectedAtStart=crossing.connectedAtStart;
|
|
||||||
value.connectedAtEnd=crossing.connectedAtEnd;
|
|
||||||
value.columnAxisSubvoxels={crossing.centerXSubvoxels,crossing.centerYSubvoxels};
|
|
||||||
value.columnTopZSubvoxels=crossing.topZSubvoxels;
|
|
||||||
value.columnBottomZSubvoxels=crossing.bottomZSubvoxels;
|
|
||||||
value.columnMinimumRadiusSubvoxels=crossing.minimumRadiusSubvoxels;
|
|
||||||
value.columnNominalRadiusSubvoxels=crossing.nominalRadiusSubvoxels;
|
|
||||||
value.columnMaximumRadiusSubvoxels=crossing.maximumRadiusSubvoxels;
|
|
||||||
value.maximumRouteOffsetMetres=crossing.maximumRouteOffsetMetres;
|
|
||||||
value.maximumBoreRadiusMetres=crossing.maximumBoreRadiusMetres;
|
|
||||||
result.push_back(value);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
bool World::isPassageColumnSample(std::size_t passageIndex,std::size_t segmentIndex,
|
|
||||||
Vec3d position) const
|
|
||||||
{return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
|
||||||
&&impl_->shape->IsResolvedPassageColumnSample(passageIndex,segmentIndex,
|
|
||||||
{position.x,position.y,position.z});}
|
|
||||||
bool World::isPassageShortSpanSample(std::size_t passageIndex,std::size_t segmentIndex,
|
|
||||||
Vec3d position) const
|
|
||||||
{return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
|
||||||
&&impl_->shape->IsResolvedPassageShortSpanSample(passageIndex,segmentIndex,
|
|
||||||
{position.x,position.y,position.z});}
|
|
||||||
std::string World::densityDispatchPathAt(Vec3d position) const
|
std::string World::densityDispatchPathAt(Vec3d position) const
|
||||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
||||||
?impl_->shape->DensityDispatchPathAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z))
|
?impl_->shape->DensityDispatchPathAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z))
|
||||||
@@ -2672,30 +2635,6 @@ namespace VoxelForgeCore
|
|||||||
RelevantMinimum=std::min(RelevantMinimum,FeatureMinimum);
|
RelevantMinimum=std::min(RelevantMinimum,FeatureMinimum);
|
||||||
RelevantMaximum=std::max(RelevantMaximum,FeatureMaximum);
|
RelevantMaximum=std::max(RelevantMaximum,FeatureMaximum);
|
||||||
}
|
}
|
||||||
// A resolved passage void column can add solid support above or
|
|
||||||
// below the native heightfield's local Z range. Its finite radial
|
|
||||||
// and vertical bounds must participate before the mesh band is clipped.
|
|
||||||
for(const auto& Crossing:tileShape.GetPassageVoidCrossings())
|
|
||||||
{
|
|
||||||
if(!Crossing.needsColumn||Crossing.maximumRadiusSubvoxels<=0
|
|
||||||
||Crossing.topZSubvoxels<=Crossing.bottomZSubvoxels)continue;
|
|
||||||
const double Radius=static_cast<double>(Crossing.maximumRadiusSubvoxels)
|
|
||||||
/static_cast<double>(SubvoxelScale);
|
|
||||||
const double CenterX=static_cast<double>(Crossing.centerXSubvoxels)
|
|
||||||
/static_cast<double>(SubvoxelScale);
|
|
||||||
const double CenterY=static_cast<double>(Crossing.centerYSubvoxels)
|
|
||||||
/static_cast<double>(SubvoxelScale);
|
|
||||||
if(CenterX+Radius<TileMinimum[0]/static_cast<double>(SubvoxelScale)
|
|
||||||
||CenterX-Radius>TileMaximum[0]/static_cast<double>(SubvoxelScale)
|
|
||||||
||CenterY+Radius<TileMinimum[1]/static_cast<double>(SubvoxelScale)
|
|
||||||
||CenterY-Radius>TileMaximum[1]/static_cast<double>(SubvoxelScale))continue;
|
|
||||||
RelevantMinimum=std::min(RelevantMinimum,
|
|
||||||
static_cast<double>(Crossing.bottomZSubvoxels)
|
|
||||||
/static_cast<double>(SubvoxelScale));
|
|
||||||
RelevantMaximum=std::max(RelevantMaximum,
|
|
||||||
static_cast<double>(Crossing.topZSubvoxels)
|
|
||||||
/static_cast<double>(SubvoxelScale));
|
|
||||||
}
|
|
||||||
// The native heightfield band only covers the owner terrain and its
|
// The native heightfield band only covers the owner terrain and its
|
||||||
// manifest features. Resolved SDF stamps can carve below that band or
|
// manifest features. Resolved SDF stamps can carve below that band or
|
||||||
// add a floor there, so include every overlapping stamp's bounded Z
|
// add a floor there, so include every overlapping stamp's bounded Z
|
||||||
@@ -2804,7 +2743,6 @@ namespace VoxelForgeCore
|
|||||||
FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope(
|
FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope(
|
||||||
tileShape,queryMinimum,queryMaximum,
|
tileShape,queryMinimum,queryMaximum,
|
||||||
options.profilePassageComposer?&out.diagnostics:nullptr,key.lod,
|
options.profilePassageComposer?&out.diagnostics:nullptr,key.lod,
|
||||||
options.diagnosticPassageSourceFloorVoxels,
|
|
||||||
options.diagnosticPassageCarveReachMetres);
|
options.diagnosticPassageCarveReachMetres);
|
||||||
passageFeatureScope.prepareSweptTubeBase(origin,MeshStep,MeshCells,
|
passageFeatureScope.prepareSweptTubeBase(origin,MeshStep,MeshCells,
|
||||||
static_cast<std::uint32_t>(world.Seed));
|
static_cast<std::uint32_t>(world.Seed));
|
||||||
@@ -2904,10 +2842,6 @@ namespace VoxelForgeCore
|
|||||||
{
|
{
|
||||||
out.diagnostics.passageTileRouteSegmentCandidates=
|
out.diagnostics.passageTileRouteSegmentCandidates=
|
||||||
passageFeatureScope.routeSegmentCandidateCount();
|
passageFeatureScope.routeSegmentCandidateCount();
|
||||||
for(std::size_t passageIndex=0;
|
|
||||||
passageIndex<impl_->resolvedLayout->passages.size();++passageIndex)
|
|
||||||
out.diagnostics.passageTileMouthSegmentCandidates+=
|
|
||||||
passageFeatureScope.mouthSupportSegments(passageIndex).size();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FGridFillWorkerContext GridFillWorkerContext;
|
FGridFillWorkerContext GridFillWorkerContext;
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Measure the fixed-depth floor and derived centreline against the authored grade."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
LAYOUT = ROOT / "Saved/Linux/l109/audit-layout-final-stamp.json"
|
||||||
|
OUT = ROOT / "Saved/Linux/l109/floor-ramp-grade-final.json"
|
||||||
|
MOUTH_RAMP_METRES = 5.0
|
||||||
|
|
||||||
|
|
||||||
|
def route_m(layout: dict, passage: dict):
|
||||||
|
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||||
|
controls = [(tuple(float(v) * scale for v in row["position_q"]),
|
||||||
|
float(row["radius_q"]) * scale) for row in passage["control_points"]]
|
||||||
|
lower_q = passage["lower_mouth_q"]
|
||||||
|
player_center_above_floor_q = round(
|
||||||
|
1.13 * 256.0 * 1_000_000.0 / layout["core_voxel_size_um"])
|
||||||
|
lower_index = next((index for index, row in enumerate(passage["control_points"])
|
||||||
|
if row["position_q"][0] == lower_q[0]
|
||||||
|
and row["position_q"][1] == lower_q[1]
|
||||||
|
and row["position_q"][2] - row["radius_q"]
|
||||||
|
+ player_center_above_floor_q == lower_q[2]), None)
|
||||||
|
if lower_index is None:
|
||||||
|
raise RuntimeError(f"{passage['stable_id']}: lower mouth control is unbound")
|
||||||
|
return controls[:lower_index + 1]
|
||||||
|
|
||||||
|
|
||||||
|
def maximum_grade(profile):
|
||||||
|
grades = []
|
||||||
|
for (p0, z0), (p1, z1) in zip(profile, profile[1:]):
|
||||||
|
horizontal = math.hypot(p1[0] - p0[0], p1[1] - p0[1])
|
||||||
|
if horizontal > 1.0e-8:
|
||||||
|
grades.append(math.degrees(math.atan2(abs(z1 - z0), horizontal)))
|
||||||
|
return max(grades, default=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
layout = json.loads(LAYOUT.read_text(encoding="utf-8"))
|
||||||
|
rows = []
|
||||||
|
for passage in layout["passages"]:
|
||||||
|
route = route_m(layout, passage)
|
||||||
|
segment_lengths = [math.dist(a[0], b[0]) for a, b in zip(route, route[1:])]
|
||||||
|
prefix = [0.0]
|
||||||
|
for length in segment_lengths:
|
||||||
|
prefix.append(prefix[-1] + length)
|
||||||
|
interior_radii = [radius for distance, (_, radius) in zip(prefix, route)
|
||||||
|
if distance >= 10.0 and prefix[-1] - distance >= 10.0]
|
||||||
|
minimum_body_radius = min(interior_radii or [radius for _, radius in route])
|
||||||
|
body_depth = max(0.5, minimum_body_radius - 0.23)
|
||||||
|
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||||
|
upper_target = max(0.0, route[0][0][2]
|
||||||
|
- passage["upper_mouth_q"][2] * scale + 1.13)
|
||||||
|
lower_target = max(0.0, route[-1][0][2]
|
||||||
|
- passage["lower_mouth_q"][2] * scale + 1.13)
|
||||||
|
floor_profile = []
|
||||||
|
raw_fixed_depth_profile = []
|
||||||
|
maximum_centerline_shift = 0.0
|
||||||
|
for segment_index, (a, b, length) in enumerate(
|
||||||
|
zip(route, route[1:], segment_lengths)):
|
||||||
|
steps = max(1, math.ceil(length / 0.05))
|
||||||
|
for step in range(steps + 1):
|
||||||
|
if floor_profile and step == 0:
|
||||||
|
continue
|
||||||
|
t = step / steps
|
||||||
|
position = tuple(a[0][axis] + (b[0][axis] - a[0][axis]) * t
|
||||||
|
for axis in range(3))
|
||||||
|
radius = a[1] + (b[1] - a[1]) * t
|
||||||
|
along = prefix[segment_index] + length * t
|
||||||
|
mouth_distance = max(0.0, min(along, prefix[-1] - along))
|
||||||
|
u = max(0.0, min(1.0, mouth_distance / MOUTH_RAMP_METRES))
|
||||||
|
ramp = u * u * (3.0 - 2.0 * u)
|
||||||
|
local_radius_depth = max(0.0, radius - 0.23)
|
||||||
|
mouth_target = upper_target if along <= prefix[-1] * 0.5 else lower_target
|
||||||
|
mouth_depth = max(0.0, min(radius, mouth_target))
|
||||||
|
route_depth = mouth_depth + (local_radius_depth - mouth_depth) * ramp
|
||||||
|
tube_depth = min(body_depth, local_radius_depth)
|
||||||
|
effective_center = position[2] - route_depth + tube_depth
|
||||||
|
floor_profile.append((position, effective_center - tube_depth))
|
||||||
|
raw_fixed_depth_profile.append((position, position[2] - tube_depth))
|
||||||
|
maximum_centerline_shift = max(maximum_centerline_shift,
|
||||||
|
abs(effective_center - position[2]))
|
||||||
|
rows.append({
|
||||||
|
"passage": passage["stable_id"],
|
||||||
|
"path_m": prefix[-1],
|
||||||
|
"body_floor_depth_m": body_depth,
|
||||||
|
"mouth_blend_m": MOUTH_RAMP_METRES,
|
||||||
|
"preserved_route_floor_max_grade_deg": maximum_grade(floor_profile),
|
||||||
|
"unshifted_fixed_depth_floor_max_grade_deg": maximum_grade(
|
||||||
|
raw_fixed_depth_profile),
|
||||||
|
"maximum_derived_centerline_shift_m": maximum_centerline_shift,
|
||||||
|
})
|
||||||
|
OUT.write_text(json.dumps({
|
||||||
|
"method": "0.05 m route samples; fixed body depth from the minimum interior radius; "
|
||||||
|
"cubic smoothstep mouth blend",
|
||||||
|
"passages": rows,
|
||||||
|
}, indent=2) + "\n", encoding="utf-8")
|
||||||
|
for row in rows:
|
||||||
|
print(f"[L109FloorRamp] {row['passage']} "
|
||||||
|
f"depth_m={row['body_floor_depth_m']:.3f} "
|
||||||
|
f"preserved_grade_deg={row['preserved_route_floor_max_grade_deg']:.3f} "
|
||||||
|
f"unshifted_fixed_depth_grade_deg={row['unshifted_fixed_depth_floor_max_grade_deg']:.3f} "
|
||||||
|
f"max_center_shift_m={row['maximum_derived_centerline_shift_m']:.3f}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Linux 109 — tunnel floor and shell
|
||||||
|
|
||||||
|
Baseline: `assembler/world` HEAD `95cf8de10723f2dffe7b3dfc116ecf350c7114ae`. Candidate source stamp: `9457b9bed7d57a4a387828961f9dd53632ec602eb5ea8f247b087be6ca5cc312` (evaluator v217).
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
The per-tile swept tube now uses a horizontal flat floor, a 0.50 m smooth floor-to-wall corner, and a 0.75 m shell. Body floor depth is fixed per passage at the minimum interior radius minus 0.23 m, capped at the local radius. Five-metre cubic blends ramp both mouths into their ground; the lower mouth follows the matching authored native-cave floor where one exists. The route centreline shifts through radius tapers to keep that floor walkable. A small wall relief remains for naturalness.
|
||||||
|
|
||||||
|
The passage interiors are unioned first and always win over every passage shell, including at coiled crossings. The shell writes solid only where the host field is air; rock remains unchanged. There is no rock classification. Open-air shell and interior samples pass in the owner audit.
|
||||||
|
|
||||||
|
## Removed and retained
|
||||||
|
|
||||||
|
Removed route support restoration, deferred support after later bores, support columns, mouth-support restoration, and their floor-specific capsule checks and profiling records. Replaced the old open-air/column audit gates with the shell audit and removed the no-long-bridge and rock-column gates, which measured the deleted support mechanism. Walkability, capsule, headroom, grade, mouth, naturalness, and ripple gates remain. Floor-ripple probes fully masked by another passage's interior air are skipped only when the overlap is explicit; capsule walkability still has to pass.
|
||||||
|
|
||||||
|
Kept the native Lattice field's explicitly authored below-floor substrate because its spine needs it for walkability; this is part of that native field, not route support restoration. The legacy landmark demo retains its compatibility floor-cap path, which is separate from the resolved route sweep. Native Caves and Lattice fast checks pass.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
Five interleaved L80 pairs per cell, after one warmup for each binary; seconds are median `LOD0–2 near-ready / whole-view-ready`. Baseline binary is from `assembler/world` HEAD. Tile counts matched within each cell.
|
||||||
|
|
||||||
|
| Strate | W8 near | W8 whole | W12 near | W12 whole |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| SW | 0.680 → 0.737 (+8.3%) | 1.686 → 1.674 (−0.7%) | 0.700 → 0.761 (+8.8%) | 1.643 → 1.644 (+0.1%) |
|
||||||
|
| FI | 1.455 → 0.911 (−37.4%) | 2.419 → 1.772 (−26.8%) | 1.394 → 0.863 (−38.0%) | 2.288 → 1.664 (−27.3%) |
|
||||||
|
| TN | 1.423 → 0.956 (−32.8%) | 2.310 → 1.778 (−23.0%) | 1.294 → 0.895 (−30.9%) | 2.132 → 1.661 (−22.1%) |
|
||||||
|
| VS | 1.008 → 0.789 (−21.8%) | 1.917 → 1.639 (−14.5%) | 0.854 → 0.738 (−13.5%) | 1.716 → 1.541 (−10.2%) |
|
||||||
|
|
||||||
|
There is no broad regression. SW near-ready is about 0.06 s slower; its whole-view time is flat. **Measured:** SW LOD0–2 skirt triangles rise from 133,894 to 196,602, and summed skirt work rises from 455 to 743 ms at W8 and 650 to 1,044 ms at W12. **Inference:** the added open-air shell surface and skirts explain the small SW near-ready cost. FI, TN, and VS improve at both readiness marks.
|
||||||
|
|
||||||
|
## L77 phase census
|
||||||
|
|
||||||
|
Two samples per case. Entries are `share / estimated worker-s`; estimates use weighted L77 samples times `WorldAssemblerLODWorker` CPU, so they are not direct phase timers.
|
||||||
|
|
||||||
|
| Case | Base passage | Base support | Candidate passage | Candidate support |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| FI near | 44.62% / 4.108 | 14.84% / 1.366 | 22.90% / 1.133 | 0% / 0 |
|
||||||
|
| FI whole | 29.31% / 4.874 | 8.08% / 1.344 | 10.59% / 1.206 | 0% / 0 |
|
||||||
|
| TN near | 38.57% / 3.318 | 14.74% / 1.267 | 19.43% / 1.019 | 0% / 0 |
|
||||||
|
| TN whole | 24.48% / 3.824 | 7.59% / 1.186 | 9.25% / 1.074 | 0% / 0 |
|
||||||
|
|
||||||
|
The sampled support share disappears. Baseline estimates were 1.19–1.37 worker-s across these cases, below the 1.6–1.9 s expectation.
|
||||||
|
|
||||||
|
## Audits and determinism
|
||||||
|
|
||||||
|
The full-view all-LOD audit passes: 21,425/21,425 canonical passage centreline samples are open. All three interfaces pass capsule and support checks with zero blocked or unsupported samples, zero grade failures, and both mouths bound (2/2). Capsule samples: 266,175 (0–1), 15,120 (1–2), 64,764 (2–3). The shell audit sampled 89,365 floor points, 8,423 wall points, and 10,527 interior points: zero floor, wall, or interior violations; 22,755 open-air shell samples. Naturalness, mouth profile, and ripple summaries pass 3/3, with negative controls detected. Derived floor grade peaks at 14.49°; fast native Caves headroom is 0.30 m and native Lattice headroom is 2.20 m.
|
||||||
|
|
||||||
|
Fast tier: PASS, 259 PASS checks plus 5 diagnostics across 80 runs. The Clang/GCC hash matrix matches at W1/W8/W12 for the owner four-strata layout and both two-strata canonical recipes. The v217 canonical pins were written for Surface World style 1 and Floating Islands style 2.
|
||||||
|
|
||||||
|
Because Core sources are Unreal-compiled, the Linux VFHostEditor build passed with `-NoUBA -MaxParallelActions=4` (27 actions, 67.4 s). Three FI and three SW `after` arrival traces also passed, each settling its full requested LOD0–2 set. Unreal processes had no `prlimit`.
|
||||||
|
|
||||||
|
## Change metrics
|
||||||
|
|
||||||
|
LOD0–2 camera mesh windows; “moved” means a shared global lattice crossing displaced by more than 0.01 cm. Coverage is the paired edge-classifier coverage.
|
||||||
|
|
||||||
|
| Strate | Added | Removed | Moved | Coverage |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| SW | 78,201 | 14,530 | 2,758 | 99.75% |
|
||||||
|
| FI | 37,810 | 42,398 | 78,215 | 98.31% |
|
||||||
|
| TN | 44,694 | 48,925 | 48,816 | 98.29% |
|
||||||
|
| VS | 7,055 | 7,096 | 8,790 | 97.38% |
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
84 PNGs: before and after, each with both mouths and 25/50/75% stations for all three passages. Each of the nine open-air sections has three views in each set. The [capture index](captures/INDEX.md) lists every image and flags every **OPEN AIR** view. All 84 source meshes and rendered views contain geometry; minimum visible triangles are 55,420 before and 55,484 after. Six before and seven after images met the pixel floor but were flagged low contrast for review.
|
||||||
|
|
||||||
|
Open-air sections (route distance in metres): `0–1`: 7.226–10.217, 13.456–17.443, 23.174–24.918, 1036.913–1489.321; `1–2`: 101.820–104.499; `2–3`: 7.677–34.747, 368.909–376.660, 393.828–396.510, 444.932–447.654.
|
||||||
|
|
||||||
|
## Failed runs
|
||||||
|
|
||||||
|
Early fast runs found a 0.79 m lower-mouth mismatch at native Caves and loss of the native Lattice route substrate; the mouth now matches the authored cave edge floor, and the explicitly authored Lattice floor field is retained. A sentinel check also initially treated the “no native floor” value as support; this was fixed. The first Unreal build stopped before compilation because UnrealBuildTool could not rotate its log under read-only `~/.config`; redirecting `XDG_CONFIG_HOME` to `/tmp` fixed it. Final audits, timings, hash runs, crossings, captures, build, and six traces passed. No run exceeded 30 minutes.
|
||||||
@@ -0,0 +1,511 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Capture before/after passage mouths, route stations, and exposed shell sections."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from array import array
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
RESEARCH = Path(__file__).resolve().parent
|
||||||
|
CAPTURES = RESEARCH / "captures"
|
||||||
|
SAVED = ROOT / "Saved/Linux/l109/captures"
|
||||||
|
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
BINARIES = {
|
||||||
|
"before": ROOT / "Saved/Linux/l109/bin-baseline/VoxelForgeCoreHeadless-l109-baseline",
|
||||||
|
"after": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||||
|
}
|
||||||
|
LAYOUTS = {
|
||||||
|
"before": ROOT / "Saved/Linux/l109/audit-layout-baseline.json",
|
||||||
|
"after": ROOT / "Saved/Linux/l109/audit-layout-final-stamp.json",
|
||||||
|
}
|
||||||
|
AUDIT_LOG = ROOT / "Saved/Linux/l109/audit-owner4-final-stamp.log"
|
||||||
|
RENDERERS = {
|
||||||
|
"before": ROOT / "Saved/Linux/l109/bin-baseline/VoxelForgeOffscreenCapture-l109-baseline",
|
||||||
|
"after": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeOffscreenCapture-l109-clang",
|
||||||
|
}
|
||||||
|
SECTION = re.compile(
|
||||||
|
r"^\[PassageOpenAirSection\] id=(\S+) section=(\d+) start_m=([\d.]+) "
|
||||||
|
r"end_m=([\d.]+) samples=(\d+) midpoint_xyz_m=\(([^)]*)\) "
|
||||||
|
r"midpoint_radius_m=([\d.]+)", re.M)
|
||||||
|
DRAWN = re.compile(r"drawn_triangles=(\d+)")
|
||||||
|
QUALITY_METRICS = re.compile(r"stddev_luma=([\d.]+).*coverage_luma_gt12=([\d.]+)")
|
||||||
|
|
||||||
|
|
||||||
|
def vector(text: str) -> tuple[float, float, float]:
|
||||||
|
return tuple(float(value) for value in text.split(","))
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
return (0.0, 1.0, 0.0)
|
||||||
|
return tuple(value / length for value in a)
|
||||||
|
|
||||||
|
|
||||||
|
def angles(camera, target):
|
||||||
|
delta = sub(target, camera)
|
||||||
|
horizontal = math.hypot(delta[0], delta[1])
|
||||||
|
return (math.degrees(math.atan2(delta[0], delta[1])),
|
||||||
|
math.degrees(math.atan2(delta[2], horizontal)))
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(values) -> str:
|
||||||
|
return ",".join(f"{value:.6f}" for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def mesh_triangle_count(mesh: Path) -> int:
|
||||||
|
"""Read only the VFMSH headers and count indexed triangles."""
|
||||||
|
with mesh.open("rb") as stream:
|
||||||
|
magic = stream.read(8)
|
||||||
|
materials = magic == b"VFMSH002"
|
||||||
|
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||||
|
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
|
||||||
|
version, _voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
if version != (2 if materials else 1):
|
||||||
|
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
|
||||||
|
stream.seek(40, 1)
|
||||||
|
triangles = 0
|
||||||
|
for _ in range(tile_count):
|
||||||
|
stream.seek(16, 1)
|
||||||
|
surface_count, _skirts, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
triangles += index_count // 3
|
||||||
|
stream.seek(vertex_count * 24 + (10 * vertex_count if materials else 0)
|
||||||
|
+ index_count * 4 + mask_count, 1)
|
||||||
|
if stream.read(1):
|
||||||
|
raise RuntimeError(f"trailing data in mesh scene: {mesh}")
|
||||||
|
return triangles
|
||||||
|
|
||||||
|
|
||||||
|
def dot(a, b):
|
||||||
|
return sum(x * y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def nearest_surface_anchor(mesh: Path, point, margin_m=200.0) -> dict:
|
||||||
|
"""Return a nearby VFMSH surface vertex and its normal."""
|
||||||
|
best_distance_sq = float("inf")
|
||||||
|
best_position = best_normal = None
|
||||||
|
with mesh.open("rb") as stream:
|
||||||
|
magic = stream.read(8)
|
||||||
|
materials = magic == b"VFMSH002"
|
||||||
|
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||||
|
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
if version != (2 if materials else 1):
|
||||||
|
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
|
||||||
|
stream.seek(40, 1)
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _skirts, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
extent = (32 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
origin = (key[0] * extent, key[1] * extent, key[2] * extent)
|
||||||
|
if any(point[axis] < origin[axis] - margin_m or
|
||||||
|
point[axis] > origin[axis] + extent + margin_m for axis in range(3)):
|
||||||
|
stream.seek(vertex_count * 24 + (10 * vertex_count if materials else 0)
|
||||||
|
+ index_count * 4 + mask_count, 1)
|
||||||
|
continue
|
||||||
|
positions = array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
if materials:
|
||||||
|
stream.seek(vertex_count * 10, 1)
|
||||||
|
stream.seek(index_count * 4 + mask_count, 1)
|
||||||
|
if struct.pack("=I", 1) != struct.pack("<I", 1):
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
for index in range(min(surface_count, vertex_count)):
|
||||||
|
offset = index * 3
|
||||||
|
position = tuple(float(positions[offset + axis]) for axis in range(3))
|
||||||
|
distance_sq = dot(sub(position, point), sub(position, point))
|
||||||
|
if distance_sq >= best_distance_sq:
|
||||||
|
continue
|
||||||
|
raw_normal = tuple(float(normals[offset + axis]) for axis in range(3))
|
||||||
|
if dot(raw_normal, raw_normal) < 1.0e-10:
|
||||||
|
continue
|
||||||
|
best_distance_sq = distance_sq
|
||||||
|
best_position = position
|
||||||
|
best_normal = norm(raw_normal)
|
||||||
|
if best_position is None:
|
||||||
|
raise RuntimeError(f"no nearby surface vertices in mesh scene: {mesh}")
|
||||||
|
return {"position_m": best_position, "normal": best_normal,
|
||||||
|
"distance_m": math.sqrt(best_distance_sq)}
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(value: str) -> str:
|
||||||
|
return re.sub(r"[^A-Za-z0-9_-]+", "-", value).strip("-").replace("/", "-")
|
||||||
|
|
||||||
|
|
||||||
|
def route_for(layout: dict, passage: dict) -> list[tuple[tuple[float, float, float], float]]:
|
||||||
|
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||||
|
controls = [(tuple(value * scale for value in row["position_q"]),
|
||||||
|
row["radius_q"] * scale) for row in passage["control_points"]]
|
||||||
|
lower_q = passage["lower_mouth_q"]
|
||||||
|
player_center_above_floor_q = round(
|
||||||
|
1.13 * 256.0 * 1_000_000.0 / layout["core_voxel_size_um"])
|
||||||
|
lower_index = next((index for index, row in enumerate(passage["control_points"])
|
||||||
|
if row["position_q"][0] == lower_q[0]
|
||||||
|
and row["position_q"][1] == lower_q[1]
|
||||||
|
and row["position_q"][2] - row["radius_q"]
|
||||||
|
+ player_center_above_floor_q == lower_q[2]), None)
|
||||||
|
if lower_index is None:
|
||||||
|
raise RuntimeError(f"{passage['stable_id']}: lower mouth control is unbound")
|
||||||
|
result = controls[:lower_index + 1]
|
||||||
|
if len(result) < 2:
|
||||||
|
raise RuntimeError(f"{passage['stable_id']}: incomplete route controls")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def route_lengths(route):
|
||||||
|
lengths = [math.dist(a[0], b[0]) for a, b in zip(route, route[1:])]
|
||||||
|
prefix = [0.0]
|
||||||
|
for length in lengths:
|
||||||
|
prefix.append(prefix[-1] + length)
|
||||||
|
return lengths, prefix
|
||||||
|
|
||||||
|
|
||||||
|
def at_distance(route, lengths, prefix, distance):
|
||||||
|
distance = max(0.0, min(distance, prefix[-1]))
|
||||||
|
for index, length in enumerate(lengths):
|
||||||
|
if distance <= prefix[index + 1] or index + 1 == len(lengths):
|
||||||
|
t = 0.0 if length <= 1.0e-9 else (distance - prefix[index]) / length
|
||||||
|
a, b = route[index], route[index + 1]
|
||||||
|
return (tuple(a[0][axis] + (b[0][axis] - a[0][axis]) * t
|
||||||
|
for axis in range(3)),
|
||||||
|
a[1] + (b[1] - a[1]) * t)
|
||||||
|
return route[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def tangent_at(route, lengths, prefix, distance):
|
||||||
|
before = at_distance(route, lengths, prefix, max(0.0, distance - 0.25))[0]
|
||||||
|
after = at_distance(route, lengths, prefix, min(prefix[-1], distance + 0.25))[0]
|
||||||
|
return norm(sub(after, before))
|
||||||
|
|
||||||
|
|
||||||
|
def derived_route(route, passage, layout):
|
||||||
|
"""Apply L109's fixed-depth floor offset to raw layout controls."""
|
||||||
|
lengths, prefix = route_lengths(route)
|
||||||
|
total = prefix[-1]
|
||||||
|
body_radii = [radius for index, (_, radius) in enumerate(route)
|
||||||
|
if prefix[index] >= 10.0 and total - prefix[index] >= 10.0]
|
||||||
|
body_depth = max(0.5, min(body_radii or [radius for _, radius in route]) - 0.23)
|
||||||
|
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||||
|
upper_z = passage["upper_mouth_q"][2] * scale
|
||||||
|
lower_z = passage["lower_mouth_q"][2] * scale
|
||||||
|
upper_target = max(0.0, route[0][0][2] - upper_z + 1.13)
|
||||||
|
lower_target = max(0.0, route[-1][0][2] - lower_z + 1.13)
|
||||||
|
transformed = []
|
||||||
|
for index, (point, radius) in enumerate(route):
|
||||||
|
local_radius_depth = max(0.0, radius - 0.23)
|
||||||
|
mouth_distance = max(0.0, min(prefix[index], total - prefix[index]))
|
||||||
|
u = max(0.0, min(1.0, mouth_distance / 5.0))
|
||||||
|
ramp = u * u * (3.0 - 2.0 * u)
|
||||||
|
target = upper_target if prefix[index] <= total * 0.5 else lower_target
|
||||||
|
mouth_depth = max(0.0, min(radius, target))
|
||||||
|
route_depth = mouth_depth + (local_radius_depth - mouth_depth) * ramp
|
||||||
|
tube_depth = min(body_depth, local_radius_depth)
|
||||||
|
transformed.append(((point[0], point[1], point[2] - route_depth + tube_depth),
|
||||||
|
radius))
|
||||||
|
return transformed, body_depth
|
||||||
|
|
||||||
|
|
||||||
|
def make_views(layout: dict, audit_sections: list[dict], *, use_derived_shape: bool,
|
||||||
|
include_open_air: bool = True) -> list[dict]:
|
||||||
|
views = []
|
||||||
|
passages = sorted(layout["passages"], key=lambda item: item["stable_id"])
|
||||||
|
for passage in passages:
|
||||||
|
raw_route = route_for(layout, passage)
|
||||||
|
if use_derived_shape:
|
||||||
|
route, body_depth = derived_route(raw_route, passage, layout)
|
||||||
|
else:
|
||||||
|
route = raw_route
|
||||||
|
body_depth = max(point[1] for point in route)
|
||||||
|
def floor_depth(radius: float) -> float:
|
||||||
|
if not use_derived_shape:
|
||||||
|
return radius
|
||||||
|
return min(body_depth, max(0.0, radius - 0.23))
|
||||||
|
lengths, prefix = route_lengths(route)
|
||||||
|
total = prefix[-1]
|
||||||
|
passage_id = passage["stable_id"]
|
||||||
|
safe_id = sanitize(passage_id)
|
||||||
|
|
||||||
|
for end_name, distance, outward_sign in (
|
||||||
|
("upper-mouth", 0.0, -1.0), ("lower-mouth", total, 1.0)):
|
||||||
|
center, radius = at_distance(route, lengths, prefix, distance)
|
||||||
|
tangent = tangent_at(route, lengths, prefix, distance)
|
||||||
|
outward = mul(tangent, outward_sign)
|
||||||
|
floor_z = center[2] - floor_depth(radius)
|
||||||
|
eye = (center[0] + outward[0] * 4.5,
|
||||||
|
center[1] + outward[1] * 4.5,
|
||||||
|
floor_z + 1.55)
|
||||||
|
if end_name == "lower-mouth":
|
||||||
|
eye = add(eye, add(mul(outward, 5.0), (0.0, 0.0, 2.0)))
|
||||||
|
target = (center[0] - outward[0] * 2.0,
|
||||||
|
center[1] - outward[1] * 2.0,
|
||||||
|
floor_z + 1.45)
|
||||||
|
yaw, pitch = angles(eye, target)
|
||||||
|
views.append({"name": f"{safe_id}-{end_name}", "passage": passage_id,
|
||||||
|
"kind": "mouth", "station": end_name,
|
||||||
|
"camera": eye, "target": target, "yaw": yaw, "pitch": pitch,
|
||||||
|
"focus": center, "center_z": center[2]})
|
||||||
|
|
||||||
|
for fraction in (0.25, 0.50, 0.75):
|
||||||
|
distance = total * fraction
|
||||||
|
center, radius = at_distance(route, lengths, prefix, distance)
|
||||||
|
tangent = tangent_at(route, lengths, prefix, distance)
|
||||||
|
depth = floor_depth(radius)
|
||||||
|
floor_z = center[2] - depth
|
||||||
|
horizontal = norm((-tangent[1], tangent[0], 0.0))
|
||||||
|
eye = (center[0] + horizontal[0] * 0.75,
|
||||||
|
center[1] + horizontal[1] * 0.75, floor_z + 1.60)
|
||||||
|
target_center, target_radius = at_distance(route, lengths, prefix,
|
||||||
|
min(total, distance + 14.0))
|
||||||
|
target_depth = floor_depth(target_radius)
|
||||||
|
target = (target_center[0] + horizontal[0] * 2.35,
|
||||||
|
target_center[1] + horizontal[1] * 2.35,
|
||||||
|
target_center[2] - target_depth + 1.80)
|
||||||
|
yaw, pitch = angles(eye, target)
|
||||||
|
views.append({"name": f"{safe_id}-station-{int(fraction * 100)}",
|
||||||
|
"passage": passage_id, "kind": "station",
|
||||||
|
"station": f"{int(fraction * 100)}%", "camera": eye,
|
||||||
|
"target": target, "yaw": yaw, "pitch": pitch,
|
||||||
|
"focus": center, "center_z": center[2],
|
||||||
|
"route_distance_m": distance,
|
||||||
|
"route_length_m": total,
|
||||||
|
"requested_route_distance_m": distance})
|
||||||
|
|
||||||
|
if not include_open_air:
|
||||||
|
continue
|
||||||
|
for section in (item for item in audit_sections if item["passage"] == passage_id):
|
||||||
|
distance = 0.5 * (section["start_m"] + section["end_m"])
|
||||||
|
center, radius = (section["midpoint"], section["radius_m"])
|
||||||
|
tangent = tangent_at(route, lengths, prefix, distance)
|
||||||
|
side = norm((-tangent[1], tangent[0], 0.0))
|
||||||
|
depth = floor_depth(radius)
|
||||||
|
section_id = section["section"]
|
||||||
|
interior_eye = (center[0], center[1], center[2] - depth + 1.25)
|
||||||
|
interior_target = add(interior_eye, mul(tangent, 7.0))
|
||||||
|
side_eye = add(center, add(mul(side, radius + 5.0), (0.0, 0.0, 1.2)))
|
||||||
|
oblique_eye = add(center, add(mul(side, -(radius + 3.5)), (0.0, 0.0, 4.0)))
|
||||||
|
targets = (add(center, (0.0, 0.0, 0.1)),
|
||||||
|
add(center, mul(tangent, 2.0)), interior_target)
|
||||||
|
for view_index, (eye, target) in enumerate(zip(
|
||||||
|
(side_eye, oblique_eye, interior_eye), targets), start=1):
|
||||||
|
yaw, pitch = angles(eye, target)
|
||||||
|
views.append({"name": f"{safe_id}-openair-{section_id}-view-{view_index}",
|
||||||
|
"passage": passage_id, "kind": "open-air",
|
||||||
|
"station": f"section-{section_id}/view-{view_index}",
|
||||||
|
"camera": eye, "target": target, "yaw": yaw,
|
||||||
|
"pitch": pitch, "focus": center,
|
||||||
|
"center_z": center[2], "open_air_section": section_id,
|
||||||
|
"route_distance_m": distance,
|
||||||
|
"requested_route_distance_m": distance,
|
||||||
|
"route_length_m": total})
|
||||||
|
return views
|
||||||
|
|
||||||
|
|
||||||
|
def run_process(name: str, command: list[str], log: Path, *, allow_failure=False) -> str:
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1740s", *map(str, command)]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||||
|
if result.returncode and not allow_failure:
|
||||||
|
raise RuntimeError(f"{name}: exit={result.returncode} log={log}")
|
||||||
|
print(f"[L109CaptureProcess] name={name} elapsed_s={time.monotonic()-started:.2f} "
|
||||||
|
f"exit={result.returncode} log={log.relative_to(ROOT)}", flush=True)
|
||||||
|
return log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def capture(variant: str, view: dict, layout: dict) -> dict:
|
||||||
|
variant_saved = SAVED / variant
|
||||||
|
variant_saved.mkdir(parents=True, exist_ok=True)
|
||||||
|
variant_capture = CAPTURES / variant
|
||||||
|
variant_capture.mkdir(parents=True, exist_ok=True)
|
||||||
|
scene = variant_saved / f"{view['name']}.vfmesh"
|
||||||
|
image = variant_capture / f"{view['name']}.png"
|
||||||
|
candidates = [(view["camera"], view["target"], view["focus"],
|
||||||
|
view.get("route_distance_m"), 0.0, "requested")]
|
||||||
|
if variant == "before" and view["kind"] != "mouth":
|
||||||
|
passage = next(item for item in layout["passages"]
|
||||||
|
if item["stable_id"] == view["passage"])
|
||||||
|
route = route_for(layout, passage)
|
||||||
|
lengths, prefix = route_lengths(route)
|
||||||
|
total = prefix[-1]
|
||||||
|
eye_offset = sub(view["camera"], view["focus"])
|
||||||
|
target_offset = sub(view["target"], view["focus"])
|
||||||
|
requested = view["route_distance_m"]
|
||||||
|
for delta in (5, -5, 10, -10, 20, -20, 35, -35, 50, -50, 75, -75,
|
||||||
|
100, -100, 150, -150, 200, -200):
|
||||||
|
distance = max(0.0, min(total, requested + delta))
|
||||||
|
if abs(distance - requested) < 0.01:
|
||||||
|
continue
|
||||||
|
center, _radius = at_distance(route, lengths, prefix, distance)
|
||||||
|
candidates.append((add(center, eye_offset), add(center, target_offset), center,
|
||||||
|
distance, distance - requested, "route-offset"))
|
||||||
|
|
||||||
|
attempt_errors = []
|
||||||
|
for attempt_index, (probe_camera, probe_target, probe_focus, route_distance,
|
||||||
|
route_adjustment, attempt_kind) in enumerate(candidates):
|
||||||
|
suffix = "" if attempt_index == 0 else f"-attempt-{attempt_index:02d}"
|
||||||
|
headless_log = variant_saved / f"{view['name']}{suffix}-mesh.log"
|
||||||
|
render_log = variant_saved / f"{view['name']}{suffix}-render.log"
|
||||||
|
quality_log = variant_saved / f"{view['name']}{suffix}-quality.log"
|
||||||
|
yaw, pitch = angles(probe_camera, probe_target)
|
||||||
|
camera = (*probe_camera, yaw, pitch)
|
||||||
|
export_command = [
|
||||||
|
str(BINARIES[variant]), str(DATA), "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--workers=12",
|
||||||
|
"--clip-radius=3", "--max-clip-level=0", "--max-level=0", "--force-generate",
|
||||||
|
"--recipe=surface-world", f"--camera={fmt(camera)}",
|
||||||
|
"--render-distance-chunks=1", f"--mesh-export={scene}",
|
||||||
|
]
|
||||||
|
export_output = run_process(f"{variant}-{view['name']}{suffix}-mesh", export_command,
|
||||||
|
headless_log)
|
||||||
|
if "status=PASS" not in export_output or not scene.is_file():
|
||||||
|
raise RuntimeError(f"{variant}-{view['name']}: missing successful scene export")
|
||||||
|
source_triangles = mesh_triangle_count(scene)
|
||||||
|
if source_triangles <= 0:
|
||||||
|
attempt_errors.append(f"{attempt_kind}@{route_distance}: empty mesh")
|
||||||
|
if variant == "after" or view["kind"] == "mouth":
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
actual_camera, actual_target = probe_camera, probe_target
|
||||||
|
actual_focus = probe_focus
|
||||||
|
surface_distance = None
|
||||||
|
if attempt_kind == "route-offset":
|
||||||
|
anchor = nearest_surface_anchor(scene, probe_focus)
|
||||||
|
surface_distance = anchor["distance_m"]
|
||||||
|
actual_camera = add(anchor["position_m"], mul(anchor["normal"], 12.0))
|
||||||
|
actual_target = anchor["position_m"]
|
||||||
|
actual_focus = anchor["position_m"]
|
||||||
|
actual_yaw, actual_pitch = angles(actual_camera, actual_target)
|
||||||
|
render_command = [
|
||||||
|
str(RENDERERS[variant]), str(scene), str(image), "--size=1920x1080",
|
||||||
|
f"--camera-m={fmt(actual_camera)}", f"--yaw-deg={actual_yaw:.6f}",
|
||||||
|
f"--pitch-deg={actual_pitch:.6f}", "--shade=readable", "--headlamp=on",
|
||||||
|
"--height-tint=on", f"--height-center-m={actual_focus[2]:.6f}",
|
||||||
|
"--height-range-m=32",
|
||||||
|
]
|
||||||
|
render_output = run_process(f"{variant}-{view['name']}{suffix}-render", render_command,
|
||||||
|
render_log)
|
||||||
|
drawn = DRAWN.search(render_output)
|
||||||
|
if drawn is None or int(drawn.group(1)) <= 0 or not image.is_file():
|
||||||
|
attempt_errors.append(f"{attempt_kind}@{route_distance}: no drawn geometry")
|
||||||
|
if variant == "after" or view["kind"] == "mouth":
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
quality = run_process(f"{variant}-{view['name']}{suffix}-quality", [
|
||||||
|
"python3", "-B", str(ROOT / "Standalone/Tools/verify/capture_quality.py"),
|
||||||
|
str(image)], quality_log, allow_failure=True)
|
||||||
|
quality_status = "pass"
|
||||||
|
if "status=PASS" not in quality:
|
||||||
|
metrics = QUALITY_METRICS.search(quality)
|
||||||
|
coverage = float(metrics.group(2)) if metrics else 0.0
|
||||||
|
if coverage >= 0.001:
|
||||||
|
quality_status = "low_contrast_geometry_visible"
|
||||||
|
else:
|
||||||
|
attempt_errors.append(f"{attempt_kind}@{route_distance}: no visible pixel coverage")
|
||||||
|
if variant == "after" or view["kind"] == "mouth":
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = {**view, "variant": variant, "capture": str(image.relative_to(RESEARCH)),
|
||||||
|
"scene": str(scene.relative_to(ROOT)), "drawn_triangles": int(drawn.group(1)),
|
||||||
|
"source_triangles": source_triangles, "verified": True,
|
||||||
|
"pixel_quality_status": quality_status,
|
||||||
|
"route_distance_actual_m": route_distance,
|
||||||
|
"route_station_adjustment_m": route_adjustment,
|
||||||
|
"fallback_kind": attempt_kind if attempt_kind != "requested" else None,
|
||||||
|
"fallback_surface_distance_m": surface_distance,
|
||||||
|
"camera_actual_m": actual_camera, "target_actual_m": actual_target,
|
||||||
|
"logs": [str(headless_log.relative_to(ROOT)),
|
||||||
|
str(render_log.relative_to(ROOT)),
|
||||||
|
str(quality_log.relative_to(ROOT))]}
|
||||||
|
print("[L109Capture] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
raise RuntimeError(f"{variant}-{view['name']}: no verified image; "
|
||||||
|
+ "; ".join(attempt_errors))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if any(not path.is_file() for path in (*BINARIES.values(), *RENDERERS.values(),
|
||||||
|
*LAYOUTS.values(), AUDIT_LOG)):
|
||||||
|
raise SystemExit("missing L109 capture binary, layout, or audit log")
|
||||||
|
sections = []
|
||||||
|
for passage, section, start, end, samples, midpoint, radius in SECTION.findall(
|
||||||
|
AUDIT_LOG.read_text(encoding="utf-8", errors="replace")):
|
||||||
|
sections.append({"passage": passage, "section": int(section),
|
||||||
|
"start_m": float(start), "end_m": float(end),
|
||||||
|
"samples": int(samples), "midpoint": vector(midpoint),
|
||||||
|
"radius_m": float(radius)})
|
||||||
|
if not sections:
|
||||||
|
raise SystemExit("no open-air sections found in the final passage audit")
|
||||||
|
all_rows = []
|
||||||
|
for variant in ("before", "after"):
|
||||||
|
layout = json.loads(LAYOUTS[variant].read_text(encoding="utf-8"))
|
||||||
|
include_open_air = True
|
||||||
|
views = make_views(layout, sections, use_derived_shape=variant == "after",
|
||||||
|
include_open_air=include_open_air)
|
||||||
|
expected = 5 * len(layout["passages"])
|
||||||
|
if include_open_air:
|
||||||
|
expected += 3 * len(sections)
|
||||||
|
if len(views) != expected:
|
||||||
|
raise RuntimeError(f"unexpected camera count: {len(views)} != {expected}")
|
||||||
|
print(f"[L109CapturePlan] variant={variant} passage_views={5*len(layout['passages'])} "
|
||||||
|
f"open_air_sections={len(sections) if include_open_air else 0} "
|
||||||
|
f"open_air_views={3*len(sections) if include_open_air else 0} "
|
||||||
|
f"total={len(views)}", flush=True)
|
||||||
|
for view in views:
|
||||||
|
all_rows.append(capture(variant, view, layout))
|
||||||
|
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||||
|
(CAPTURES / "capture-index.json").write_text(
|
||||||
|
json.dumps({"open_air_sections": sections, "captures": all_rows}, indent=2) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
lines = ["# L109 passage captures", "",
|
||||||
|
"Each PNG was rendered from its own exported mesh. The renderer reported visible "
|
||||||
|
"triangles, and each decoded image met the pixel coverage floor. "
|
||||||
|
"`low_contrast_geometry_visible` marks images that met this floor below the "
|
||||||
|
"preferred contrast target.", ""]
|
||||||
|
for variant in ("before", "after"):
|
||||||
|
lines.extend([f"## {variant.title()}", ""])
|
||||||
|
for row in all_rows:
|
||||||
|
if row["variant"] != variant:
|
||||||
|
continue
|
||||||
|
flag = " **OPEN AIR**" if row["kind"] == "open-air" else ""
|
||||||
|
quality_note = (f"; {row['pixel_quality_status']}"
|
||||||
|
if row["pixel_quality_status"] != "pass" else "")
|
||||||
|
lines.append(f"- [{row['station']}]({variant}/{Path(row['capture']).name}) — "
|
||||||
|
f"{row['passage']}{flag}; {row['drawn_triangles']} visible triangles"
|
||||||
|
f"{quality_note}")
|
||||||
|
lines.append("")
|
||||||
|
(CAPTURES / "INDEX.md").write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
failures = [row for row in all_rows if not row["verified"]]
|
||||||
|
print(f"[L109CaptureSummary] captures={len(all_rows)} verified={len(all_rows)-len(failures)} "
|
||||||
|
f"failed={len(failures)} open_air_sections={len(sections)}", flush=True)
|
||||||
|
return 1 if failures else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare LOD0-2 owner mesh edge crossings before and after L109."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import statistics
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l109/crossings"
|
||||||
|
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
BINARIES = {
|
||||||
|
"before": ROOT / "Saved/Linux/l109/bin-baseline/VoxelForgeCoreHeadless-l109-baseline",
|
||||||
|
"after": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||||
|
}
|
||||||
|
CAMERAS = {
|
||||||
|
"SW": (8.0, 16.0, -372.2626953125),
|
||||||
|
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||||
|
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||||
|
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def read_exact(stream, size):
|
||||||
|
data = stream.read(size)
|
||||||
|
if len(data) != size:
|
||||||
|
raise RuntimeError("truncated VFMSH export")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def crossing_map(path: Path) -> tuple[dict, dict]:
|
||||||
|
crossings = {}
|
||||||
|
meta = {"tiles": 0, "surface_vertices": 0, "classified": 0}
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
magic = read_exact(stream, 8)
|
||||||
|
materials = magic == b"VFMSH002"
|
||||||
|
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||||
|
raise RuntimeError(f"invalid mesh export {path}")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12))
|
||||||
|
if version != (2 if materials else 1):
|
||||||
|
raise RuntimeError("VFMSH version mismatch")
|
||||||
|
read_exact(stream, 40)
|
||||||
|
voxel_m = voxel_um / 1_000_000.0
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", read_exact(stream, 16))
|
||||||
|
surface_count, _skirts, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", read_exact(stream, 20))
|
||||||
|
positions = struct.iter_unpack("<3f", read_exact(stream, vertex_count * 12))
|
||||||
|
values = list(positions)
|
||||||
|
read_exact(stream, vertex_count * 12) # normals
|
||||||
|
if materials:
|
||||||
|
read_exact(stream, vertex_count * 10)
|
||||||
|
read_exact(stream, index_count * 4 + mask_count)
|
||||||
|
meta["tiles"] += 1
|
||||||
|
meta["surface_vertices"] += min(surface_count, vertex_count)
|
||||||
|
unit = voxel_m * (1 << key[3])
|
||||||
|
for position in values[:min(surface_count, vertex_count)]:
|
||||||
|
grid = tuple(value / unit for value in position)
|
||||||
|
fractions = tuple(abs(value - round(value)) for value in grid)
|
||||||
|
axis = max(range(3), key=fractions.__getitem__)
|
||||||
|
if fractions[axis] < 1.0e-4:
|
||||||
|
continue
|
||||||
|
fixed = [index for index in range(3) if index != axis]
|
||||||
|
if any(fractions[index] > 0.01 for index in fixed):
|
||||||
|
continue
|
||||||
|
edge = [0, 0, 0]
|
||||||
|
for index in fixed:
|
||||||
|
edge[index] = int(round(grid[index]))
|
||||||
|
edge[axis] = math.floor(grid[axis])
|
||||||
|
identity = (key[3], axis, *edge)
|
||||||
|
crossings.setdefault(identity, tuple(float(v) for v in position))
|
||||||
|
meta["classified"] += 1
|
||||||
|
if stream.read(1):
|
||||||
|
raise RuntimeError("trailing data in mesh export")
|
||||||
|
meta["classified_pct"] = (100.0 * meta["classified"] / meta["surface_vertices"]
|
||||||
|
if meta["surface_vertices"] else 0.0)
|
||||||
|
return crossings, meta
|
||||||
|
|
||||||
|
|
||||||
|
def export(strate: str, variant: str) -> Path:
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
mesh = OUT / f"{strate.lower()}-{variant}.vfmesh"
|
||||||
|
log = OUT / f"{strate.lower()}-{variant}.log"
|
||||||
|
camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1740s", str(BINARIES[variant]), str(DATA),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--workers=8", "--clip-radius=3", "--max-clip-level=2", "--max-level=2",
|
||||||
|
"--force-generate", "--recipe=surface-world", f"--camera={camera}",
|
||||||
|
"--render-distance-chunks=0",
|
||||||
|
f"--mesh-export={mesh}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1740, check=False)
|
||||||
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
if result.returncode or not mesh.is_file() or "status=PASS" not in output:
|
||||||
|
raise RuntimeError(f"mesh export failed: {strate}/{variant} exit={result.returncode} {log}")
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
summaries = []
|
||||||
|
for strate in CAMERAS:
|
||||||
|
paths = {variant: export(strate, variant) for variant in ("before", "after")}
|
||||||
|
maps = {variant: crossing_map(path) for variant, path in paths.items()}
|
||||||
|
old, old_meta = maps["before"]
|
||||||
|
new, new_meta = maps["after"]
|
||||||
|
old_keys, new_keys = set(old), set(new)
|
||||||
|
shared = old_keys & new_keys
|
||||||
|
moved_cm = [100.0 * math.dist(old[key], new[key]) for key in shared
|
||||||
|
if math.dist(old[key], new[key]) > 0.0001]
|
||||||
|
row = {
|
||||||
|
"strate": strate, "scope": "camera LOD0-2 mesh window; global lattice edge IDs",
|
||||||
|
"added_crossings": len(new_keys - old_keys),
|
||||||
|
"removed_crossings": len(old_keys - new_keys),
|
||||||
|
"shared_crossings": len(shared), "moved_crossings": len(moved_cm),
|
||||||
|
"mean_moved_cm": statistics.mean(moved_cm) if moved_cm else 0.0,
|
||||||
|
"max_moved_cm": max(moved_cm, default=0.0),
|
||||||
|
"before": old_meta, "after": new_meta,
|
||||||
|
"classified_pair_coverage_pct": min(old_meta["classified_pct"],
|
||||||
|
new_meta["classified_pct"]),
|
||||||
|
"exports": {key: str(value.relative_to(ROOT)) for key, value in paths.items()},
|
||||||
|
}
|
||||||
|
summaries.append(row)
|
||||||
|
print("[L109CrossingDelta] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
(OUT / "summary.json").write_text(
|
||||||
|
json.dumps({"measurement": "per-vertex marching-cubes edge classifier; compare shared global lattice edges; positions >0.01cm count as moved",
|
||||||
|
"strata": summaries}, indent=2) + "\n", encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check Clang/GCC hash identity at W1/W8/W12 and repin v217 owner hashes."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l109/hashes"
|
||||||
|
DATASETS = {
|
||||||
|
"owner4": ROOT / "Standalone/data/owner_world.json",
|
||||||
|
"owner2": ROOT / "Standalone/data/regression/owner_world_2strate_l61.json",
|
||||||
|
}
|
||||||
|
BINARIES = {
|
||||||
|
"clang": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||||
|
"gcc": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-gcc",
|
||||||
|
}
|
||||||
|
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=(\S+)$", re.M)
|
||||||
|
|
||||||
|
|
||||||
|
def run(dataset: str, recipe: str, workers: int, compiler: str) -> dict:
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
label = f"{dataset}-{recipe}-w{workers}-{compiler}"
|
||||||
|
log = OUT / f"{label}.log"
|
||||||
|
layout = OUT / f"{label}.layout.json"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1740s", str(BINARIES[compiler]), str(DATASETS[dataset]),
|
||||||
|
f"--workers={workers}", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=8", f"--recipe={recipe}",
|
||||||
|
"--stream-hash", f"--resolved-layout-output={layout}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||||
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
hashes = HASH.findall(output)
|
||||||
|
if result.returncode or len(hashes) != 12:
|
||||||
|
raise SystemExit(f"hash run failed or incomplete: {label}; exit={result.returncode} "
|
||||||
|
f"hashes={len(hashes)} log={log}")
|
||||||
|
row = {"dataset": dataset, "recipe": recipe, "workers": workers,
|
||||||
|
"compiler": compiler, "exit_code": result.returncode,
|
||||||
|
"hashes": hashes, "source_stamp": (STAMP.search(output).group(1)
|
||||||
|
if STAMP.search(output) else None),
|
||||||
|
"elapsed_s": round(time.monotonic() - started, 3),
|
||||||
|
"log": str(log.relative_to(ROOT))}
|
||||||
|
print("[L109HashRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if any(not path.is_file() for path in (*DATASETS.values(), *BINARIES.values())):
|
||||||
|
raise SystemExit("missing L109 hash input")
|
||||||
|
rows = []
|
||||||
|
recipes = {"owner4": ("surface-world",),
|
||||||
|
"owner2": ("surface-world", "floating-islands")}
|
||||||
|
for dataset, dataset_recipes in recipes.items():
|
||||||
|
for recipe in dataset_recipes:
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
rows.append(run(dataset, recipe, workers, compiler))
|
||||||
|
(OUT / "runs-partial.json").write_text(
|
||||||
|
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||||
|
groups = {}
|
||||||
|
for dataset, dataset_recipes in recipes.items():
|
||||||
|
for recipe in dataset_recipes:
|
||||||
|
key = f"{dataset}/{recipe}"
|
||||||
|
group = [row for row in rows if row["dataset"] == dataset
|
||||||
|
and row["recipe"] == recipe]
|
||||||
|
reference = group[0]["hashes"]
|
||||||
|
groups[key] = {"all_worker_compiler_hashes_match": all(
|
||||||
|
row["hashes"] == reference for row in group),
|
||||||
|
"source_stamps": sorted({row["source_stamp"] for row in group}),
|
||||||
|
"runs": [{"workers": row["workers"], "compiler": row["compiler"],
|
||||||
|
"log": row["log"]} for row in group]}
|
||||||
|
if not groups[key]["all_worker_compiler_hashes_match"]:
|
||||||
|
raise SystemExit(f"determinism mismatch: {key}")
|
||||||
|
if dataset == "owner2":
|
||||||
|
canonical = next(row for row in group if row["workers"] == 12
|
||||||
|
and row["compiler"] == "clang")
|
||||||
|
recipe_name = "floating-islands" if recipe == "floating-islands" else "surface-world"
|
||||||
|
style = 1 if recipe_name == "surface-world" else 2
|
||||||
|
baseline = (ROOT / "Standalone/Tools/verify/baselines/owner-hash/canonical"
|
||||||
|
/ f"owner-hash-{recipe_name}-style{style}-v217.baseline")
|
||||||
|
baseline.write_text("\n".join(canonical["hashes"]) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
summary = {"runs": rows, "groups": groups,
|
||||||
|
"owner4_all_four_strata": "owner4/surface-world",
|
||||||
|
"owner2_canonical_pins": ["owner2/surface-world",
|
||||||
|
"owner2/floating-islands"]}
|
||||||
|
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Estimate L77 passage/support worker shares before and after L109."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l109/l77-phase-census"
|
||||||
|
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
BINARIES = {
|
||||||
|
"base": ROOT / "Saved/Linux/l109/bin-baseline-profile/VoxelForgeCoreHeadless-l109-base-profile",
|
||||||
|
"candidate": ROOT / "Saved/Linux/l109/bin-final-profile/VoxelForgeCoreHeadless-l109-candidate-profile",
|
||||||
|
}
|
||||||
|
CAMERAS = {
|
||||||
|
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||||
|
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||||
|
}
|
||||||
|
WORKER = re.compile(r"^\[WorldAssemblerLODWorker\] (.*)$", re.M)
|
||||||
|
SAMPLE = re.compile(r"^\[L77Sample\] (.*)$", re.M)
|
||||||
|
SAMPLER = re.compile(r"^\[L77Sampler\] action=stop (.*)$", re.M)
|
||||||
|
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||||
|
|
||||||
|
|
||||||
|
def run(stratum: str, variant: str, replicate: int) -> dict:
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
label = f"{stratum.lower()}-{variant}-r{replicate}"
|
||||||
|
log = OUT / f"{label}.log"
|
||||||
|
camera = ",".join(f"{value:.12f}" for value in CAMERAS[stratum]) + ",0,0"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1740s", str(BINARIES[variant]), str(DATA),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||||
|
f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3",
|
||||||
|
"--linux77-profile",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||||
|
content = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
sampler_match = SAMPLER.search(content)
|
||||||
|
workers = [dict(FIELDS.findall(match.group(1))) for match in WORKER.finditer(content)]
|
||||||
|
samples = [dict(FIELDS.findall(match.group(1))) for match in SAMPLE.finditer(content)]
|
||||||
|
if result.returncode or sampler_match is None or len(workers) < 9 or not samples:
|
||||||
|
raise SystemExit(f"phase census failed or markers missing: {log}; exit={result.returncode}")
|
||||||
|
sampler = dict(FIELDS.findall(sampler_match.group(1)))
|
||||||
|
if sampler.get("status") != "PASS":
|
||||||
|
raise SystemExit(f"L77 sampler failed: {log}: {sampler}")
|
||||||
|
lod_seconds = {int(row["lod"]): float(row["worker_sum_s"]) for row in workers}
|
||||||
|
|
||||||
|
def estimate(scope: str) -> dict:
|
||||||
|
lods = set(range(3)) if scope == "lod0-2" else set(range(9))
|
||||||
|
eligible = [row for row in samples if int(row["lod"]) in lods]
|
||||||
|
weights = {phase: sum(int(row.get("weight", "1")) for row in eligible
|
||||||
|
if row.get("phase") == phase)
|
||||||
|
for phase in ("passages", "support")}
|
||||||
|
total_weight = sum(int(row.get("weight", "1")) for row in eligible)
|
||||||
|
cpu_seconds = sum(value for lod, value in lod_seconds.items() if lod in lods)
|
||||||
|
result_row = {"worker_cpu_s": round(cpu_seconds, 4),
|
||||||
|
"weighted_samples": total_weight}
|
||||||
|
for phase, weight in weights.items():
|
||||||
|
share = weight / total_weight if total_weight else 0.0
|
||||||
|
result_row[f"{phase}_share_pct"] = round(share * 100.0, 3)
|
||||||
|
result_row[f"{phase}_worker_s_estimate"] = round(cpu_seconds * share, 4)
|
||||||
|
return result_row
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"stratum": stratum, "variant": variant, "replicate": replicate,
|
||||||
|
"exit_code": result.returncode,
|
||||||
|
"sampler": sampler,
|
||||||
|
"lod0_2": estimate("lod0-2"), "whole_view": estimate("whole"),
|
||||||
|
"log": str(log.relative_to(ROOT)),
|
||||||
|
"runner_wall_s": round(time.monotonic() - started, 3),
|
||||||
|
}
|
||||||
|
print("[L109PhaseCensusRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if any(not path.is_file() for path in BINARIES.values()):
|
||||||
|
raise SystemExit("missing L77 profile binary")
|
||||||
|
rows = [run(stratum, variant, replicate)
|
||||||
|
for stratum in ("FI", "TN")
|
||||||
|
for replicate in (1, 2)
|
||||||
|
for variant in ("base", "candidate")]
|
||||||
|
summary = []
|
||||||
|
for stratum in ("FI", "TN"):
|
||||||
|
for variant in ("base", "candidate"):
|
||||||
|
entry = {"stratum": stratum, "variant": variant, "replicates": 2}
|
||||||
|
group = [row for row in rows if row["stratum"] == stratum
|
||||||
|
and row["variant"] == variant]
|
||||||
|
for scope in ("lod0_2", "whole_view"):
|
||||||
|
for key in ("worker_cpu_s", "passages_share_pct", "passages_worker_s_estimate",
|
||||||
|
"support_share_pct", "support_worker_s_estimate"):
|
||||||
|
values = [row[scope][key] for row in group]
|
||||||
|
entry[f"{scope}_{key}_runs"] = values
|
||||||
|
entry[f"{scope}_{key}_median"] = statistics.median(values)
|
||||||
|
summary.append(entry)
|
||||||
|
(OUT / "summary.json").write_text(
|
||||||
|
json.dumps({"runs": rows, "summary": summary}, indent=2) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Five interleaved L80 readiness pairs for L109, at W8 and W12."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l109/timing"
|
||||||
|
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
BINARIES = {
|
||||||
|
"base": ROOT / "Saved/Linux/l109/bin-baseline/VoxelForgeCoreHeadless-l109-baseline",
|
||||||
|
"candidate": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||||
|
}
|
||||||
|
CAMERAS = {
|
||||||
|
0: (8.0, 16.0, -372.2626953125),
|
||||||
|
1: (25.8369140625, 3.19921875, -737.0),
|
||||||
|
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||||
|
3: (-20.0625, -5.068359375, -900.353515625),
|
||||||
|
}
|
||||||
|
PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
|
||||||
|
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||||
|
NEAR = re.compile(
|
||||||
|
r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$",
|
||||||
|
re.M,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run(strate: int, workers: int, variant: str, phase: str, replicate: int) -> dict:
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
label = f"s{strate}-w{workers}-{variant}-{phase}{replicate}"
|
||||||
|
log = OUT / f"{label}.log"
|
||||||
|
camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1740s", str(BINARIES[variant]), str(DATA),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||||
|
f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3",
|
||||||
|
]
|
||||||
|
start = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as output:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||||
|
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||||
|
content = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
pipeline_match = PIPELINE.search(content)
|
||||||
|
near_match = NEAR.search(content)
|
||||||
|
if result.returncode or pipeline_match is None or near_match is None:
|
||||||
|
raise SystemExit(f"readiness run failed or trace missing: {log}; exit={result.returncode}")
|
||||||
|
pipeline = dict(FIELDS.findall(pipeline_match.group(1)))
|
||||||
|
row = {
|
||||||
|
"strate": strate, "workers": workers, "variant": variant,
|
||||||
|
"phase": phase, "replicate": replicate, "exit_code": result.returncode,
|
||||||
|
"wall_s": round(time.monotonic() - start, 3),
|
||||||
|
"lod02_ready_s": float(near_match.group(3)),
|
||||||
|
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
|
||||||
|
"whole_tiles": int(pipeline["whole_tiles"].split("/")[0]),
|
||||||
|
"log": str(log.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
print("[L109TimingRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if not DATA.is_file() or any(not binary.is_file() for binary in BINARIES.values()):
|
||||||
|
raise SystemExit("missing data or timing binary")
|
||||||
|
rows = []
|
||||||
|
for strate in range(4):
|
||||||
|
for workers in (8, 12):
|
||||||
|
run(strate, workers, "base", "warmup", 0)
|
||||||
|
run(strate, workers, "candidate", "warmup", 0)
|
||||||
|
for replicate in range(1, 6):
|
||||||
|
order = ("base", "candidate") if replicate % 2 else ("candidate", "base")
|
||||||
|
for variant in order:
|
||||||
|
rows.append(run(strate, workers, variant, "measured", replicate))
|
||||||
|
(OUT / "runs-partial.json").write_text(
|
||||||
|
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||||
|
summary = []
|
||||||
|
for strate in range(4):
|
||||||
|
for workers in (8, 12):
|
||||||
|
item = {"strate": strate, "workers": workers, "pairs": 5}
|
||||||
|
for variant in ("base", "candidate"):
|
||||||
|
group = [row for row in rows if row["strate"] == strate
|
||||||
|
and row["workers"] == workers and row["variant"] == variant]
|
||||||
|
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||||
|
values = [row[key] for row in group]
|
||||||
|
item[f"{variant}_{key}"] = values
|
||||||
|
item[f"{variant}_{key}_median"] = statistics.median(values)
|
||||||
|
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||||
|
base = item[f"base_{key}_median"]
|
||||||
|
candidate = item[f"candidate_{key}_median"]
|
||||||
|
item[f"{key}_delta_pct"] = round(100 * (candidate / base - 1), 2)
|
||||||
|
summary.append(item)
|
||||||
|
(OUT / "summary.json").write_text(
|
||||||
|
json.dumps({"runs": rows, "summary": summary}, indent=2) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run one pinned null-RHI SW/FI arrival for Linux 107."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from collections import Counter
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
HOST = Path("/home/jahni/VFHost")
|
||||||
|
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l109"
|
||||||
|
HOST_SAVED = HOST / "Saved/Linux/l109"
|
||||||
|
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
||||||
|
OUT = ROOT / "Saved/Linux/l109/unreal-arrival"
|
||||||
|
SPAWNS_M = {
|
||||||
|
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||||
|
"SW": (25.8369140625, 3.19921875, -200.0),
|
||||||
|
}
|
||||||
|
MOVE_START_S = 20.0
|
||||||
|
MOVE_DURATION_S = 12.0
|
||||||
|
EXIT_S = MOVE_START_S + MOVE_DURATION_S + 5.0
|
||||||
|
RUN_TIMEOUT_S = 1740
|
||||||
|
|
||||||
|
|
||||||
|
def read_events(path: Path) -> list[dict]:
|
||||||
|
events = []
|
||||||
|
if path.is_file():
|
||||||
|
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||||
|
if line.startswith("{"):
|
||||||
|
try:
|
||||||
|
events.append(json.loads(line))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: list[float], fraction: float) -> float | None:
|
||||||
|
if not values:
|
||||||
|
return None
|
||||||
|
ordered = sorted(values)
|
||||||
|
return ordered[min(len(ordered) - 1, int(round((len(ordered) - 1) * fraction)))]
|
||||||
|
|
||||||
|
|
||||||
|
def sample_unreal_peak_rss() -> tuple[int, int] | None:
|
||||||
|
executable = str(EDITOR).encode()
|
||||||
|
project = str(HOST / "VFHost.uproject").encode()
|
||||||
|
peak: tuple[int, int] | None = None
|
||||||
|
for process in Path("/proc").glob("[0-9]*"):
|
||||||
|
try:
|
||||||
|
command_line = (process / "cmdline").read_bytes()
|
||||||
|
if executable not in command_line or project not in command_line:
|
||||||
|
continue
|
||||||
|
status = (process / "status").read_text(encoding="ascii", errors="ignore")
|
||||||
|
match = re.search(r"^VmHWM:\s+(\d+)\s+kB$", status, re.MULTILINE)
|
||||||
|
if match:
|
||||||
|
rss_kib = int(match.group(1))
|
||||||
|
if peak is None or rss_kib > peak[0]:
|
||||||
|
peak = (rss_kib, int(process.name))
|
||||||
|
except (FileNotFoundError, PermissionError, ProcessLookupError, OSError):
|
||||||
|
continue
|
||||||
|
return peak
|
||||||
|
|
||||||
|
|
||||||
|
def endphysics_gap(stdout: Path) -> tuple[float | None, int | None]:
|
||||||
|
release_times: dict[int, dict[int, datetime]] = {}
|
||||||
|
pattern = re.compile(
|
||||||
|
r"^\[(\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}\.\d{2}:\d{3})\].*"
|
||||||
|
r"LogTick: tick\s+(\d+)\s+-+\s+Release tick group ([34])")
|
||||||
|
with stdout.open(encoding="utf-8", errors="replace") as stream:
|
||||||
|
for line in stream:
|
||||||
|
match = pattern.match(line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
timestamp = datetime.strptime(match.group(1), "%Y.%m.%d-%H.%M.%S:%f")
|
||||||
|
frame = int(match.group(2))
|
||||||
|
group = int(match.group(3))
|
||||||
|
release_times.setdefault(frame, {})[group] = timestamp
|
||||||
|
gaps = [(round((times[4] - times[3]).total_seconds() * 1000.0, 3), frame)
|
||||||
|
for frame, times in release_times.items() if 3 in times and 4 in times]
|
||||||
|
return max(gaps, default=(None, None))
|
||||||
|
|
||||||
|
|
||||||
|
def run(stratum: str, repeat: int, phase: str, ready_depth: int,
|
||||||
|
background_cap: int, worker_reserve: int, helper_background: int,
|
||||||
|
tile_background_low: int, tile_background_normal: int, startup_boost: int,
|
||||||
|
tick_logs: bool, insights: bool) -> dict:
|
||||||
|
label = (f"{phase}-{stratum.lower()}-rd{ready_depth}-bg{background_cap}"
|
||||||
|
f"-reserve{worker_reserve}-helperbg{helper_background}"
|
||||||
|
f"-tilelow{tile_background_low}-tilenormal{tile_background_normal}"
|
||||||
|
f"-ticklog{int(tick_logs)}"
|
||||||
|
f"-boost{startup_boost}-{repeat}")
|
||||||
|
trace = OUT / f"{label}.trace.jsonl"
|
||||||
|
utrace = OUT / f"{label}.utrace"
|
||||||
|
abslog = OUT / f"{label}.log"
|
||||||
|
stdout = OUT / f"{label}.stdout.log"
|
||||||
|
output = HOST_PLUGIN_SAVED / "output" / label
|
||||||
|
userdir = HOST_SAVED / "user" / label
|
||||||
|
zendata = HOST_SAVED / "zen" / label
|
||||||
|
ddc = HOST_SAVED / "DDC"
|
||||||
|
for directory in (OUT, output, userdir, zendata, ddc):
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
trace.unlink(missing_ok=True)
|
||||||
|
utrace.unlink(missing_ok=True)
|
||||||
|
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWNS_M[stratum])
|
||||||
|
command = [
|
||||||
|
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
||||||
|
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
||||||
|
"-notraceserver", "-nocrashreports",
|
||||||
|
"-DisablePlugins=AndroidFileServer",
|
||||||
|
"-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}",
|
||||||
|
f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0",
|
||||||
|
"-archetype=TunnelNetwork", f"-slot={1 if stratum == 'FI' else 0}",
|
||||||
|
"-modes=export", "-gameconfig=1", "-coreworldtest=1",
|
||||||
|
"-ownerfourstrateplan=1", "-corevoxelsizecm=25", "-coreislandtest=0",
|
||||||
|
"-corefallbacktest=0", "-coreofftest=0", "-coreplantest=0",
|
||||||
|
"-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings", "-corehybridmesher=0",
|
||||||
|
f"-out={output}", "-voxel.StreamingCoarseFirst=1",
|
||||||
|
"-voxel.StreamingDynamicNearReserve=1",
|
||||||
|
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||||
|
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||||
|
"-voxel.StreamingResultBacklogCap=256",
|
||||||
|
f"-voxel.StreamingReadyQueueDepth={ready_depth}",
|
||||||
|
f"-voxel.StreamingBackgroundWorkerCap={background_cap}",
|
||||||
|
f"-voxel.StreamingBackgroundWorkerReserve={worker_reserve}",
|
||||||
|
f"-voxel.CoreParallelForBackgroundPriority={helper_background}",
|
||||||
|
f"-voxel.StreamingTileBackgroundLow={tile_background_low}",
|
||||||
|
f"-voxel.StreamingTileBackgroundNormal={tile_background_normal}",
|
||||||
|
f"-voxel.TestLogTickFunctions={int(tick_logs)}",
|
||||||
|
"-voxel.CoreColumnCacheDirectMapped=0", "-voxel.CoreMCGradientMemoization=1",
|
||||||
|
"-voxel.StreamingCollisionLookahead=1",
|
||||||
|
f"-voxel.StreamingStartupApplyBoost={startup_boost}",
|
||||||
|
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=140.0",
|
||||||
|
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||||
|
f"-voxel.TestMoveDurationSeconds={MOVE_DURATION_S:.1f}",
|
||||||
|
f"-voxel.TestExitSeconds={EXIT_S:.1f}", "-voxel.TestMoveDirection=1,0,0",
|
||||||
|
"-voxel.StreamingSchedulerPolicy=3", "-voxel.TestExitOnSteady=0",
|
||||||
|
"-coreworldstreamtrace=1", f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
||||||
|
"-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1",
|
||||||
|
"-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}",
|
||||||
|
f"-abslog={abslog}",
|
||||||
|
]
|
||||||
|
if insights:
|
||||||
|
command += ["-trace=cpu,task,frame", "-statnamedevents",
|
||||||
|
f"-tracefile={utrace}", "-tracefiletrunc"]
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "timeout", f"{RUN_TIMEOUT_S}s", *command]
|
||||||
|
started = time.monotonic()
|
||||||
|
with stdout.open("w", encoding="utf-8") as stream:
|
||||||
|
process = subprocess.Popen(wrapped, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
peak_rss = None
|
||||||
|
while process.poll() is None:
|
||||||
|
observed = sample_unreal_peak_rss()
|
||||||
|
if observed and (peak_rss is None or observed[0] > peak_rss[0]):
|
||||||
|
peak_rss = observed
|
||||||
|
time.sleep(0.25)
|
||||||
|
exit_code = process.wait()
|
||||||
|
observed = sample_unreal_peak_rss()
|
||||||
|
if observed and (peak_rss is None or observed[0] > peak_rss[0]):
|
||||||
|
peak_rss = observed
|
||||||
|
|
||||||
|
events = read_events(trace)
|
||||||
|
worker_backend_config = next(
|
||||||
|
(e for e in events if e.get("type") == "worker_backend_config"), {})
|
||||||
|
worker_cap_readback = next(
|
||||||
|
(e for e in events if e.get("type") == "streaming_worker_cap_readback"), {})
|
||||||
|
samples = [e for e in events if e.get("type") == "streaming_sample"]
|
||||||
|
arrival = [e for e in samples if float(e.get("t_s", 0.0)) < MOVE_START_S]
|
||||||
|
full = next((e for e in arrival
|
||||||
|
if sum(e.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||||
|
and sum(e.get("settled_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(e.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||||
|
first_admission = next((e for e in events if e.get("type") == "scheduler_admission"
|
||||||
|
and e.get("desired_rebuilt") == 1), None)
|
||||||
|
request_events = [e for e in events if e.get("type") == "request"]
|
||||||
|
initial_request_events = [e for e in request_events if e.get("epoch") == 0]
|
||||||
|
tile_by_ordinal = {e.get("request_ordinal"): e for e in events
|
||||||
|
if e.get("type") == "tile"}
|
||||||
|
collision_count = sum(1 for e in events if e.get("type") == "collision_ready"
|
||||||
|
and e.get("level", 9) <= 2
|
||||||
|
and full and e.get("t_s", 1e30) <= full["t_s"])
|
||||||
|
initial_tiles = [tile_by_ordinal[r.get("request_ordinal")] for r in initial_request_events
|
||||||
|
if full and r.get("t_s", 1e30) <= full["t_s"]
|
||||||
|
and r.get("request_ordinal") in tile_by_ordinal
|
||||||
|
and tile_by_ordinal[r["request_ordinal"]].get("level", 9) <= 2]
|
||||||
|
burst_start_s = float(first_admission["t_s"]) if first_admission else -1.0
|
||||||
|
burst_end_s = float(full["t_s"]) if full else -1.0
|
||||||
|
work_by_frame = {int(e.get("frame", -1)): e for e in events
|
||||||
|
if e.get("type") == "game_thread_work_frame"}
|
||||||
|
work_values = [e for e in work_by_frame.values()
|
||||||
|
if burst_start_s <= float(e.get("t_s", -1.0)) <= burst_end_s]
|
||||||
|
arrival_frame_ids = {int(e.get("frame", -1)) for e in work_values}
|
||||||
|
scheduler_by_frame = {int(e.get("frame", -1)): e for e in events
|
||||||
|
if e.get("type") == "scheduler_admission"}
|
||||||
|
scheduler_values = [e for e in events if e.get("type") == "scheduler_admission"
|
||||||
|
and burst_start_s <= float(e.get("t_s", -1.0)) <= burst_end_s]
|
||||||
|
apply_by_frame = {int(e.get("frame", -1)): e for e in events
|
||||||
|
if e.get("type") == "apply_frame"}
|
||||||
|
commandlet_tick_by_frame = {int(e.get("frame", -1)): e for e in events
|
||||||
|
if e.get("type") == "commandlet_world_tick_frame"}
|
||||||
|
garbage_collect_by_frame = {int(e.get("frame", -1)): e for e in events
|
||||||
|
if e.get("type") == "garbage_collect"}
|
||||||
|
# apply_frame N+1 records the interval containing AVoxelWorld::Tick N. Its apply_ms
|
||||||
|
# belongs to Tick N+1, so use apply_by_frame[N] for the same-tick apply breakdown.
|
||||||
|
near_intervals = [apply_by_frame[frame + 1] for frame in arrival_frame_ids
|
||||||
|
if frame + 1 in apply_by_frame]
|
||||||
|
rmc_by_frame: dict[int, dict[str, float]] = {}
|
||||||
|
for event in events:
|
||||||
|
frame = int(event.get("frame", -1))
|
||||||
|
if event.get("type") != "rmc_submit" or frame not in arrival_frame_ids:
|
||||||
|
continue
|
||||||
|
values = rmc_by_frame.setdefault(frame, {
|
||||||
|
"component_acquire_ms": 0.0,
|
||||||
|
"create_section_group_ms": 0.0,
|
||||||
|
"create_sections_ms": 0.0,
|
||||||
|
"collision_handoff_ms": 0.0,
|
||||||
|
})
|
||||||
|
for key in values:
|
||||||
|
values[key] += float(event.get(key, 0.0))
|
||||||
|
collision_completion_by_frame: dict[int, float] = {}
|
||||||
|
for event in events:
|
||||||
|
frame = int(event.get("frame", -1))
|
||||||
|
if event.get("type") == "collision_completion" and frame in arrival_frame_ids:
|
||||||
|
collision_completion_by_frame[frame] = collision_completion_by_frame.get(frame, 0.0) \
|
||||||
|
+ float(event.get("handler_ms", 0.0))
|
||||||
|
near_requests = [r for r in request_events if full and r.get("t_s", 1e30) <= full["t_s"]
|
||||||
|
and r.get("level", 9) <= 2]
|
||||||
|
initial_lod0_counts = Counter(
|
||||||
|
tuple(r.get("tile", ())) for r in near_requests if r.get("level") == 0)
|
||||||
|
repeated_lod0_keys = [
|
||||||
|
{"tile": list(tile), "requests": count}
|
||||||
|
for tile, count in sorted(initial_lod0_counts.items()) if count > 1]
|
||||||
|
initial_scheduler_frames = [e for e in events
|
||||||
|
if e.get("type") == "scheduler_admission" and full
|
||||||
|
and e.get("t_s", 1e30) <= full["t_s"]]
|
||||||
|
worst_interval = max(near_intervals, key=lambda e: float(e.get("frame_ms", 0.0)), default=None)
|
||||||
|
worst_interval_frame_id = int(worst_interval.get("frame", -1)) if worst_interval else -1
|
||||||
|
worst_work_frame_id = worst_interval_frame_id - 1
|
||||||
|
worst_work = work_by_frame.get(worst_work_frame_id, {})
|
||||||
|
worst_apply = apply_by_frame.get(worst_work_frame_id, {})
|
||||||
|
worst_scheduler = scheduler_by_frame.get(worst_work_frame_id, {})
|
||||||
|
worst_rmc = rmc_by_frame.get(worst_work_frame_id, {})
|
||||||
|
worst_collision_completion_ms = collision_completion_by_frame.get(worst_work_frame_id, 0.0)
|
||||||
|
rmc_values = [rmc_by_frame.get(frame, {}) for frame in arrival_frame_ids]
|
||||||
|
apply_values = [apply_by_frame[frame] for frame in arrival_frame_ids
|
||||||
|
if frame in apply_by_frame]
|
||||||
|
commandlet_tick_values = [commandlet_tick_by_frame[frame] for frame in arrival_frame_ids
|
||||||
|
if frame in commandlet_tick_by_frame]
|
||||||
|
garbage_collect_values = [garbage_collect_by_frame[frame] for frame in arrival_frame_ids
|
||||||
|
if frame in garbage_collect_by_frame]
|
||||||
|
|
||||||
|
def work_stat(name: str, fraction: float | None = None) -> float | None:
|
||||||
|
values = [float(e.get(name, 0.0)) for e in work_values]
|
||||||
|
return percentile(values, fraction) if fraction is not None else max(values, default=0.0)
|
||||||
|
|
||||||
|
def scheduler_stat(name: str, fraction: float | None = None) -> float | None:
|
||||||
|
values = [float(e.get(name, 0.0)) for e in scheduler_values]
|
||||||
|
return percentile(values, fraction) if fraction is not None else max(values, default=0.0)
|
||||||
|
|
||||||
|
def rmc_stat(name: str, fraction: float | None = None) -> float | None:
|
||||||
|
values = [float(e.get(name, 0.0)) for e in rmc_values]
|
||||||
|
return percentile(values, fraction) if fraction is not None else max(values, default=0.0)
|
||||||
|
|
||||||
|
def commandlet_stat(name: str, fraction: float | None = None) -> float | None:
|
||||||
|
values = [float(e.get(name, 0.0)) for e in commandlet_tick_values]
|
||||||
|
return percentile(values, fraction) if fraction is not None else max(values, default=0.0)
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"label": label, "stratum": stratum, "repeat": repeat, "phase": phase,
|
||||||
|
"ready_queue_depth": ready_depth,
|
||||||
|
"requested_background_worker_cap": background_cap,
|
||||||
|
"background_worker_cap": worker_cap_readback.get(
|
||||||
|
"streaming_background_worker_cap", background_cap),
|
||||||
|
"requested_background_worker_reserve": worker_reserve,
|
||||||
|
"background_worker_reserve": worker_backend_config.get(
|
||||||
|
"streaming_background_worker_reserve", worker_cap_readback.get(
|
||||||
|
"background_worker_reserve", worker_reserve)),
|
||||||
|
"requested_core_parallel_for_background_priority": helper_background,
|
||||||
|
"core_parallel_for_background_priority": worker_backend_config.get(
|
||||||
|
"core_parallel_for_background_priority", helper_background),
|
||||||
|
"requested_streaming_tile_background_low": tile_background_low,
|
||||||
|
"streaming_tile_background_low": worker_backend_config.get(
|
||||||
|
"streaming_tile_background_low", tile_background_low),
|
||||||
|
"requested_streaming_tile_background_normal": tile_background_normal,
|
||||||
|
"streaming_tile_background_normal": worker_backend_config.get(
|
||||||
|
"streaming_tile_background_normal", tile_background_normal),
|
||||||
|
"effective_tile_slots": worker_backend_config.get("effective_tile_slots"),
|
||||||
|
"background_worker_threads": worker_backend_config.get("taskgraph_background_threads"),
|
||||||
|
"startup_apply_boost": startup_boost, "insights": insights,
|
||||||
|
"engine_tick_logs": tick_logs,
|
||||||
|
"exit_code": exit_code,
|
||||||
|
"launch_wrapper": "taskset -c 0-11 timeout 1740s (no address-space limit)",
|
||||||
|
"peak_rss_kib": peak_rss[0] if peak_rss else None,
|
||||||
|
"peak_rss_mib": round(peak_rss[0] / 1024.0, 1) if peak_rss else None,
|
||||||
|
"peak_rss_pid": peak_rss[1] if peak_rss else None,
|
||||||
|
"endphysics_to_postphysics_gap_ms_max": None,
|
||||||
|
"endphysics_gap_frame": None,
|
||||||
|
"wall_s": round(time.monotonic() - started, 3),
|
||||||
|
"desired_t_s": next((e.get("t_s") for e in events if e.get("type") == "desired_set"), None),
|
||||||
|
"first_admission_t_s": first_admission.get("t_s") if first_admission else None,
|
||||||
|
"lod02_full_settled_t_s": full.get("t_s") if full else None,
|
||||||
|
"near_settle_from_first_admission_s": round(
|
||||||
|
float(full["t_s"]) - float(first_admission["t_s"]), 4)
|
||||||
|
if full and first_admission else None,
|
||||||
|
"desired_by_lod": full.get("desired_by_lod", [])[:3] if full else None,
|
||||||
|
"settled_by_lod": full.get("settled_by_lod", [])[:3] if full else None,
|
||||||
|
"near_admitted_tiles": len(near_requests),
|
||||||
|
"initial_lod0_admissions": sum(initial_lod0_counts.values()),
|
||||||
|
"repeated_lod0_request_keys_before_settle": repeated_lod0_keys,
|
||||||
|
"peak_result_backlog_before_settle": max(
|
||||||
|
(int(e.get("result_backlog", 0)) for e in initial_scheduler_frames), default=0),
|
||||||
|
"last_admission_t_s": max((e.get("t_s", 0.0) for e in near_requests), default=None),
|
||||||
|
"near_tile_events": len(initial_tiles),
|
||||||
|
"near_worker_seconds": round(sum(e.get("worker_task_s", 0.0) for e in initial_tiles), 4),
|
||||||
|
"near_core_seconds": round(sum(e.get("core_generate_s", 0.0) for e in initial_tiles), 4),
|
||||||
|
"collision_ready_by_settle": collision_count,
|
||||||
|
"burst_frame_count": len(near_intervals),
|
||||||
|
"burst_frame_ms_p95": round(sorted(e.get("frame_ms", 0.0) for e in near_intervals)[
|
||||||
|
min(len(near_intervals) - 1, int(.95 * (len(near_intervals) - 1)))], 3)
|
||||||
|
if near_intervals else None,
|
||||||
|
"burst_frame_ms_max": round(max((e.get("frame_ms", 0.0) for e in near_intervals), default=0.0), 3),
|
||||||
|
"burst_full_world_tick_ms_p99": round(commandlet_stat("world_tick_ms", .99) or 0.0, 3),
|
||||||
|
"burst_full_world_tick_ms_max": round(commandlet_stat("world_tick_ms") or 0.0, 3),
|
||||||
|
"burst_world_pre_actor_ms_p99": round(commandlet_stat("world_pre_actor_ms", .99) or 0.0, 3),
|
||||||
|
"burst_world_pre_actor_ms_max": round(commandlet_stat("world_pre_actor_ms") or 0.0, 3),
|
||||||
|
"burst_world_actor_phase_ms_p99": round(commandlet_stat("world_actor_phase_ms", .99) or 0.0, 3),
|
||||||
|
"burst_world_actor_phase_ms_max": round(commandlet_stat("world_actor_phase_ms") or 0.0, 3),
|
||||||
|
"burst_world_post_actor_ms_p99": round(commandlet_stat("world_post_actor_ms", .99) or 0.0, 3),
|
||||||
|
"burst_world_post_actor_ms_max": round(commandlet_stat("world_post_actor_ms") or 0.0, 3),
|
||||||
|
"burst_commandlet_readiness_check_ms_max": round(
|
||||||
|
commandlet_stat("readiness_check_ms") or 0.0, 3),
|
||||||
|
"burst_commandlet_sleep_ms_p99": round(commandlet_stat("sleep_ms", .99) or 0.0, 3),
|
||||||
|
"burst_commandlet_sleep_ms_max": round(commandlet_stat("sleep_ms") or 0.0, 3),
|
||||||
|
"burst_commandlet_loop_ms_max": round(commandlet_stat("loop_ms") or 0.0, 3),
|
||||||
|
"burst_gc_count": len(garbage_collect_values),
|
||||||
|
"burst_gc_duration_ms_max": round(max(
|
||||||
|
(float(e.get("duration_ms", 0.0)) for e in garbage_collect_values), default=0.0), 3),
|
||||||
|
"burst_max_meshes_per_frame": max((e.get("meshes_applied", 0) for e in apply_values), default=0),
|
||||||
|
"burst_max_apply_ms": round(max((e.get("apply_ms", 0.0) for e in apply_values), default=0.0), 3),
|
||||||
|
"burst_max_admit_per_frame": max((sum(e.get("submitted_by_lod", [0] * 9)[:3])
|
||||||
|
for e in scheduler_values), default=0),
|
||||||
|
"burst_frame_timing_count": len(work_values),
|
||||||
|
"burst_active_generation_workers_p95": round(work_stat("active_generation_workers", .95) or 0.0, 3),
|
||||||
|
"burst_active_generation_workers_max": round(work_stat("active_generation_workers") or 0.0, 3),
|
||||||
|
"burst_active_tile_tasks_max": round(work_stat("active_tile_tasks") or 0.0, 3),
|
||||||
|
"burst_world_tick_ms_p99": round(work_stat("tick_ms", .99) or 0.0, 3),
|
||||||
|
"burst_world_tick_ms_max": round(work_stat("tick_ms") or 0.0, 3),
|
||||||
|
"burst_update_chunks_ms_p99": round(work_stat("update_chunks_ms", .99) or 0.0, 3),
|
||||||
|
"burst_update_chunks_ms_max": round(work_stat("update_chunks_ms") or 0.0, 3),
|
||||||
|
"burst_pending_drain_ms_p99": round(work_stat("pending_drain_ms", .99) or 0.0, 3),
|
||||||
|
"burst_pending_drain_ms_max": round(work_stat("pending_drain_ms") or 0.0, 3),
|
||||||
|
"burst_post_apply_callbacks_ms_p99": round(work_stat("post_apply_callbacks_ms", .99) or 0.0, 3),
|
||||||
|
"burst_post_apply_callbacks_ms_max": round(work_stat("post_apply_callbacks_ms") or 0.0, 3),
|
||||||
|
"burst_visibility_reconcile_ms_p99": round(work_stat("visibility_reconcile_ms", .99) or 0.0, 3),
|
||||||
|
"burst_visibility_reconcile_ms_max": round(work_stat("visibility_reconcile_ms") or 0.0, 3),
|
||||||
|
"burst_collision_probe_ms_max": round(work_stat("collision_probe_ms") or 0.0, 3),
|
||||||
|
"burst_visible_apply_probe_ms_max": round(work_stat("visible_apply_probe_ms") or 0.0, 3),
|
||||||
|
"burst_lod0_ready_ms_max": round(work_stat("lod0_ready_ms") or 0.0, 3),
|
||||||
|
"burst_steady_trace_ms_max": round(work_stat("steady_trace_ms") or 0.0, 3),
|
||||||
|
"burst_modification_ms_max": round(work_stat("modification_ms") or 0.0, 3),
|
||||||
|
"burst_lifecycle_audit_ms_p99": round(work_stat("lifecycle_audit_ms", .99) or 0.0, 3),
|
||||||
|
"burst_lifecycle_audit_ms_max": round(work_stat("lifecycle_audit_ms") or 0.0, 3),
|
||||||
|
"burst_collision_gate_ms_p99": round(work_stat("collision_gate_ms", .99) or 0.0, 3),
|
||||||
|
"burst_collision_gate_ms_max": round(work_stat("collision_gate_ms") or 0.0, 3),
|
||||||
|
"burst_unload_ms_p99": round(work_stat("unload_ms", .99) or 0.0, 3),
|
||||||
|
"burst_unload_ms_max": round(work_stat("unload_ms") or 0.0, 3),
|
||||||
|
"burst_trace_json_ms_p99": round(work_stat("trace_json_ms", .99) or 0.0, 3),
|
||||||
|
"burst_trace_json_ms_max": round(work_stat("trace_json_ms") or 0.0, 3),
|
||||||
|
"burst_desired_rebuild_ms_max": round(scheduler_stat("desired_rebuild_ms") or 0.0, 3),
|
||||||
|
"burst_rmc_create_group_ms_p99": round(rmc_stat("create_section_group_ms", .99) or 0.0, 3),
|
||||||
|
"burst_rmc_create_group_ms_max": round(rmc_stat("create_section_group_ms") or 0.0, 3),
|
||||||
|
"burst_rmc_collision_handoff_ms_max": round(rmc_stat("collision_handoff_ms") or 0.0, 3),
|
||||||
|
"burst_collision_completion_ms_max": round(max(
|
||||||
|
collision_completion_by_frame.values(), default=0.0), 3),
|
||||||
|
"worst_frame_breakdown": {
|
||||||
|
"frame": worst_work_frame_id,
|
||||||
|
"world_tick_frame": worst_work_frame_id,
|
||||||
|
"frame_interval_end_frame": worst_interval_frame_id,
|
||||||
|
"frame_interval_ms": round(float(worst_interval.get("frame_ms", 0.0)), 3)
|
||||||
|
if worst_interval else None,
|
||||||
|
"apply_ms": round(float(worst_apply.get("apply_ms", 0.0)), 3)
|
||||||
|
if worst_apply else None,
|
||||||
|
"result_drain_ms": round(float(worst_apply.get("drain_ms", 0.0)), 3)
|
||||||
|
if worst_apply else None,
|
||||||
|
"outside_world_tick_ms": round(max(0.0, float(worst_interval.get("frame_ms", 0.0))
|
||||||
|
- float(worst_work.get("tick_ms", 0.0))), 3)
|
||||||
|
if worst_interval else None,
|
||||||
|
"outside_full_world_tick_ms": round(max(0.0, float(worst_interval.get("frame_ms", 0.0))
|
||||||
|
- float(commandlet_tick_by_frame.get(worst_work_frame_id, {}).get("world_tick_ms", 0.0))), 3)
|
||||||
|
if worst_interval else None,
|
||||||
|
"full_world_tick_ms": round(float(commandlet_tick_by_frame.get(
|
||||||
|
worst_work_frame_id, {}).get("world_tick_ms", 0.0)), 3),
|
||||||
|
"world_pre_actor_ms": round(float(commandlet_tick_by_frame.get(
|
||||||
|
worst_work_frame_id, {}).get("world_pre_actor_ms", 0.0)), 3),
|
||||||
|
"world_actor_phase_ms": round(float(commandlet_tick_by_frame.get(
|
||||||
|
worst_work_frame_id, {}).get("world_actor_phase_ms", 0.0)), 3),
|
||||||
|
"world_post_actor_ms": round(float(commandlet_tick_by_frame.get(
|
||||||
|
worst_work_frame_id, {}).get("world_post_actor_ms", 0.0)), 3),
|
||||||
|
"commandlet_readiness_check_ms": round(float(commandlet_tick_by_frame.get(
|
||||||
|
worst_work_frame_id, {}).get("readiness_check_ms", 0.0)), 3),
|
||||||
|
"commandlet_sleep_ms": round(float(commandlet_tick_by_frame.get(
|
||||||
|
worst_work_frame_id, {}).get("sleep_ms", 0.0)), 3),
|
||||||
|
"commandlet_loop_ms": round(float(commandlet_tick_by_frame.get(
|
||||||
|
worst_work_frame_id, {}).get("loop_ms", 0.0)), 3),
|
||||||
|
"gc_duration_ms": round(float(garbage_collect_by_frame.get(
|
||||||
|
worst_work_frame_id, {}).get("duration_ms", 0.0)), 3),
|
||||||
|
"world_tick_ms": round(float(worst_work.get("tick_ms", 0.0)), 3),
|
||||||
|
"active_generation_workers": int(worst_work.get("active_generation_workers", 0)),
|
||||||
|
"active_tile_tasks": int(worst_work.get("active_tile_tasks", 0)),
|
||||||
|
"collision_gate_ms": round(float(worst_work.get("collision_gate_ms", 0.0)), 3),
|
||||||
|
"update_chunks_ms": round(float(worst_work.get("update_chunks_ms", 0.0)), 3),
|
||||||
|
"desired_rebuild_ms": round(float(worst_scheduler.get("desired_rebuild_ms", 0.0)), 3),
|
||||||
|
"pending_drain_ms": round(float(worst_work.get("pending_drain_ms", 0.0)), 3),
|
||||||
|
"post_apply_callbacks_ms": round(float(worst_work.get("post_apply_callbacks_ms", 0.0)), 3),
|
||||||
|
"collision_probe_ms": round(float(worst_work.get("collision_probe_ms", 0.0)), 3),
|
||||||
|
"visibility_reconcile_ms": round(float(worst_work.get("visibility_reconcile_ms", 0.0)), 3),
|
||||||
|
"visible_apply_probe_ms": round(float(worst_work.get("visible_apply_probe_ms", 0.0)), 3),
|
||||||
|
"lod0_ready_ms": round(float(worst_work.get("lod0_ready_ms", 0.0)), 3),
|
||||||
|
"unload_ms": round(float(worst_work.get("unload_ms", 0.0)), 3),
|
||||||
|
"steady_trace_ms": round(float(worst_work.get("steady_trace_ms", 0.0)), 3),
|
||||||
|
"modification_ms": round(float(worst_work.get("modification_ms", 0.0)), 3),
|
||||||
|
"lifecycle_audit_ms": round(float(worst_work.get("lifecycle_audit_ms", 0.0)), 3),
|
||||||
|
"trace_json_ms": round(float(worst_work.get("trace_json_ms", 0.0)), 3),
|
||||||
|
"trace_archive_serialize_ms": round(float(
|
||||||
|
worst_work.get("trace_archive_serialize_ms", 0.0)), 3),
|
||||||
|
"collision_completion_ms": round(worst_collision_completion_ms, 3),
|
||||||
|
**{key: round(float(worst_rmc.get(key, 0.0)), 3)
|
||||||
|
for key in ("component_acquire_ms", "create_section_group_ms",
|
||||||
|
"create_sections_ms", "collision_handoff_ms")},
|
||||||
|
},
|
||||||
|
"trace": str(trace.relative_to(ROOT)), "utrace": str(utrace.relative_to(ROOT)),
|
||||||
|
"abslog": str(abslog.relative_to(ROOT)), "stdout": str(stdout.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
stdout_text = stdout.read_text(encoding="utf-8", errors="replace")
|
||||||
|
gap_ms, gap_frame = endphysics_gap(stdout)
|
||||||
|
row["endphysics_to_postphysics_gap_ms_max"] = gap_ms
|
||||||
|
row["endphysics_gap_frame"] = gap_frame
|
||||||
|
failure = re.search(r"\[VoxelForgeCoreWorldTest\] failed: ([^\r\n]+)", stdout_text)
|
||||||
|
if failure:
|
||||||
|
row["world_test_failure"] = failure.group(1)
|
||||||
|
with (OUT / "runs.jsonl").open("a", encoding="utf-8") as stream:
|
||||||
|
stream.write(json.dumps(row, sort_keys=True) + "\n")
|
||||||
|
(OUT / f"{label}.summary.json").write_text(json.dumps(row, indent=2) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
print("[L109UnrealArrival] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
if exit_code:
|
||||||
|
raise SystemExit(f"Unreal commandlet failed: {stdout}; exit={exit_code}")
|
||||||
|
if not full or len(arrival) < 8:
|
||||||
|
raise SystemExit(f"Unreal arrival trace incomplete: {stdout}; exit={exit_code}")
|
||||||
|
if insights and (not utrace.is_file() or utrace.stat().st_size < 1024):
|
||||||
|
raise SystemExit(f"Unreal Insights trace missing or empty: {utrace}")
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--stratum", choices=("FI", "SW"), required=True)
|
||||||
|
parser.add_argument("--repeat", type=int, default=1)
|
||||||
|
parser.add_argument("--phase", choices=("probe", "before", "after", "snapshot"), default="probe")
|
||||||
|
parser.add_argument("--ready-depth", type=int, default=64)
|
||||||
|
parser.add_argument("--background-cap", type=int, choices=(0, 1), default=1)
|
||||||
|
parser.add_argument("--worker-reserve", type=int, choices=(0, 1, 2, 3), default=0)
|
||||||
|
parser.add_argument("--helper-background", type=int, choices=(0, 1), default=1)
|
||||||
|
parser.add_argument("--tile-background-low", type=int, choices=(0, 1), default=1)
|
||||||
|
parser.add_argument("--tile-background-normal", type=int, choices=(0, 1), default=0)
|
||||||
|
parser.add_argument("--startup-boost", type=int, choices=(0, 1), default=1)
|
||||||
|
parser.add_argument("--tick-logs", action="store_true")
|
||||||
|
parser.add_argument("--insights", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not EDITOR.is_file():
|
||||||
|
raise SystemExit(f"missing Unreal commandlet executable: {EDITOR}")
|
||||||
|
run(args.stratum, args.repeat, args.phase, args.ready_depth,
|
||||||
|
args.background_cap, args.worker_reserve, args.helper_background,
|
||||||
|
args.tile_background_low, args.tile_background_normal, args.startup_boost,
|
||||||
|
args.tick_logs, args.insights)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# L109 passage captures
|
||||||
|
|
||||||
|
Each PNG was rendered from its own exported mesh. The renderer reported visible triangles, and each decoded image met the pixel coverage floor. `low_contrast_geometry_visible` marks images that met this floor below the preferred contrast target.
|
||||||
|
|
||||||
|
## Before
|
||||||
|
|
||||||
|
- [upper-mouth](before/passage-0-1-0-upper-mouth.png) — passage/0-1/0; 188440 visible triangles
|
||||||
|
- [lower-mouth](before/passage-0-1-0-lower-mouth.png) — passage/0-1/0; 564520 visible triangles
|
||||||
|
- [25%](before/passage-0-1-0-station-25.png) — passage/0-1/0; 234197 visible triangles
|
||||||
|
- [50%](before/passage-0-1-0-station-50.png) — passage/0-1/0; 315900 visible triangles
|
||||||
|
- [75%](before/passage-0-1-0-station-75.png) — passage/0-1/0; 256359 visible triangles
|
||||||
|
- [section-1/view-1](before/passage-0-1-0-openair-1-view-1.png) — passage/0-1/0 **OPEN AIR**; 211462 visible triangles
|
||||||
|
- [section-1/view-2](before/passage-0-1-0-openair-1-view-2.png) — passage/0-1/0 **OPEN AIR**; 208255 visible triangles
|
||||||
|
- [section-1/view-3](before/passage-0-1-0-openair-1-view-3.png) — passage/0-1/0 **OPEN AIR**; 221239 visible triangles
|
||||||
|
- [section-2/view-1](before/passage-0-1-0-openair-2-view-1.png) — passage/0-1/0 **OPEN AIR**; 218664 visible triangles
|
||||||
|
- [section-2/view-2](before/passage-0-1-0-openair-2-view-2.png) — passage/0-1/0 **OPEN AIR**; 223817 visible triangles
|
||||||
|
- [section-2/view-3](before/passage-0-1-0-openair-2-view-3.png) — passage/0-1/0 **OPEN AIR**; 221239 visible triangles
|
||||||
|
- [section-3/view-1](before/passage-0-1-0-openair-3-view-1.png) — passage/0-1/0 **OPEN AIR**; 224005 visible triangles
|
||||||
|
- [section-3/view-2](before/passage-0-1-0-openair-3-view-2.png) — passage/0-1/0 **OPEN AIR**; 230599 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-3/view-3](before/passage-0-1-0-openair-3-view-3.png) — passage/0-1/0 **OPEN AIR**; 229291 visible triangles
|
||||||
|
- [section-4/view-1](before/passage-0-1-0-openair-4-view-1.png) — passage/0-1/0 **OPEN AIR**; 407353 visible triangles
|
||||||
|
- [section-4/view-2](before/passage-0-1-0-openair-4-view-2.png) — passage/0-1/0 **OPEN AIR**; 406732 visible triangles
|
||||||
|
- [section-4/view-3](before/passage-0-1-0-openair-4-view-3.png) — passage/0-1/0 **OPEN AIR**; 363878 visible triangles
|
||||||
|
- [upper-mouth](before/passage-1-2-0-upper-mouth.png) — passage/1-2/0; 576595 visible triangles
|
||||||
|
- [lower-mouth](before/passage-1-2-0-lower-mouth.png) — passage/1-2/0; 555286 visible triangles
|
||||||
|
- [25%](before/passage-1-2-0-station-25.png) — passage/1-2/0; 667602 visible triangles
|
||||||
|
- [50%](before/passage-1-2-0-station-50.png) — passage/1-2/0; 602756 visible triangles
|
||||||
|
- [75%](before/passage-1-2-0-station-75.png) — passage/1-2/0; 620502 visible triangles
|
||||||
|
- [section-1/view-1](before/passage-1-2-0-openair-1-view-1.png) — passage/1-2/0 **OPEN AIR**; 636438 visible triangles
|
||||||
|
- [section-1/view-2](before/passage-1-2-0-openair-1-view-2.png) — passage/1-2/0 **OPEN AIR**; 593960 visible triangles
|
||||||
|
- [section-1/view-3](before/passage-1-2-0-openair-1-view-3.png) — passage/1-2/0 **OPEN AIR**; 622040 visible triangles
|
||||||
|
- [upper-mouth](before/passage-2-3-0-upper-mouth.png) — passage/2-3/0; 59568 visible triangles
|
||||||
|
- [lower-mouth](before/passage-2-3-0-lower-mouth.png) — passage/2-3/0; 294430 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [25%](before/passage-2-3-0-station-25.png) — passage/2-3/0; 55420 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [50%](before/passage-2-3-0-station-50.png) — passage/2-3/0; 458134 visible triangles
|
||||||
|
- [75%](before/passage-2-3-0-station-75.png) — passage/2-3/0; 475978 visible triangles
|
||||||
|
- [section-1/view-1](before/passage-2-3-0-openair-1-view-1.png) — passage/2-3/0 **OPEN AIR**; 104612 visible triangles
|
||||||
|
- [section-1/view-2](before/passage-2-3-0-openair-1-view-2.png) — passage/2-3/0 **OPEN AIR**; 81078 visible triangles
|
||||||
|
- [section-1/view-3](before/passage-2-3-0-openair-1-view-3.png) — passage/2-3/0 **OPEN AIR**; 94962 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-2/view-1](before/passage-2-3-0-openair-2-view-1.png) — passage/2-3/0 **OPEN AIR**; 476052 visible triangles
|
||||||
|
- [section-2/view-2](before/passage-2-3-0-openair-2-view-2.png) — passage/2-3/0 **OPEN AIR**; 479198 visible triangles
|
||||||
|
- [section-2/view-3](before/passage-2-3-0-openair-2-view-3.png) — passage/2-3/0 **OPEN AIR**; 479198 visible triangles
|
||||||
|
- [section-3/view-1](before/passage-2-3-0-openair-3-view-1.png) — passage/2-3/0 **OPEN AIR**; 485580 visible triangles
|
||||||
|
- [section-3/view-2](before/passage-2-3-0-openair-3-view-2.png) — passage/2-3/0 **OPEN AIR**; 478866 visible triangles
|
||||||
|
- [section-3/view-3](before/passage-2-3-0-openair-3-view-3.png) — passage/2-3/0 **OPEN AIR**; 490304 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-4/view-1](before/passage-2-3-0-openair-4-view-1.png) — passage/2-3/0 **OPEN AIR**; 475662 visible triangles
|
||||||
|
- [section-4/view-2](before/passage-2-3-0-openair-4-view-2.png) — passage/2-3/0 **OPEN AIR**; 496128 visible triangles
|
||||||
|
- [section-4/view-3](before/passage-2-3-0-openair-4-view-3.png) — passage/2-3/0 **OPEN AIR**; 496508 visible triangles; low_contrast_geometry_visible
|
||||||
|
|
||||||
|
## After
|
||||||
|
|
||||||
|
- [upper-mouth](after/passage-0-1-0-upper-mouth.png) — passage/0-1/0; 253282 visible triangles
|
||||||
|
- [lower-mouth](after/passage-0-1-0-lower-mouth.png) — passage/0-1/0; 559326 visible triangles
|
||||||
|
- [25%](after/passage-0-1-0-station-25.png) — passage/0-1/0; 145630 visible triangles
|
||||||
|
- [50%](after/passage-0-1-0-station-50.png) — passage/0-1/0; 430386 visible triangles
|
||||||
|
- [75%](after/passage-0-1-0-station-75.png) — passage/0-1/0; 294374 visible triangles
|
||||||
|
- [section-1/view-1](after/passage-0-1-0-openair-1-view-1.png) — passage/0-1/0 **OPEN AIR**; 289074 visible triangles
|
||||||
|
- [section-1/view-2](after/passage-0-1-0-openair-1-view-2.png) — passage/0-1/0 **OPEN AIR**; 298057 visible triangles
|
||||||
|
- [section-1/view-3](after/passage-0-1-0-openair-1-view-3.png) — passage/0-1/0 **OPEN AIR**; 322309 visible triangles
|
||||||
|
- [section-2/view-1](after/passage-0-1-0-openair-2-view-1.png) — passage/0-1/0 **OPEN AIR**; 283642 visible triangles
|
||||||
|
- [section-2/view-2](after/passage-0-1-0-openair-2-view-2.png) — passage/0-1/0 **OPEN AIR**; 316771 visible triangles
|
||||||
|
- [section-2/view-3](after/passage-0-1-0-openair-2-view-3.png) — passage/0-1/0 **OPEN AIR**; 322309 visible triangles
|
||||||
|
- [section-3/view-1](after/passage-0-1-0-openair-3-view-1.png) — passage/0-1/0 **OPEN AIR**; 262299 visible triangles
|
||||||
|
- [section-3/view-2](after/passage-0-1-0-openair-3-view-2.png) — passage/0-1/0 **OPEN AIR**; 310919 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-3/view-3](after/passage-0-1-0-openair-3-view-3.png) — passage/0-1/0 **OPEN AIR**; 363071 visible triangles
|
||||||
|
- [section-4/view-1](after/passage-0-1-0-openair-4-view-1.png) — passage/0-1/0 **OPEN AIR**; 378007 visible triangles
|
||||||
|
- [section-4/view-2](after/passage-0-1-0-openair-4-view-2.png) — passage/0-1/0 **OPEN AIR**; 471132 visible triangles
|
||||||
|
- [section-4/view-3](after/passage-0-1-0-openair-4-view-3.png) — passage/0-1/0 **OPEN AIR**; 382187 visible triangles
|
||||||
|
- [upper-mouth](after/passage-1-2-0-upper-mouth.png) — passage/1-2/0; 570417 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [lower-mouth](after/passage-1-2-0-lower-mouth.png) — passage/1-2/0; 511318 visible triangles
|
||||||
|
- [25%](after/passage-1-2-0-station-25.png) — passage/1-2/0; 661032 visible triangles
|
||||||
|
- [50%](after/passage-1-2-0-station-50.png) — passage/1-2/0; 602996 visible triangles
|
||||||
|
- [75%](after/passage-1-2-0-station-75.png) — passage/1-2/0; 615520 visible triangles
|
||||||
|
- [section-1/view-1](after/passage-1-2-0-openair-1-view-1.png) — passage/1-2/0 **OPEN AIR**; 629052 visible triangles
|
||||||
|
- [section-1/view-2](after/passage-1-2-0-openair-1-view-2.png) — passage/1-2/0 **OPEN AIR**; 588414 visible triangles
|
||||||
|
- [section-1/view-3](after/passage-1-2-0-openair-1-view-3.png) — passage/1-2/0 **OPEN AIR**; 616234 visible triangles
|
||||||
|
- [upper-mouth](after/passage-2-3-0-upper-mouth.png) — passage/2-3/0; 59562 visible triangles
|
||||||
|
- [lower-mouth](after/passage-2-3-0-lower-mouth.png) — passage/2-3/0; 294522 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [25%](after/passage-2-3-0-station-25.png) — passage/2-3/0; 55484 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [50%](after/passage-2-3-0-station-50.png) — passage/2-3/0; 457754 visible triangles
|
||||||
|
- [75%](after/passage-2-3-0-station-75.png) — passage/2-3/0; 475706 visible triangles
|
||||||
|
- [section-1/view-1](after/passage-2-3-0-openair-1-view-1.png) — passage/2-3/0 **OPEN AIR**; 104612 visible triangles
|
||||||
|
- [section-1/view-2](after/passage-2-3-0-openair-1-view-2.png) — passage/2-3/0 **OPEN AIR**; 81078 visible triangles
|
||||||
|
- [section-1/view-3](after/passage-2-3-0-openair-1-view-3.png) — passage/2-3/0 **OPEN AIR**; 94962 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-2/view-1](after/passage-2-3-0-openair-2-view-1.png) — passage/2-3/0 **OPEN AIR**; 475688 visible triangles
|
||||||
|
- [section-2/view-2](after/passage-2-3-0-openair-2-view-2.png) — passage/2-3/0 **OPEN AIR**; 479134 visible triangles
|
||||||
|
- [section-2/view-3](after/passage-2-3-0-openair-2-view-3.png) — passage/2-3/0 **OPEN AIR**; 479134 visible triangles
|
||||||
|
- [section-3/view-1](after/passage-2-3-0-openair-3-view-1.png) — passage/2-3/0 **OPEN AIR**; 485216 visible triangles
|
||||||
|
- [section-3/view-2](after/passage-2-3-0-openair-3-view-2.png) — passage/2-3/0 **OPEN AIR**; 479198 visible triangles
|
||||||
|
- [section-3/view-3](after/passage-2-3-0-openair-3-view-3.png) — passage/2-3/0 **OPEN AIR**; 490436 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-4/view-1](after/passage-2-3-0-openair-4-view-1.png) — passage/2-3/0 **OPEN AIR**; 475298 visible triangles
|
||||||
|
- [section-4/view-2](after/passage-2-3-0-openair-4-view-2.png) — passage/2-3/0 **OPEN AIR**; 495856 visible triangles
|
||||||
|
- [section-4/view-3](after/passage-2-3-0-openair-4-view-3.png) — passage/2-3/0 **OPEN AIR**; 496236 visible triangles; low_contrast_geometry_visible
|
||||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 513 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build review sheets for every L110 before/after passage capture."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
CAPTURES = Path(__file__).resolve().parent / "captures"
|
||||||
|
SHEETS = CAPTURES / "contact-sheets"
|
||||||
|
GROUPS = {
|
||||||
|
"openair-view-1": ("*-openair-*-view-1.png", "3x3"),
|
||||||
|
"openair-view-2": ("*-openair-*-view-2.png", "3x3"),
|
||||||
|
"openair-view-3": ("*-openair-*-view-3.png", "3x3"),
|
||||||
|
"openair-closeups": ("*-openair-*-closeup-*.png", "3x4"),
|
||||||
|
"mouths": ("*-*-*-*-mouth.png", "3x2"),
|
||||||
|
"stations": ("*-station-*.png", "3x3"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build(variant: str, group: str, pattern: str, layout: str) -> int:
|
||||||
|
files = sorted((CAPTURES / variant).glob(pattern))
|
||||||
|
if not files:
|
||||||
|
raise RuntimeError(f"no images matched {variant}/{pattern}")
|
||||||
|
columns, rows = (int(value) for value in layout.split("x"))
|
||||||
|
scale = "480:270"
|
||||||
|
output = SHEETS / f"{variant}-{group}.png"
|
||||||
|
command = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
|
||||||
|
"-f", "image2", "-pattern_type", "glob", "-framerate", "1",
|
||||||
|
"-i", str(CAPTURES / variant / pattern), "-vf",
|
||||||
|
f"scale={scale},tile={columns}x{rows}:nb_frames={columns*rows}:padding=6:margin=6",
|
||||||
|
"-frames:v", "1", str(output)]
|
||||||
|
result = subprocess.run(command, cwd=ROOT, check=False,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
|
if result.returncode:
|
||||||
|
raise RuntimeError(f"ffmpeg failed for {variant}/{group}: {result.stderr.strip()}")
|
||||||
|
return len(files)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
SHEETS.mkdir(parents=True, exist_ok=True)
|
||||||
|
rows = []
|
||||||
|
for variant in ("before", "after"):
|
||||||
|
for group, (pattern, layout) in GROUPS.items():
|
||||||
|
count = build(variant, group, pattern, layout)
|
||||||
|
rows.append(f"- [{variant} {group}]({variant}-{group}.png) — {count} views")
|
||||||
|
(SHEETS / "INDEX.md").write_text("# L110 capture contact sheets\n\n"
|
||||||
|
+ "\n".join(rows) + "\n", encoding="utf-8")
|
||||||
|
print(f"contact sheets={len(rows)} captures={sum(
|
||||||
|
int(row.rsplit('— ', 1)[1].split()[0]) for row in rows)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Linux 110 — open-air tunnel shell
|
||||||
|
|
||||||
|
Candidate evaluator v257, source stamp `04e9180521ab0963da5f1cabbf7c4646e32311edf1439c2dea514d3a61e7e464`.
|
||||||
|
|
||||||
|
## Cause and fix
|
||||||
|
|
||||||
|
The bands are the end profiles of per-control-span shell capsules. The affected 452.408 m open-air section touches 50 control spans: chord lengths are 8.520–10.076 m (median 9.353 m, mean 9.324 m). The visible band pitch follows that roughly 9.35 m span spacing. Radius delta is 0 across those spans, and the floor depth is constant, so radius/floor interpolation does not explain it. The 8 m tiles, 0.25 m voxels, and 0.75 m shell do not match the period. The pattern remains with tile skirts off; wall relief is unchanged. The chord/Catmull mismatch is not the periodic source.
|
||||||
|
|
||||||
|
The shell now smooth-unions adjacent spans over 6 m and reuses the chord distance already calculated for the passage interior. A 0.60 m shell clearance fades in from 5 m to 20 m from each mouth. Interior carving still takes precedence, and the shell is written only into host air. The repeated bands are removed from the exposed shell close-ups. Regular faceting on tunnel-facing interior walls remains visible in both L109 and v257 station captures.
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
The [capture index](captures/INDEX.md) flags every **OPEN AIR** view. The [verification](captures/capture-verification.json) passes all 106 images: per before/after set, 6 mouths, 9 stations, 27 open-air views, and 11 open-air close-ups. All 22 close-ups have a measured 4.000 m standoff. Nine images in each set are marked low contrast, with visible geometry. Paired bytes changed in 24/27 open-air views, 10/11 close-ups, 4/6 mouths, and 1/9 stations.
|
||||||
|
|
||||||
|
The [contact sheets](captures/contact-sheets/INDEX.md) cover all views. The target 4 m close-ups are [before 1](captures/before/passage-0-1-0-openair-4-closeup-1.png), [after 1](captures/after/passage-0-1-0-openair-4-closeup-1.png), [before 2](captures/before/passage-0-1-0-openair-4-closeup-2.png), [after 2](captures/after/passage-0-1-0-openair-4-closeup-2.png), [before 3](captures/before/passage-0-1-0-openair-4-closeup-3.png), and [after 3](captures/after/passage-0-1-0-openair-4-closeup-3.png).
|
||||||
|
|
||||||
|
## Readiness
|
||||||
|
|
||||||
|
Five interleaved L80 pairs per cell; times are medians in seconds (`LOD0–2 near-ready / whole-view-ready`). All base/candidate tile counts matched.
|
||||||
|
|
||||||
|
| Strate | W8 near | W8 whole | W12 near | W12 whole |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| SW | 0.778 → 0.854 (+9.7%) | 1.767 → 1.829 (+3.5%) | 0.807 → 0.906 (+12.3%) | 1.722 → 1.843 (+7.0%) |
|
||||||
|
| FI | 0.937 → 1.010 (+7.8%) | 1.811 → 1.903 (+5.1%) | 0.883 → 0.995 (+12.6%) | 1.700 → 1.798 (+5.8%) |
|
||||||
|
| TN | 0.979 → 1.090 (+11.4%) | 1.806 → 1.932 (+7.0%) | 0.920 → 1.006 (+9.3%) | 1.709 → 1.790 (+4.7%) |
|
||||||
|
| VS | 0.797 → 0.871 (+9.2%) | 1.699 → 1.765 (+3.9%) | 0.758 → 0.835 (+10.2%) | 1.575 → 1.659 (+5.3%) |
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Owner four-strata walk audit: all 3 interfaces pass; zero blocked, unsupported, or grade-failure samples. Shell audit: 89,365 floor, 8,423 wall, and 10,527 interior samples; zero violations; 22,755 open-air shell samples.
|
||||||
|
- Clang/GCC determinism: all 18 W1/W8/W12 hash runs match across owner4 Surface World, owner2 Surface World, and owner2 Floating Islands.
|
||||||
|
- Fast tier: PASS, 259 checks plus 5 diagnostic-only results; no failed gates.
|
||||||
|
- Linux VFHostEditor: PASS, 27 actions, 69.0 s, `-NoUBA -MaxParallelActions=4`. Unreal was run without `prlimit`.
|
||||||
|
- `git diff --check`: PASS. No run exceeded 30 minutes.
|
||||||
|
|
||||||
|
## Failed experiments
|
||||||
|
|
||||||
|
A clearance ramp delayed to 80–120 m left 6 owner2 wall samples uncovered. A 1.2 m mouth join produced 23 wall violations. Both were rejected; the final 6 m join with the 5–20 m clearance ramp passes owner2 and owner4 audits.
|
||||||