Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ea01988c4 |
No files matched your search
@@ -137,31 +137,11 @@ namespace VoxelForgeCore
|
||||
bool passageComposerProfiled = false;
|
||||
std::uint64_t passageTileFeatureCount = 0;
|
||||
std::uint64_t passageTileRouteSegmentCandidates = 0;
|
||||
std::uint64_t passageTileMouthSegmentCandidates = 0;
|
||||
std::uint64_t resolvedBoundaryBandQueries = 0;
|
||||
std::uint64_t passageComposerQueries = 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 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 passageSmoothstepEvaluations = 0;
|
||||
// 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.
|
||||
std::uint64_t passageSweepRouteSegmentCandidates = 0;
|
||||
@@ -488,7 +468,8 @@ namespace VoxelForgeCore
|
||||
// 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 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.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 217;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
@@ -1041,7 +1022,6 @@ namespace VoxelForgeCore
|
||||
{
|
||||
bool hasSurfaceLanding = false;
|
||||
bool hasFloatingIslandLanding = false;
|
||||
bool usePassageAnchorForColumns = false;
|
||||
bool hasNativeCaveField = false;
|
||||
bool hasNativeLatticeField = false;
|
||||
};
|
||||
@@ -1145,28 +1125,6 @@ namespace VoxelForgeCore
|
||||
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
|
||||
{
|
||||
std::int32_t upperStrateIndex = -1;
|
||||
@@ -1836,10 +1794,6 @@ namespace VoxelForgeCore
|
||||
bool profileMesherStages = false;
|
||||
// Research-only: count passage-composer work per generated tile.
|
||||
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;
|
||||
// a positive value bounds its signed-distance reach in metres. A negative
|
||||
// value leaves the process-level L93 setting in control.
|
||||
@@ -1932,12 +1886,6 @@ namespace VoxelForgeCore
|
||||
OreMiningYieldV1 measureOreMiningYieldForSphere(
|
||||
Vec3d centerVoxel, double radiusMetres,
|
||||
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.
|
||||
std::string densityDispatchPathAt(Vec3d voxelPosition) const;
|
||||
// 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))
|
||||
x.passageCapabilities.hasNativeLatticeField=true;
|
||||
else if(x.generatorKind=="FloatingIslands")
|
||||
{
|
||||
x.passageCapabilities.hasFloatingIslandLanding=true;
|
||||
x.passageCapabilities.usePassageAnchorForColumns=true;
|
||||
}
|
||||
if(i+1<strates.size())
|
||||
{
|
||||
const auto& lower=strates[i+1];
|
||||
|
||||
@@ -1284,103 +1284,104 @@ namespace VoxelForgeCore
|
||||
constexpr double Q=static_cast<double>(WorldAssemblySubvoxelsPerVoxel);
|
||||
const double voxelsPerMetre=coreVoxelSizeMicrons>0
|
||||
?1000000.0/coreVoxelSizeMicrons:4.0;
|
||||
const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre;
|
||||
const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre;
|
||||
constexpr double WallReliefMetres=0.21;
|
||||
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),
|
||||
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
|
||||
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
||||
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)
|
||||
{
|
||||
const auto& a=passage.controlPoints[segment-1];
|
||||
const auto& b=passage.controlPoints[segment];
|
||||
const double radius=static_cast<double>(std::max(
|
||||
a.radiusSubvoxels,b.radiusSubvoxels))/Q;
|
||||
const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels);
|
||||
const double low[3]={
|
||||
std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent,
|
||||
std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent,
|
||||
std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius
|
||||
-bridgeFootDepthVoxels};
|
||||
const double high[3]={
|
||||
std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent,
|
||||
std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent,
|
||||
std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius};
|
||||
const double extent=std::max(static_cast<double>(a.radiusSubvoxels),
|
||||
static_cast<double>(b.radiusSubvoxels))/Q+tubeExtentMetres;
|
||||
double low[3]{},high[3]{};
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
const double aPosition=axis==2?effectiveCenterZVoxels[segment-1]
|
||||
:static_cast<double>(a.positionSubvoxels[axis])/Q;
|
||||
const double bPosition=axis==2?effectiveCenterZVoxels[segment]
|
||||
:static_cast<double>(b.positionSubvoxels[axis])/Q;
|
||||
low[axis]=std::min(aPosition,bPosition)-extent;
|
||||
high[axis]=std::max(aPosition,bPosition)+extent;
|
||||
}
|
||||
bool overlap=true;
|
||||
for(int axis=0;axis<3;++axis)
|
||||
overlap&=high[axis]>=tileMinimum[axis]&&low[axis]<=tileMaximum[axis];
|
||||
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;
|
||||
}
|
||||
@@ -1866,44 +1867,6 @@ namespace VoxelForgeCore
|
||||
static_cast<double>(weighted)/65535.0*sampleVolume});
|
||||
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
|
||||
{ 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))
|
||||
@@ -2672,30 +2635,6 @@ namespace VoxelForgeCore
|
||||
RelevantMinimum=std::min(RelevantMinimum,FeatureMinimum);
|
||||
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
|
||||
// manifest features. Resolved SDF stamps can carve below that band or
|
||||
// add a floor there, so include every overlapping stamp's bounded Z
|
||||
@@ -2804,7 +2743,6 @@ namespace VoxelForgeCore
|
||||
FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope(
|
||||
tileShape,queryMinimum,queryMaximum,
|
||||
options.profilePassageComposer?&out.diagnostics:nullptr,key.lod,
|
||||
options.diagnosticPassageSourceFloorVoxels,
|
||||
options.diagnosticPassageCarveReachMetres);
|
||||
passageFeatureScope.prepareSweptTubeBase(origin,MeshStep,MeshCells,
|
||||
static_cast<std::uint32_t>(world.Seed));
|
||||
@@ -2904,10 +2842,6 @@ namespace VoxelForgeCore
|
||||
{
|
||||
out.diagnostics.passageTileRouteSegmentCandidates=
|
||||
passageFeatureScope.routeSegmentCandidateCount();
|
||||
for(std::size_t passageIndex=0;
|
||||
passageIndex<impl_->resolvedLayout->passages.size();++passageIndex)
|
||||
out.diagnostics.passageTileMouthSegmentCandidates+=
|
||||
passageFeatureScope.mouthSupportSegments(passageIndex).size();
|
||||
}
|
||||
}
|
||||
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 |
@@ -24,37 +24,6 @@ struct FStandaloneParitySample
|
||||
int32 StrateIndex = INDEX_NONE;
|
||||
};
|
||||
|
||||
struct FStandalonePassageVoidCrossing
|
||||
{
|
||||
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> startPoint{};
|
||||
std::array<double,4> endPoint{};
|
||||
bool needsColumn=false;
|
||||
bool connectedAtStart=false;
|
||||
bool connectedAtEnd=false;
|
||||
std::int64_t centerXSubvoxels=0;
|
||||
std::int64_t centerYSubvoxels=0;
|
||||
std::int64_t topZSubvoxels=0;
|
||||
std::int64_t bottomZSubvoxels=0;
|
||||
std::int64_t minimumRadiusSubvoxels=0;
|
||||
std::int64_t nominalRadiusSubvoxels=0;
|
||||
std::int64_t maximumRadiusSubvoxels=0;
|
||||
double naturalSurfaceReliefMetres=0.0;
|
||||
double maximumRouteOffsetMetres=0.0;
|
||||
double maximumBoreRadiusMetres=0.0;
|
||||
bool needsRootTaper=false;
|
||||
};
|
||||
|
||||
struct FStandalonePassageVoidCrossingCache
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::unordered_map<std::string,std::vector<FStandalonePassageVoidCrossing>> byLayout;
|
||||
};
|
||||
|
||||
struct FStandaloneOwnerWorld
|
||||
{
|
||||
std::unique_ptr<UVoxelSettings> Settings;
|
||||
@@ -81,8 +50,6 @@ struct FStandaloneOwnerWorld
|
||||
std::uint32_t StrateLayoutChunkSizeMicrometres = 8000000;
|
||||
std::int64_t StrateLayoutChunkWidthSubvoxels = 8192;
|
||||
bool bHasUnrealResolvedCorePlan = false;
|
||||
std::shared_ptr<FStandalonePassageVoidCrossingCache> PassageVoidCrossingCache =
|
||||
std::make_shared<FStandalonePassageVoidCrossingCache>();
|
||||
};
|
||||
|
||||
namespace StandaloneOwnerLoader
|
||||
|
||||