wip(assembler): round L30 -- FI floor ring defect fixed; transition cost recovered
The Gradient interface left native solid density in the air-side gap and snapped the face to voxel levels (concentric ring steps on the FI floor). The owner Gradient path now writes signed distance both sides; match error RMS 0.36 -> 0.11 m. Band and fade-tile culling cut transition cost: whole view vs L24 SW +2.7%, FI +0.2%. Evaluator 16. Deterministic across compilers and workers; interface and passage audits pass. Found: on the real FloatingIslands recipe, the rock-column connected-ends audit fails (all styles); earlier matrices ran SurfaceWorld twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
No files matched your search
@@ -249,8 +249,8 @@ namespace VoxelForgeCore
|
||||
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
||||
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
||||
// Version 14 adds shared passage columns for long native open-air crossings.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 15;
|
||||
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 16;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
|
||||
@@ -53,6 +53,10 @@ public:
|
||||
:shape_(&shape),minimumSubvoxels_(minimumSubvoxels),
|
||||
maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries)
|
||||
{
|
||||
mayContainResolvedBoundaryBand_=shape.ResolvedBoundaryBandsIntersectSubvoxelRange(
|
||||
minimumSubvoxels_[2],maximumSubvoxels_[2]);
|
||||
mayContainTransitionPassageFade_=shape.ResolvedTransitionPassageFadeIntersectsTileXY(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
featureIndices_=shape.BuildPassageFeatureTileList(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0);
|
||||
@@ -92,6 +96,13 @@ public:
|
||||
{return passageIndex<passageMouthSupports_.size()&&passageMouthSupports_[passageIndex]!=0;}
|
||||
bool hasRouteFeatureOverlap() const{return routeFeatureOverlap_;}
|
||||
bool hasColumnZOverlap() const{return columnZOverlap_;}
|
||||
bool mayContainResolvedBoundaryBand() const{return mayContainResolvedBoundaryBand_;}
|
||||
bool mayContainTransitionPassageFade() const{return mayContainTransitionPassageFade_;}
|
||||
bool mayContainResolvedBoundaryBandAtZ(double zMicrometres) const
|
||||
{
|
||||
return mayContainResolvedBoundaryBand_
|
||||
&&shape_->ResolvedBoundaryBandMayAffectAtZMicrometres(zMicrometres);
|
||||
}
|
||||
std::size_t featureCount() const{return featureIndices_.size();}
|
||||
|
||||
private:
|
||||
@@ -100,6 +111,8 @@ public:
|
||||
std::vector<std::uint32_t> featureIndices_;
|
||||
std::vector<std::uint8_t> passageRoutes_,passageColumns_,passageMouthSupports_;
|
||||
bool routeFeatureOverlap_=false,columnZOverlap_=false;
|
||||
bool mayContainResolvedBoundaryBand_=true;
|
||||
bool mayContainTransitionPassageFade_=true;
|
||||
const FScopedPassageTileQueries* previous_;
|
||||
};
|
||||
|
||||
@@ -174,6 +187,83 @@ public:
|
||||
&&Upper.transitionHonouredInCore;
|
||||
}
|
||||
|
||||
bool ResolvedBoundaryBandsIntersectSubvoxelRange(std::int64_t minimumZSubvoxels,
|
||||
std::int64_t maximumZSubvoxels) const
|
||||
{
|
||||
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0
|
||||
||minimumZSubvoxels>maximumZSubvoxels)return false;
|
||||
constexpr double Subvoxels=static_cast<double>(
|
||||
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
const double queryMinimumMicrometres=static_cast<double>(minimumZSubvoxels)
|
||||
*Owner->CoreVoxelSizeMicrons/Subvoxels;
|
||||
const double queryMaximumMicrometres=static_cast<double>(maximumZSubvoxels)
|
||||
*Owner->CoreVoxelSizeMicrons/Subvoxels;
|
||||
const double sampleMarginMicrometres=static_cast<double>(Owner->CoreVoxelSizeMicrons);
|
||||
for(const auto& interval:ResolvedBoundaryZIntervalsMicrometres)
|
||||
{
|
||||
if(queryMaximumMicrometres+sampleMarginMicrometres>=interval[0]-1.0
|
||||
&&queryMinimumMicrometres-sampleMarginMicrometres<interval[1]+1.0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResolvedBoundaryBandMayAffectAtZMicrometres(double zMicrometres) const
|
||||
{
|
||||
if(!std::isfinite(zMicrometres)||ResolvedBoundaryZIntervalsMicrometres.empty())return false;
|
||||
const auto it=std::upper_bound(ResolvedBoundaryZIntervalsMicrometres.begin(),
|
||||
ResolvedBoundaryZIntervalsMicrometres.end(),zMicrometres,
|
||||
[](double z,const auto& interval){return z<interval[0];});
|
||||
return it!=ResolvedBoundaryZIntervalsMicrometres.begin()
|
||||
&&zMicrometres<std::prev(it)->at(1);
|
||||
}
|
||||
|
||||
void BuildResolvedBoundaryZIntervals()
|
||||
{
|
||||
ResolvedBoundaryZIntervalsMicrometres.clear();
|
||||
if(!ResolvedLayout)return;
|
||||
for(const auto& stratum:ResolvedLayout->strates)
|
||||
{
|
||||
if(stratum.ceiling.presentInCurrentField
|
||||
&&stratum.ceiling.bottomMicrometres<stratum.ceiling.topMicrometres)
|
||||
ResolvedBoundaryZIntervalsMicrometres.push_back({
|
||||
static_cast<double>(stratum.ceiling.bottomMicrometres),
|
||||
static_cast<double>(stratum.ceiling.topMicrometres)});
|
||||
if(stratum.bedrock.presentInCurrentField
|
||||
&&stratum.bedrock.bottomMicrometres<stratum.bedrock.topMicrometres)
|
||||
ResolvedBoundaryZIntervalsMicrometres.push_back({
|
||||
static_cast<double>(stratum.bedrock.bottomMicrometres),
|
||||
static_cast<double>(stratum.bedrock.topMicrometres)});
|
||||
}
|
||||
for(std::int32_t order=0;order+1<static_cast<std::int32_t>(ResolvedLayout->strates.size());
|
||||
++order)
|
||||
{
|
||||
const auto* upper=FindResolvedLayoutStratumByOrder(order);
|
||||
const auto* lower=FindResolvedLayoutStratumByOrder(order+1);
|
||||
if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))continue;
|
||||
const auto displacement=ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
||||
*upper,*lower);
|
||||
ResolvedBoundaryZIntervalsMicrometres.push_back({
|
||||
static_cast<double>(std::min(upper->bedrock.bottomMicrometres,
|
||||
lower->ceiling.bottomMicrometres)-displacement),
|
||||
static_cast<double>(std::max(upper->bedrock.topMicrometres,
|
||||
lower->ceiling.topMicrometres)+displacement)});
|
||||
}
|
||||
std::sort(ResolvedBoundaryZIntervalsMicrometres.begin(),
|
||||
ResolvedBoundaryZIntervalsMicrometres.end(),
|
||||
[](const auto& a,const auto& b){return a[0]<b[0];});
|
||||
std::size_t write=0;
|
||||
for(const auto& interval:ResolvedBoundaryZIntervalsMicrometres)
|
||||
{
|
||||
if(write>0&&interval[0]<=ResolvedBoundaryZIntervalsMicrometres[write-1][1])
|
||||
ResolvedBoundaryZIntervalsMicrometres[write-1][1]=std::max(
|
||||
ResolvedBoundaryZIntervalsMicrometres[write-1][1],interval[1]);
|
||||
else
|
||||
ResolvedBoundaryZIntervalsMicrometres[write++]=interval;
|
||||
}
|
||||
ResolvedBoundaryZIntervalsMicrometres.resize(write);
|
||||
}
|
||||
|
||||
std::int64_t ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2& Upper,
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2& Lower) const
|
||||
@@ -189,11 +279,68 @@ public:
|
||||
return (maximumCrossing+1)/2;
|
||||
}
|
||||
|
||||
bool ResolvedTransitionPassageFadeIntersectsTileXY(
|
||||
const std::array<std::int64_t,3>& minimumSubvoxels,
|
||||
const std::array<std::int64_t,3>& maximumSubvoxels) const
|
||||
{
|
||||
if(!Owner||!ResolvedLayout||Owner->CoreVoxelSizeMicrons==0)return true;
|
||||
for(std::size_t axis=0;axis<2;++axis)
|
||||
if(minimumSubvoxels[axis]>maximumSubvoxels[axis])return true;
|
||||
constexpr std::int64_t Subvoxels=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr std::int64_t SupportMarginMicrometres=1000000;
|
||||
constexpr std::int64_t FadeWidthMicrometres=2000000;
|
||||
const double voxelMicrometres=static_cast<double>(Owner->CoreVoxelSizeMicrons);
|
||||
const double minimumX=static_cast<double>(minimumSubvoxels[0])*voxelMicrometres
|
||||
/Subvoxels-voxelMicrometres;
|
||||
const double maximumX=static_cast<double>(maximumSubvoxels[0])*voxelMicrometres
|
||||
/Subvoxels+voxelMicrometres;
|
||||
const double minimumY=static_cast<double>(minimumSubvoxels[1])*voxelMicrometres
|
||||
/Subvoxels-voxelMicrometres;
|
||||
const double maximumY=static_cast<double>(maximumSubvoxels[1])*voxelMicrometres
|
||||
/Subvoxels+voxelMicrometres;
|
||||
for(std::int32_t order=0;order+1<static_cast<std::int32_t>(ResolvedLayout->strates.size());
|
||||
++order)
|
||||
{
|
||||
const auto* upper=FindResolvedLayoutStratumByOrder(order);
|
||||
const auto* lower=FindResolvedLayoutStratumByOrder(order+1);
|
||||
if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))continue;
|
||||
const std::int64_t seam=upper->bedrock.bottomMicrometres;
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
if(passage.upperStrateIndex!=upper->strateIndex
|
||||
||passage.lowerStrateIndex!=lower->strateIndex)continue;
|
||||
const std::int64_t mouthRadius=std::max<std::int64_t>(0,
|
||||
passage.mouthRadiusSubvoxels*Owner->CoreVoxelSizeMicrons/Subvoxels);
|
||||
const std::int64_t protectedRadius=mouthRadius+SupportMarginMicrometres;
|
||||
const double fadeEnd=static_cast<double>(protectedRadius+FadeWidthMicrometres);
|
||||
for(const auto* mouth:{&passage.upperMouthSubvoxels,&passage.lowerMouthSubvoxels})
|
||||
{
|
||||
const std::int64_t mouthZ=mouth->at(2)*Owner->CoreVoxelSizeMicrons/Subvoxels;
|
||||
if(std::abs(mouthZ-seam)
|
||||
>upper->transitionWidthMicrometres/2+mouthRadius)continue;
|
||||
const double mouthX=static_cast<double>(mouth->at(0)
|
||||
*Owner->CoreVoxelSizeMicrons/Subvoxels);
|
||||
const double mouthY=static_cast<double>(mouth->at(1)
|
||||
*Owner->CoreVoxelSizeMicrons/Subvoxels);
|
||||
const double dx=mouthX<minimumX?minimumX-mouthX
|
||||
:mouthX>maximumX?mouthX-maximumX:0.0;
|
||||
const double dy=mouthY<minimumY?minimumY-mouthY
|
||||
:mouthY>maximumY?mouthY-maximumY:0.0;
|
||||
if(dx*dx+dy*dy<fadeEnd*fadeEnd)return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::int64_t ResolvedTransitionPassageWeightQ16(
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2& Upper,
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2& Lower,
|
||||
std::int64_t XMicrometres,std::int64_t YMicrometres) const
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().TransitionPassageFadeCalls;
|
||||
#endif
|
||||
constexpr std::int64_t Q16=1ll<<16;
|
||||
constexpr std::int64_t SupportMarginMicrometres=1000000;
|
||||
constexpr std::int64_t FadeWidthMicrometres=2000000;
|
||||
@@ -212,6 +359,9 @@ public:
|
||||
+SupportMarginMicrometres;
|
||||
for(const auto* mouth:{&passage.upperMouthSubvoxels,&passage.lowerMouthSubvoxels})
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().TransitionPassageMouthTests;
|
||||
#endif
|
||||
const std::int64_t mouthZMicrometres=mouth->at(2)*coreMicrometres
|
||||
/subvoxelsPerVoxel;
|
||||
if(std::abs(mouthZMicrometres-seam)
|
||||
@@ -236,6 +386,31 @@ public:
|
||||
|
||||
bool IsInsideResolvedStrateBoundaryBand(float Z) const
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().BoundaryBandCalls;
|
||||
#endif
|
||||
const auto* tileQueries=ActivePassageTileQueries;
|
||||
if(tileQueries&&tileQueries->matches(*this))
|
||||
{
|
||||
if(!tileQueries->mayContainResolvedBoundaryBand())
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().BoundaryTileCullRejects;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
if(Owner&&!tileQueries->mayContainResolvedBoundaryBandAtZ(
|
||||
static_cast<double>(Z)*Owner->CoreVoxelSizeMicrons))
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().BoundaryPointCullRejects;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
// This predicate only selects the scalar path for boundary-capable Z
|
||||
// samples. The merged intervals are conservative, so a positive is safe.
|
||||
return true;
|
||||
}
|
||||
if(!ResolvedLayout||!Owner||!Owner->Manager||!std::isfinite(Z))return false;
|
||||
const double worldZCentimetres=static_cast<double>(Z)*GetCoreVoxelSizeCm();
|
||||
const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast<float>(worldZCentimetres));
|
||||
@@ -248,6 +423,9 @@ public:
|
||||
const auto mayTouchTransition=[&](const VoxelForgeCore::ResolvedLayoutStratumV2* Upper,
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2* Lower)
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().TransitionWindowChecks;
|
||||
#endif
|
||||
if(!Upper||!Lower||!IsResolvedLayoutTransitionActive(*Upper))return false;
|
||||
const auto maxDisplacement=ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
||||
*Upper,*Lower);
|
||||
@@ -255,7 +433,11 @@ public:
|
||||
Lower->ceiling.bottomMicrometres)-maxDisplacement;
|
||||
const double maximum=std::max(Upper->bedrock.topMicrometres,
|
||||
Lower->ceiling.topMicrometres)+maxDisplacement;
|
||||
return zMicrometres>=minimum&&zMicrometres<maximum;
|
||||
const bool touches=zMicrometres>=minimum&&zMicrometres<maximum;
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
if(touches)++ThreadClassificationStats().TransitionWindowHits;
|
||||
#endif
|
||||
return touches;
|
||||
};
|
||||
const auto* lower=FindResolvedLayoutStratumByOrder(stratum->order+1);
|
||||
const auto* upper=stratum->order>0?FindResolvedLayoutStratumByOrder(stratum->order-1):nullptr;
|
||||
@@ -265,7 +447,29 @@ public:
|
||||
|
||||
float ApplyResolvedStrateBoundaryBand(float density,float X,float Y,float Z) const
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().BoundaryBandCalls;
|
||||
#endif
|
||||
(void)X;(void)Y;
|
||||
const auto* tileQueries=ActivePassageTileQueries;
|
||||
if(tileQueries&&tileQueries->matches(*this))
|
||||
{
|
||||
if(!tileQueries->mayContainResolvedBoundaryBand())
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().BoundaryTileCullRejects;
|
||||
#endif
|
||||
return density;
|
||||
}
|
||||
if(Owner&&!tileQueries->mayContainResolvedBoundaryBandAtZ(
|
||||
static_cast<double>(Z)*Owner->CoreVoxelSizeMicrons))
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().BoundaryPointCullRejects;
|
||||
#endif
|
||||
return density;
|
||||
}
|
||||
}
|
||||
if(!ResolvedLayout||!Owner||!Owner->Manager||!std::isfinite(Z))return density;
|
||||
const double worldZCentimetres=static_cast<double>(Z)*GetCoreVoxelSizeCm();
|
||||
const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast<float>(worldZCentimetres));
|
||||
@@ -280,6 +484,9 @@ public:
|
||||
const auto applyInterface=[&](const VoxelForgeCore::ResolvedLayoutStratumV2* upper,
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2* lower)
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().TransitionWindowChecks;
|
||||
#endif
|
||||
if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))return false;
|
||||
const auto maxDisplacement=ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
||||
*upper,*lower);
|
||||
@@ -288,6 +495,10 @@ public:
|
||||
const double maximum=std::max(upper->bedrock.topMicrometres,
|
||||
lower->ceiling.topMicrometres)+maxDisplacement;
|
||||
if(zMicrometres<minimum||zMicrometres>=maximum)return false;
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().TransitionWindowHits;
|
||||
++ThreadClassificationStats().TransitionEvaluatorCalls;
|
||||
#endif
|
||||
if(upper==stratum)bBedrockHandledByTransition=true;
|
||||
if(lower==stratum)bCeilingHandledByTransition=true;
|
||||
const std::int64_t xMicrometres=static_cast<std::int64_t>(std::llround(
|
||||
@@ -298,9 +509,15 @@ public:
|
||||
Owner->Seed,ResolvedLayout->strateLayoutChunkSizeMicrometres,
|
||||
xMicrometres,yMicrometres);
|
||||
auto resolvedBands=bands;
|
||||
const std::int64_t transitionWeight=ResolvedTransitionPassageWeightQ16(
|
||||
*upper,*lower,xMicrometres,yMicrometres);
|
||||
constexpr std::int64_t TransitionQ16=1ll<<16;
|
||||
const auto* tileQueries=ActivePassageTileQueries;
|
||||
const bool skipPassageFade=tileQueries&&tileQueries->matches(*this)
|
||||
&&!tileQueries->mayContainTransitionPassageFade();
|
||||
const std::int64_t transitionWeight=skipPassageFade?TransitionQ16
|
||||
:ResolvedTransitionPassageWeightQ16(*upper,*lower,xMicrometres,yMicrometres);
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
if(skipPassageFade)++ThreadClassificationStats().TransitionPassageTileSkips;
|
||||
#endif
|
||||
if(transitionWeight<TransitionQ16)
|
||||
{
|
||||
const std::int64_t seam=upper->bedrock.bottomMicrometres;
|
||||
@@ -325,9 +542,36 @@ public:
|
||||
&&zMicrometres<resolvedBands.upperBedrockTopMicrometres;
|
||||
const bool lowerCeiling=zMicrometres>=resolvedBands.lowerCeilingBottomMicrometres
|
||||
&&zMicrometres<resolvedBands.lowerCeilingTopMicrometres;
|
||||
if(!upperBedrock&&!lowerCeiling)return false;
|
||||
if(!upperBedrock&&!lowerCeiling)
|
||||
{
|
||||
// FloatingIslands can retain native solid density above a negatively
|
||||
// displaced Gradient face, creating a second surface instead of the
|
||||
// resolved transition top. Keep that air-side gap open in the owner field.
|
||||
const std::int64_t seam=upper->bedrock.bottomMicrometres;
|
||||
if(upper->generatorKind=="FloatingIslands"
|
||||
&&upper->transitionMode=="Gradient"
|
||||
&&resolvedBands.upperBedrockTopMicrometres<seam
|
||||
&&zMicrometres>=resolvedBands.upperBedrockTopMicrometres
|
||||
&&zMicrometres<seam)
|
||||
{
|
||||
const double distanceVoxels=static_cast<double>(zMicrometres
|
||||
-resolvedBands.upperBedrockTopMicrometres)
|
||||
/std::max<std::uint32_t>(1,Owner->CoreVoxelSizeMicrons);
|
||||
density=std::max(0.05f,static_cast<float>(distanceVoxels));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const auto& band=upperBedrock?upper->bedrock:lower->ceiling;
|
||||
density=-std::max(0.05f,static_cast<float>(band.solidDensityQ16)/65536.0f);
|
||||
if(upperBedrock&&upper->generatorKind=="FloatingIslands"
|
||||
&&upper->transitionMode=="Gradient")
|
||||
{
|
||||
const double distanceVoxels=static_cast<double>(
|
||||
resolvedBands.upperBedrockTopMicrometres-zMicrometres)
|
||||
/std::max<std::uint32_t>(1,Owner->CoreVoxelSizeMicrons);
|
||||
density=-std::max(0.05f,static_cast<float>(distanceVoxels));
|
||||
}
|
||||
else density=-std::max(0.05f,static_cast<float>(band.solidDensityQ16)/65536.0f);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -435,23 +679,49 @@ public:
|
||||
{
|
||||
if(!crossing.needsColumn||crossing.passageIndex!=passageIndex
|
||||
||qz>crossing.topZSubvoxels||qz<crossing.bottomZSubvoxels)continue;
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().ColumnZOverlapCalls;
|
||||
#endif
|
||||
double columnDistance;
|
||||
const bool tileProvesColumnExterior=passageTileScopeMatches
|
||||
&&!ActivePassageTileQueries->hasFeature(passageIndex,true);
|
||||
if(tileProvesColumnExterior)
|
||||
{
|
||||
if(composed<0.0f)continue;
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().ColumnGuardCalls;
|
||||
#endif
|
||||
if(composed<0.0f)
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().ColumnGuardNonpositiveSkips;
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
const double dx=qx-crossing.centerXSubvoxels;
|
||||
const double dy=qy-crossing.centerYSubvoxels;
|
||||
const double horizontalSquared=dx*dx+dy*dy;
|
||||
const double maximumRadius=static_cast<double>(crossing.maximumRadiusSubvoxels);
|
||||
const double exteriorLimit=maximumRadius
|
||||
+static_cast<double>(composed)*Q+1.0e-4;
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
if(horizontalSquared>exteriorLimit*exteriorLimit)
|
||||
{
|
||||
++ThreadClassificationStats().ColumnGuardRadialRejects;
|
||||
continue;
|
||||
}
|
||||
++ThreadClassificationStats().ColumnSignedDistanceCalls;
|
||||
#else
|
||||
if(horizontalSquared>exteriorLimit*exteriorLimit)continue;
|
||||
#endif
|
||||
columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
++ThreadClassificationStats().ColumnSignedDistanceCalls;
|
||||
#endif
|
||||
columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z);
|
||||
}
|
||||
composed=std::min(composed,static_cast<float>(columnDistance/Q));
|
||||
}
|
||||
const bool tilePassageFree=passageTileScopeMatches
|
||||
@@ -867,6 +1137,14 @@ public:
|
||||
uint64 SolidBoxes=0,AirBoxes=0,MixedBoxes=0,UnknownBoxes=0;
|
||||
uint64 NativeFloatFieldCalls=0,NativeLatticeFieldCalls=0,NativeUnclampedFieldCalls=0;
|
||||
double Milliseconds=0.0;
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
uint64 BoundaryBandCalls=0,TransitionWindowChecks=0,TransitionWindowHits=0;
|
||||
uint64 TransitionEvaluatorCalls=0,TransitionPassageFadeCalls=0,TransitionPassageMouthTests=0;
|
||||
uint64 BoundaryTileCullRejects=0,BoundaryPointCullRejects=0;
|
||||
uint64 ColumnZOverlapCalls=0,ColumnGuardCalls=0,ColumnGuardNonpositiveSkips=0;
|
||||
uint64 ColumnGuardRadialRejects=0,ColumnSignedDistanceCalls=0;
|
||||
uint64 TransitionPassageTileSkips=0;
|
||||
#endif
|
||||
};
|
||||
static void ResetThreadClassificationStats() { ThreadClassificationStats()={}; }
|
||||
static FClassificationThreadStats GetThreadClassificationStats()
|
||||
@@ -2793,6 +3071,7 @@ private:
|
||||
|
||||
void BuildPassageVoidCrossingPlan()
|
||||
{
|
||||
BuildResolvedBoundaryZIntervals();
|
||||
PassageVoidCrossings.clear();
|
||||
if(!Owner||!ResolvedLayout||ResolvedLayout->passages.empty())return;
|
||||
if(!Owner->PassageVoidCrossingCache)
|
||||
@@ -7231,6 +7510,7 @@ private:
|
||||
FIntVector NativeOnlyMaximum=FIntVector::ZeroValue;
|
||||
mutable std::atomic<uint32> NativeOnlyViolations{0};
|
||||
bool FarBuilt=false;
|
||||
std::vector<std::array<double,2>> ResolvedBoundaryZIntervalsMicrometres;
|
||||
std::vector<FInterval> LayerRanges;
|
||||
std::vector<FLayerField> LayerFields;
|
||||
std::vector<FPrimitive> Primitives;
|
||||
|
||||
@@ -40,6 +40,34 @@ namespace VoxelForgeCore
|
||||
|
||||
namespace
|
||||
{
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
void PrintL30CostCounters(const FStandaloneShapeWorld::FClassificationThreadStats& stats)
|
||||
{
|
||||
if(stats.BoundaryBandCalls==0&&stats.ColumnZOverlapCalls==0)return;
|
||||
std::printf("[L30CostTile] boundary_band_calls=%llu transition_window_checks=%llu "
|
||||
"transition_window_hits=%llu transition_evaluations=%llu passage_fade_calls=%llu "
|
||||
"passage_fade_mouth_tests=%llu column_z_overlap_calls=%llu column_guard_calls=%llu "
|
||||
"column_guard_nonpositive_skips=%llu column_guard_radial_rejects=%llu "
|
||||
"boundary_tile_cull_rejects=%llu boundary_point_cull_rejects=%llu "
|
||||
"transition_passage_tile_skips=%llu "
|
||||
"column_sdf_calls=%llu\n",
|
||||
static_cast<unsigned long long>(stats.BoundaryBandCalls),
|
||||
static_cast<unsigned long long>(stats.TransitionWindowChecks),
|
||||
static_cast<unsigned long long>(stats.TransitionWindowHits),
|
||||
static_cast<unsigned long long>(stats.TransitionEvaluatorCalls),
|
||||
static_cast<unsigned long long>(stats.TransitionPassageFadeCalls),
|
||||
static_cast<unsigned long long>(stats.TransitionPassageMouthTests),
|
||||
static_cast<unsigned long long>(stats.ColumnZOverlapCalls),
|
||||
static_cast<unsigned long long>(stats.ColumnGuardCalls),
|
||||
static_cast<unsigned long long>(stats.ColumnGuardNonpositiveSkips),
|
||||
static_cast<unsigned long long>(stats.ColumnGuardRadialRejects),
|
||||
static_cast<unsigned long long>(stats.BoundaryTileCullRejects),
|
||||
static_cast<unsigned long long>(stats.BoundaryPointCullRejects),
|
||||
static_cast<unsigned long long>(stats.TransitionPassageTileSkips),
|
||||
static_cast<unsigned long long>(stats.ColumnSignedDistanceCalls));
|
||||
}
|
||||
#endif
|
||||
|
||||
FIntVector ToNative(Vec3i value) { return {value.x,value.y,value.z}; }
|
||||
FVector ToNative(Vec3d value) { return {value.x,value.y,value.z}; }
|
||||
Vec3d FromNative(const FVector& value) { return {value.X,value.Y,value.Z}; }
|
||||
@@ -1995,6 +2023,9 @@ namespace VoxelForgeCore
|
||||
out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0,
|
||||
Counts.Milliseconds-classificationColumnComputeMilliseconds);
|
||||
out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds;
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
PrintL30CostCounters(Counts);
|
||||
#endif
|
||||
if(options.captureColumnCoordinates)
|
||||
{
|
||||
out.diagnostics.columnEvaluationWorkerHash=static_cast<std::uint64_t>(
|
||||
@@ -2356,6 +2387,9 @@ namespace VoxelForgeCore
|
||||
if(bHasWorldAssembly)
|
||||
{
|
||||
const auto Counts=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||
PrintL30CostCounters(Counts);
|
||||
#endif
|
||||
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
|
||||
out.diagnostics.fieldCalls=Columns.pointCalls;
|
||||
out.diagnostics.floatFieldCalls=Counts.NativeFloatFieldCalls;
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rasterize upward mesh triangles into a top-down field height map."""
|
||||
import argparse
|
||||
import array
|
||||
import json
|
||||
import math
|
||||
import struct
|
||||
import sys
|
||||
|
||||
Q16 = 1 << 16
|
||||
MASK64 = (1 << 64) - 1
|
||||
|
||||
|
||||
def trunc_div(numerator, denominator):
|
||||
return numerator // denominator if numerator >= 0 else -((-numerator) // denominator)
|
||||
|
||||
|
||||
def transition_hash(seed, interface, x, y, z, salt):
|
||||
value = 14695981039346656037
|
||||
for word in (seed & 0xffffffff, interface & 0xffffffff,
|
||||
x & MASK64, y & MASK64, z & MASK64, salt):
|
||||
for byte in range(8):
|
||||
value ^= (word >> (byte * 8)) & 0xff
|
||||
value = (value * 1099511628211) & MASK64
|
||||
value ^= value >> 30
|
||||
value = (value * 0xbf58476d1ce4e5b9) & MASK64
|
||||
value ^= value >> 27
|
||||
value = (value * 0x94d049bb133111eb) & MASK64
|
||||
value ^= value >> 31
|
||||
return ((value >> 32) >> 15) - Q16
|
||||
|
||||
|
||||
def transition_fade(value):
|
||||
value = max(0, min(Q16, value))
|
||||
square = value * value // Q16
|
||||
return square * (3 * Q16 - 2 * value) // Q16
|
||||
|
||||
|
||||
def gradient_height_um(layout, x_um, y_um):
|
||||
upper = next(item for item in layout["strates"] if item["order"] == 0)
|
||||
lower = next(item for item in layout["strates"] if item["order"] == 1)
|
||||
width = upper["transition_width_um"]
|
||||
cell = max(1_000_000, layout["strate_layout_chunk_size_um"] * 4)
|
||||
ix = x_um // cell
|
||||
iy = y_um // cell
|
||||
tx = transition_fade(((x_um - ix * cell) * Q16) // cell)
|
||||
ty = transition_fade(((y_um - iy * cell) * Q16) // cell)
|
||||
|
||||
def at(dx, dy):
|
||||
noise = transition_hash(0, upper["strate_index"], ix + dx, iy + dy, 0,
|
||||
0x47524144)
|
||||
return noise
|
||||
|
||||
def lerp(a, b, t):
|
||||
return a + trunc_div((b - a) * t, Q16)
|
||||
|
||||
low = lerp(lerp(at(0, 0), at(1, 0), tx),
|
||||
lerp(at(0, 1), at(1, 1), tx), ty)
|
||||
displacement = trunc_div(low * (width // 8), Q16)
|
||||
seam = upper["bedrock"]["bottom_um"]
|
||||
thickness = upper["bedrock"]["top_um"] - seam
|
||||
return seam + displacement + thickness
|
||||
|
||||
|
||||
def read_scene(path):
|
||||
triangles = []
|
||||
with open(path, "rb") as stream:
|
||||
if stream.read(8) != b"VFMSH001":
|
||||
raise SystemExit(f"{path}: invalid scene header")
|
||||
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
|
||||
stream.read(40)
|
||||
if version != 1 or count > 100000:
|
||||
raise SystemExit("unsupported scene")
|
||||
for _ in range(count):
|
||||
key = struct.unpack("<4i", stream.read(16))
|
||||
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||
"<5I", stream.read(20))
|
||||
positions = array.array("f")
|
||||
positions.frombytes(stream.read(vertex_count * 12))
|
||||
normals = array.array("f")
|
||||
normals.frombytes(stream.read(vertex_count * 12))
|
||||
indices = array.array("I")
|
||||
indices.frombytes(stream.read(index_count * 4))
|
||||
masks = stream.read(mask_count)
|
||||
if sys.byteorder != "little":
|
||||
positions.byteswap()
|
||||
normals.byteswap()
|
||||
indices.byteswap()
|
||||
for index in range(0, index_count, 3):
|
||||
if masks and masks[index // 3]:
|
||||
continue
|
||||
a, b, c = indices[index:index + 3]
|
||||
if max(a, b, c) >= surface_vertices:
|
||||
continue
|
||||
nz = (normals[a * 3 + 2] + normals[b * 3 + 2]
|
||||
+ normals[c * 3 + 2]) / 3.0
|
||||
if nz < 0.35:
|
||||
continue
|
||||
vertices = tuple((positions[v * 3], positions[v * 3 + 1],
|
||||
positions[v * 3 + 2]) for v in (a, b, c))
|
||||
triangles.append(vertices)
|
||||
return voxel_um, triangles
|
||||
|
||||
|
||||
def rasterize(triangles, bounds, width, height):
|
||||
x0, x1, y0, y1 = bounds
|
||||
values = [None] * (width * height)
|
||||
sx, sy = width / (x1 - x0), height / (y1 - y0)
|
||||
for tri in triangles:
|
||||
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = tri
|
||||
den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
|
||||
if abs(den) < 1.0e-12:
|
||||
continue
|
||||
left = max(0, int(math.floor((min(ax, bx, cx) - x0) * sx)))
|
||||
right = min(width - 1, int(math.ceil((max(ax, bx, cx) - x0) * sx)))
|
||||
top = max(0, int(math.floor((y1 - max(ay, by, cy)) * sy)))
|
||||
bottom = min(height - 1, int(math.ceil((y1 - min(ay, by, cy)) * sy)))
|
||||
for py in range(top, bottom + 1):
|
||||
y = y1 - (py + 0.5) / sy
|
||||
row = py * width
|
||||
for px in range(left, right + 1):
|
||||
x = x0 + (px + 0.5) / sx
|
||||
u = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den
|
||||
v = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den
|
||||
w = 1.0 - u - v
|
||||
if min(u, v, w) < -1.0e-6:
|
||||
continue
|
||||
z = u * az + v * bz + w * cz
|
||||
idx = row + px
|
||||
if values[idx] is None or z > values[idx]:
|
||||
values[idx] = z
|
||||
return values
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("scene")
|
||||
parser.add_argument("output_ppm")
|
||||
parser.add_argument("--bounds", nargs=4, type=float, required=True,
|
||||
metavar=("XMIN", "XMAX", "YMIN", "YMAX"))
|
||||
parser.add_argument("--size", nargs=2, type=int, default=(640, 640))
|
||||
parser.add_argument("--range", nargs=2, type=float, default=(-122.0, -118.0),
|
||||
metavar=("ZMIN", "ZMAX"))
|
||||
parser.add_argument("--ring-center", nargs=2, type=float,
|
||||
metavar=("X", "Y"))
|
||||
parser.add_argument("--layout")
|
||||
args = parser.parse_args()
|
||||
voxel_um, triangles = read_scene(args.scene)
|
||||
width, height = args.size
|
||||
sx, sy = width / (args.bounds[1] - args.bounds[0]), height / (args.bounds[3] - args.bounds[2])
|
||||
values = rasterize(triangles, args.bounds, width, height)
|
||||
finite = sorted(value for value in values if value is not None)
|
||||
if not finite:
|
||||
raise SystemExit("no upward-facing field samples in bounds")
|
||||
low, high = args.range
|
||||
if high <= low:
|
||||
raise SystemExit("height range must increase")
|
||||
stops = ((0.00, (31, 52, 92)), (0.24, (41, 119, 153)),
|
||||
(0.50, (85, 177, 143)), (0.73, (226, 207, 105)),
|
||||
(1.00, (181, 71, 49)))
|
||||
with open(args.output_ppm, "wb") as output:
|
||||
output.write(f"P6\n{width} {height}\n255\n".encode())
|
||||
for value in values:
|
||||
if value is None:
|
||||
output.write(bytes((18, 22, 30)))
|
||||
continue
|
||||
t = max(0.0, min(1.0, (value - low) / (high - low)))
|
||||
for index in range(len(stops) - 1):
|
||||
if t <= stops[index + 1][0]:
|
||||
ta, ca = stops[index]
|
||||
tb, cb = stops[index + 1]
|
||||
blend = (t - ta) / (tb - ta)
|
||||
rgb = tuple(round(ca[c] + blend * (cb[c] - ca[c]))
|
||||
for c in range(3))
|
||||
break
|
||||
else:
|
||||
rgb = stops[-1][1]
|
||||
output.write(bytes(rgb))
|
||||
def quantile(p):
|
||||
return finite[round((len(finite) - 1) * p)]
|
||||
print(f"[RingFieldRaster] voxel_um={voxel_um} triangles={len(triangles)} "
|
||||
f"pixels={len(finite)}/{len(values)} height_min_m={finite[0]:.6f} "
|
||||
f"height_p05_m={quantile(.05):.6f} height_median_m={quantile(.5):.6f} "
|
||||
f"height_p95_m={quantile(.95):.6f} height_max_m={finite[-1]:.6f} "
|
||||
f"height_range_m={finite[-1]-finite[0]:.6f} "
|
||||
f"map={args.output_ppm}")
|
||||
if args.ring_center:
|
||||
cx, cy = args.ring_center
|
||||
def at_world(x, y):
|
||||
fx = (x - args.bounds[0]) * sx - 0.5
|
||||
fy = (args.bounds[3] - y) * sy - 0.5
|
||||
ix, iy = math.floor(fx), math.floor(fy)
|
||||
tx, ty = fx - ix, fy - iy
|
||||
samples = []
|
||||
for px, py in ((ix, iy), (ix + 1, iy), (ix, iy + 1), (ix + 1, iy + 1)):
|
||||
if not (0 <= px < width and 0 <= py < height):
|
||||
return None
|
||||
value = values[py * width + px]
|
||||
if value is None:
|
||||
return None
|
||||
samples.append(value)
|
||||
return ((samples[0] * (1 - tx) + samples[1] * tx) * (1 - ty)
|
||||
+ (samples[2] * (1 - tx) + samples[3] * tx) * ty)
|
||||
print(f"[RingRadialProbe] center_m=({cx:.3f},{cy:.3f}) "
|
||||
"radius_m mean_m angular_sd_m min_m max_m samples")
|
||||
for radius in range(1, 17):
|
||||
row = [at_world(cx + radius * math.cos(a * math.tau / 32),
|
||||
cy + radius * math.sin(a * math.tau / 32))
|
||||
for a in range(32)]
|
||||
row = [value for value in row if value is not None]
|
||||
if row:
|
||||
mean = sum(row) / len(row)
|
||||
sd = math.sqrt(sum((value - mean) ** 2 for value in row) / len(row))
|
||||
print(f" {radius:>2} {mean:.6f} {sd:.6f} {min(row):.6f} "
|
||||
f"{max(row):.6f} {len(row)}")
|
||||
if args.layout:
|
||||
layout = json.load(open(args.layout, encoding="utf-8"))
|
||||
if next(item for item in layout["strates"] if item["order"] == 0)["transition_mode"] != "Gradient":
|
||||
raise SystemExit("--layout must describe a Gradient owner interface")
|
||||
errors = []
|
||||
for index, actual in enumerate(values):
|
||||
if actual is None:
|
||||
continue
|
||||
px, py = index % width, index // width
|
||||
x = args.bounds[0] + (px + 0.5) / sx
|
||||
y = args.bounds[3] - (py + 0.5) / sy
|
||||
expected = gradient_height_um(layout, round(x * 1_000_000),
|
||||
round(y * 1_000_000)) / 1_000_000.0
|
||||
errors.append(actual - expected)
|
||||
absolute = sorted(abs(error) for error in errors)
|
||||
rms = math.sqrt(sum(error * error for error in errors) / len(errors))
|
||||
print(f"[RingGradientMatch] samples={len(errors)} rms_m={rms:.6f} "
|
||||
f"p95_abs_m={absolute[round((len(absolute)-1)*.95)]:.6f} "
|
||||
f"max_abs_m={absolute[-1]:.6f} "
|
||||
"expected=owner-resolved-2d-hash-value-noise")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "CoreNamespacePreamble.h"
|
||||
#include "ResolvedLayoutInternal.h"
|
||||
|
||||
int main()
|
||||
{
|
||||
VoxelForgeCore::ResolvedLayoutStratumV2 upper, lower;
|
||||
upper.strateIndex=0;upper.order=0;upper.transitionMode="Gradient";
|
||||
upper.transitionWidthMicrometres=16000000;
|
||||
upper.bedrock.bottomMicrometres=-120000000;
|
||||
upper.bedrock.topMicrometres=-119500000;
|
||||
upper.bedrock.presentInCurrentField=true;
|
||||
lower.strateIndex=1;lower.order=1;
|
||||
lower.ceiling.bottomMicrometres=-126250000;
|
||||
lower.ceiling.topMicrometres=-120000000;
|
||||
lower.ceiling.presentInCurrentField=true;
|
||||
for(const auto& point:std::array<std::array<std::int64_t,2>,5>{
|
||||
std::array<std::int64_t,2>{64000000,-32000000},
|
||||
{65000000,-32000000},{64000000,-31000000},{70000000,-32000000},
|
||||
{80000000,-35000000}})
|
||||
{
|
||||
const auto bands=VoxelForgeCore::EvaluateResolvedLayoutTransitionBandsV1(
|
||||
upper,lower,0,8000000,point[0],point[1]);
|
||||
std::printf("x_m=%.3f y_m=%.3f top_m=%.6f bottom_m=%.6f\n",
|
||||
point[0]/1000000.0,point[1]/1000000.0,
|
||||
bands.upperBedrockTopMicrometres/1000000.0,
|
||||
bands.upperBedrockBottomMicrometres/1000000.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# VoxelForge L30
|
||||
|
||||
Work is on `assembler/world`. The speed and look patches are saved separately under
|
||||
`Saved/Linux/l30/`; evaluator version is now 16 for the field change. No Unreal files were edited.
|
||||
Nothing was committed, pushed, or stashed.
|
||||
|
||||
## Ring measurement and fix
|
||||
|
||||
I rasterized upward-facing triangles from the L29 mesh (skirt triangles excluded) over the same
|
||||
409,600 pixels as the after field. At `(64, -32)`, the L29 field stayed almost flat at `-120.063 m` from
|
||||
radii 1–11 m, then broke at 12 m. The expected Gradient at radius 1 m is `-121.360 m`. This is a
|
||||
field defect, not shading: the probe is about 67 m from the passage mouth, and the raster has no
|
||||
skirts or readable-shading tint. The passage column flare is at that distant mouth as well.
|
||||
|
||||
The upper FloatingIslands generator could leave native solid density in the air-side gap above a
|
||||
negatively displaced Gradient face. The constant band densities also snapped the face to Core
|
||||
voxel levels. The owner Gradient path now clears that gap with positive signed distance and writes
|
||||
negative signed distance below the face, allowing the mesh to interpolate its height. The after
|
||||
profile follows the resolved hash surface at every sampled radius. Match error fell from RMS
|
||||
`0.361765 m`, p95 `1.016178 m`, max `1.304169 m` to RMS `0.110357 m`, p95 `0.218531 m`, max
|
||||
`0.248818 m` (one Core voxel is `0.25 m`).
|
||||
|
||||
Before/after captures use the same color ramp and bounds:
|
||||
|
||||
- [L29 height map](captures/fi-floor-gradient-before-heightmap.jpg), [fixed height map](captures/fi-floor-gradient-after-heightmap.jpg)
|
||||
- [L29 readable render](captures/fi-floor-gradient-before.jpg), [fixed readable render](captures/fi-floor-gradient-after.jpg)
|
||||
|
||||
## Transition captures
|
||||
|
||||
The maps cover the full 640×640 surface and are all below 300 KB. Gradient is the owner world
|
||||
without a mode override. Hard and Interleaved use fixture overrides; the resolved layouts record
|
||||
those modes.
|
||||
|
||||
- [Hard](captures/transition-hard-heightmap.jpg) — flat interface.
|
||||
- [Gradient](captures/transition-gradient-heightmap.jpg) — owner field.
|
||||
- [Interleaved](captures/transition-interleaved-heightmap.jpg) — fixture override.
|
||||
|
||||
## Cost
|
||||
|
||||
Five alternating W12 pairs, hashing off and `cache_hits=0`. First table is the separable speed-only
|
||||
build, whose hashes remain L29-identical. Second table is the final version-16 build.
|
||||
|
||||
| Recipe | L24 | L29 | L30 speed-only | vs L24 | L30 final | vs L24 |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| SurfaceWorld | 1.566 s | 1.653 s | 1.606 s | +2.55% | 1.530 s | +2.68% |
|
||||
| FloatingIslands | 16.625 s | 16.779 s | 16.470 s | −0.93% | 16.543 s | +0.21% |
|
||||
|
||||
Counter-enabled runs on the same plain W12 workload locate the saved work:
|
||||
|
||||
| Recipe | Boundary calls | Window checks → evaluations | Fade calls | Boundary tile / point culls | Column Z tests → guards → radial rejects | SDF calls |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| SurfaceWorld | 19.95 M | 3.54 M → 739 k | 8.2 k | 8.99 M / 8.96 M | 1.87 M → 1.85 M → 1.58 M | 18.4 k |
|
||||
| FloatingIslands | 66.33 M | 8.00 M → 2.68 M | 8.6 k | 38.25 M / 23.63 M | 8.17 M → 8.14 M → 6.82 M | 83.4 k |
|
||||
|
||||
The fade-tile cull skips 731 k SurfaceWorld and 2.67 M FloatingIslands fade evaluations; the
|
||||
column guard exits on nonpositive field values 272 k and 1.27 M times. Timing comes from
|
||||
uninstrumented binaries; the counts above use counter logging.
|
||||
|
||||
## Hashes and audits
|
||||
|
||||
The speed-only v15 build matches all 12 L29 hash records for each recipe/compiler/worker matrix
|
||||
(W1/W8/W12), and all 12 records for each 24 m crossing matrix. That is 24 exact comparisons;
|
||||
all pass. The final v16 build is deterministic across both compilers and all worker counts, in the
|
||||
normal and 24 m crossing runs. Representative W12 hashes (`density / mesh / payload / topology`):
|
||||
|
||||
| Recipe | Full detail | Whole view |
|
||||
|---|---|---|
|
||||
| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` |
|
||||
| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `4ee19ea6b6406ca6 / be648f94c899b47e / 310b525f31604f98 / 10366eb6dfce2384` |
|
||||
|
||||
Exact-query hashes also match across v16 runs: SurfaceWorld
|
||||
`c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849`; FloatingIslands
|
||||
`7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf`.
|
||||
|
||||
- Interface audit: all 12 compiler/recipe/mode combinations pass; 4,221 columns each, zero holes
|
||||
and air samples, both negative controls reject.
|
||||
- The 16 L29 fixture-matrix replays pass passage safety/open-air, validator controls (10/10), L13
|
||||
route parity, L9 proof checks (zero violations), and walkability. `FloorProfileBlend` still
|
||||
reports 2.544 vox in style 1 and 3.645 vox in style 3, unchanged from L29.
|
||||
- Audit scope caveat: the L29 files named `floating-islands` were run with
|
||||
`--recipe=surface-world`. With the actual `--recipe=floating-islands` option, the rock-column
|
||||
audit fails on all four styles on both L29 and L30: connected ends are 1/2, 3/4, 1/2, 3/4.
|
||||
The open-air audit passes. This is inherited, but means the L29 report's “two recipes” matrix did
|
||||
not exercise the FloatingIslands CLI recipe.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- Broad air-gap experiments first lost a SurfaceWorld style-2 support sample. Route/radius masks
|
||||
and support restoration then caused open-air violations. Widening the fade restored that case,
|
||||
but measurement placed the ring far outside the fade, so those variants were discarded. The
|
||||
final owner-only gate and signed-distance face pass the targeted support audit.
|
||||
- Early speed variants missed the target: tile-only `+4.04%`, Z-point `+3.11%`, scalar path
|
||||
`+4.33%` vs L24. The final band and fade-tile culls meet the target.
|
||||
- The first counter-instrumentation compile failed; the counter hook was corrected and rebuilt
|
||||
with Clang and GCC. One duplicated timing sequence was discarded; only the clean five-pair runs
|
||||
are reported.
|
||||
- The first 24 m crossing comparison supplied an extra camera override and mismatched. Rerunning
|
||||
with the L29 camera setup and only `--camera-step=24,0,0` matches all hashes.
|
||||
- An early Hard export hit `std::bad_alloc`; retry passed. The first EGL render used an unwritable
|
||||
cache; retry with `XDG_CACHE_HOME` under `Saved/Linux/l30` passed. The first map conversion also
|
||||
stopped before the Hard/Interleaved maps; both were rerun individually and have full raster
|
||||
coverage.
|
||||
- The first passage-matrix summary script assumed a different marker order and reported failure;
|
||||
checking each audit marker by name confirms the core matrix passes. A `check=True` true-recipe
|
||||
control runner stopped on the expected L29 exit code 10; rerunning while recording return codes
|
||||
confirmed the same four inherited rock-column failures on L29 and L30.
|
||||
|
||||
No build or run exceeded 30 minutes or remained stuck for 15 minutes. Builds and runs used CPUs
|
||||
0–11, at most 12 workers, normal priority, and the 6 GiB address-space cap. No Unreal build was
|
||||
attempted.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Merged: the ring was a real field defect (air-side gap left solid, face snapped to voxel levels), now fixed
|
||||
(RMS 0.36 → 0.11 m). Cost back to SW +2.7% / FI +0.2% vs L24. The speed part is bit-identical; final v16 is
|
||||
deterministic.
|
||||
- Still visible: `fi-floor-gradient-after.jpg` shows faint concentric lines. Unknown whether that is residual
|
||||
geometry (≤0.25 m steps) or the readable shader's height-tint banding; L31 measures it.
|
||||
- **Important finding (thanks to Luna's caveat):** L29's "both recipes" passage matrix ran SurfaceWorld twice. On
|
||||
the real `--recipe=floating-islands`, the rock-column connected-ends audit fails in all 4 styles (1/2 or 3/4).
|
||||
Since L27, FI column results were never really tested. L31 priority.
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Interleave five W12 hash-off whole-view cost triplets for L24/L29/L30."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l30"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
BINARIES = {
|
||||
"l24": ROOT / "Saved/Linux/l24-source/bin/linux/VoxelForgeCoreHeadless-linux24-clang19",
|
||||
"l29": ROOT / "Saved/Linux/bin/VoxelForgeCoreHeadless-linux29-clang",
|
||||
"l30": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
|
||||
}
|
||||
ORDERS = (
|
||||
("l24", "l29", "l30"),
|
||||
("l29", "l30", "l24"),
|
||||
("l30", "l24", "l29"),
|
||||
("l24", "l30", "l29"),
|
||||
("l29", "l24", "l30"),
|
||||
)
|
||||
WHOLE_READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||
rows = []
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for pair, order in enumerate(ORDERS, 1):
|
||||
for build in order:
|
||||
name = f"cost-speedfade-{recipe}-pair{pair}-{build}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"prlimit", "--as=6442450944", "--", str(BINARIES[build]),
|
||||
str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||
check=True, timeout=1800)
|
||||
elapsed = time.monotonic() - started
|
||||
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
matches = WHOLE_READY.findall(text)
|
||||
if len(matches) != 1 or "hash=off" not in text or "cache_hits=0" not in text:
|
||||
raise SystemExit(f"invalid cost run: {name}; see {logfile}")
|
||||
ready = float(matches[0])
|
||||
rows.append((recipe, pair, build, ready, elapsed))
|
||||
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||
f"process_elapsed={elapsed:.3f}s", flush=True)
|
||||
|
||||
summary = OUT / "cost-runs-speedfade.csv"
|
||||
summary.write_text("recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds\n" +
|
||||
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.6f}\n"
|
||||
for recipe, pair, build, ready, elapsed in rows), encoding="utf-8")
|
||||
print(f"wrote {summary}", flush=True)
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify speed-only L30 hashes on the 24 m crossing against L29."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l30"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
BINARIES = {
|
||||
"clang": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
|
||||
"gcc": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade-gcc",
|
||||
}
|
||||
PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
passed = True
|
||||
for compiler, binary in BINARIES.items():
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
reference_path = ROOT / f"Saved/Linux/linux29-final-crossing24-{compiler}-{recipe}.log"
|
||||
reference = PATTERN.findall(reference_path.read_text(encoding="utf-8"))
|
||||
for workers in (1, 8, 12):
|
||||
name = f"crossing-speedonly-default-{compiler}-{recipe}-w{workers}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"prlimit", "--as=6442450944", "--", str(binary),
|
||||
str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--stream-hash", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", f"--workers={workers}",
|
||||
"--camera-step=24,0,0",
|
||||
"--region-crossing", f"--recipe={recipe}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||
check=True, timeout=1800)
|
||||
elapsed = time.monotonic() - started
|
||||
rows = PATTERN.findall(logfile.read_text(encoding="utf-8", errors="replace"))
|
||||
ok = len(reference) == 12 and rows == reference
|
||||
passed &= ok
|
||||
print(f"{compiler} {recipe} W{workers} elapsed={elapsed:.3f}s "
|
||||
f"hash_rows={len(rows)} {'MATCH' if ok else 'MISMATCH'}", flush=True)
|
||||
print(f"[CrossingSpeedOnly] runs=12 status={'PASS' if passed else 'FAIL'}", flush=True)
|
||||
raise SystemExit(0 if passed else 1)
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 23 KiB |