From 103d8b471ecdc4ae6ae3e6369b1818398980693a Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Wed, 21 Oct 2026 11:40:27 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX --- .../include/VoxelForgeCore/VoxelForgeCore.h | 4 +- Standalone/Core/src/ShapeWorldInternal.h | 292 +++++++++++++++++- Standalone/Core/src/VoxelForgeCore.cpp | 34 ++ .../WorldAssemblerM3/Linux30/MeasureRing.py | 239 ++++++++++++++ .../Linux30/ProbeTransitionNoise.cpp | 28 ++ .../WorldAssemblerM3/Linux30/REPORT.md | 121 ++++++++ .../WorldAssemblerM3/Linux30/RunCost.py | 55 ++++ .../WorldAssemblerM3/Linux30/RunCrossing30.py | 44 +++ .../fi-floor-gradient-after-heightmap.jpg | Bin 0 -> 13838 bytes .../captures/fi-floor-gradient-after.jpg | Bin 0 -> 19073 bytes .../fi-floor-gradient-before-heightmap.jpg | Bin 0 -> 12470 bytes .../captures/fi-floor-gradient-before.jpg | Bin 0 -> 19598 bytes .../transition-gradient-heightmap.jpg | Bin 0 -> 13838 bytes .../captures/transition-hard-heightmap.jpg | Bin 0 -> 5004 bytes .../transition-interleaved-heightmap.jpg | Bin 0 -> 23118 bytes 15 files changed, 809 insertions(+), 8 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/MeasureRing.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/ProbeTransitionNoise.cpp create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/RunCost.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/RunCrossing30.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after-heightmap.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-before-heightmap.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-before.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-gradient-heightmap.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-hard-heightmap.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-interleaved-heightmap.jpg diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 4e49fa6..f928630 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -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 { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 581d7e0..7536a32 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -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 passageIndexResolvedBoundaryBandMayAffectAtZMicrometres(zMicrometres); + } std::size_t featureCount() const{return featureIndices_.size();} private: @@ -100,6 +111,8 @@ public: std::vector featureIndices_; std::vector 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( + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + const double queryMinimumMicrometres=static_cast(minimumZSubvoxels) + *Owner->CoreVoxelSizeMicrons/Subvoxels; + const double queryMaximumMicrometres=static_cast(maximumZSubvoxels) + *Owner->CoreVoxelSizeMicrons/Subvoxels; + const double sampleMarginMicrometres=static_cast(Owner->CoreVoxelSizeMicrons); + for(const auto& interval:ResolvedBoundaryZIntervalsMicrometres) + { + if(queryMaximumMicrometres+sampleMarginMicrometres>=interval[0]-1.0 + &&queryMinimumMicrometres-sampleMarginMicrometresat(1); + } + + void BuildResolvedBoundaryZIntervals() + { + ResolvedBoundaryZIntervalsMicrometres.clear(); + if(!ResolvedLayout)return; + for(const auto& stratum:ResolvedLayout->strates) + { + if(stratum.ceiling.presentInCurrentField + &&stratum.ceiling.bottomMicrometres(stratum.ceiling.bottomMicrometres), + static_cast(stratum.ceiling.topMicrometres)}); + if(stratum.bedrock.presentInCurrentField + &&stratum.bedrock.bottomMicrometres(stratum.bedrock.bottomMicrometres), + static_cast(stratum.bedrock.topMicrometres)}); + } + for(std::int32_t order=0;order+1(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(std::min(upper->bedrock.bottomMicrometres, + lower->ceiling.bottomMicrometres)-displacement), + static_cast(std::max(upper->bedrock.topMicrometres, + lower->ceiling.topMicrometres)+displacement)}); + } + std::sort(ResolvedBoundaryZIntervalsMicrometres.begin(), + ResolvedBoundaryZIntervalsMicrometres.end(), + [](const auto& a,const auto& b){return a[0]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& minimumSubvoxels, + const std::array& 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(Owner->CoreVoxelSizeMicrons); + const double minimumX=static_cast(minimumSubvoxels[0])*voxelMicrometres + /Subvoxels-voxelMicrometres; + const double maximumX=static_cast(maximumSubvoxels[0])*voxelMicrometres + /Subvoxels+voxelMicrometres; + const double minimumY=static_cast(minimumSubvoxels[1])*voxelMicrometres + /Subvoxels-voxelMicrometres; + const double maximumY=static_cast(maximumSubvoxels[1])*voxelMicrometres + /Subvoxels+voxelMicrometres; + for(std::int32_t order=0;order+1(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(0, + passage.mouthRadiusSubvoxels*Owner->CoreVoxelSizeMicrons/Subvoxels); + const std::int64_t protectedRadius=mouthRadius+SupportMarginMicrometres; + const double fadeEnd=static_cast(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(mouth->at(0) + *Owner->CoreVoxelSizeMicrons/Subvoxels); + const double mouthY=static_cast(mouth->at(1) + *Owner->CoreVoxelSizeMicrons/Subvoxels); + const double dx=mouthXmaximumX?mouthX-maximumX:0.0; + const double dy=mouthYmaximumY?mouthY-maximumY:0.0; + if(dx*dx+dy*dyat(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(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(Z)*GetCoreVoxelSizeCm(); const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast(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=minimum&&zMicrometresorder+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(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(Z)*GetCoreVoxelSizeCm(); const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast(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=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::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(transitionWeightbedrock.bottomMicrometres; @@ -325,9 +542,36 @@ public: &&zMicrometres=resolvedBands.lowerCeilingBottomMicrometres &&zMicrometresbedrock.bottomMicrometres; + if(upper->generatorKind=="FloatingIslands" + &&upper->transitionMode=="Gradient" + &&resolvedBands.upperBedrockTopMicrometres=resolvedBands.upperBedrockTopMicrometres + &&zMicrometres(zMicrometres + -resolvedBands.upperBedrockTopMicrometres) + /std::max(1,Owner->CoreVoxelSizeMicrons); + density=std::max(0.05f,static_cast(distanceVoxels)); + return true; + } + return false; + } const auto& band=upperBedrock?upper->bedrock:lower->ceiling; - density=-std::max(0.05f,static_cast(band.solidDensityQ16)/65536.0f); + if(upperBedrock&&upper->generatorKind=="FloatingIslands" + &&upper->transitionMode=="Gradient") + { + const double distanceVoxels=static_cast( + resolvedBands.upperBedrockTopMicrometres-zMicrometres) + /std::max(1,Owner->CoreVoxelSizeMicrons); + density=-std::max(0.05f,static_cast(distanceVoxels)); + } + else density=-std::max(0.05f,static_cast(band.solidDensityQ16)/65536.0f); return true; }; @@ -435,23 +679,49 @@ public: { if(!crossing.needsColumn||crossing.passageIndex!=passageIndex ||qz>crossing.topZSubvoxels||qzhasFeature(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(crossing.maximumRadiusSubvoxels); const double exteriorLimit=maximumRadius +static_cast(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(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 NativeOnlyViolations{0}; bool FarBuilt=false; + std::vector> ResolvedBoundaryZIntervalsMicrometres; std::vector LayerRanges; std::vector LayerFields; std::vector Primitives; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index a0a00c1..c9408c6 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -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(stats.BoundaryBandCalls), + static_cast(stats.TransitionWindowChecks), + static_cast(stats.TransitionWindowHits), + static_cast(stats.TransitionEvaluatorCalls), + static_cast(stats.TransitionPassageFadeCalls), + static_cast(stats.TransitionPassageMouthTests), + static_cast(stats.ColumnZOverlapCalls), + static_cast(stats.ColumnGuardCalls), + static_cast(stats.ColumnGuardNonpositiveSkips), + static_cast(stats.ColumnGuardRadialRejects), + static_cast(stats.BoundaryTileCullRejects), + static_cast(stats.BoundaryPointCullRejects), + static_cast(stats.TransitionPassageTileSkips), + static_cast(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( @@ -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; diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/MeasureRing.py b/Standalone/Research/WorldAssemblerM3/Linux30/MeasureRing.py new file mode 100644 index 0000000..9a41a07 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux30/MeasureRing.py @@ -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() diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/ProbeTransitionNoise.cpp b/Standalone/Research/WorldAssemblerM3/Linux30/ProbeTransitionNoise.cpp new file mode 100644 index 0000000..087bb9a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux30/ProbeTransitionNoise.cpp @@ -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,5>{ + std::array{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); + } +} diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux30/REPORT.md new file mode 100644 index 0000000..ae1106c --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux30/REPORT.md @@ -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. diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/RunCost.py b/Standalone/Research/WorldAssemblerM3/Linux30/RunCost.py new file mode 100644 index 0000000..6f596e7 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux30/RunCost.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/RunCrossing30.py b/Standalone/Research/WorldAssemblerM3/Linux30/RunCrossing30.py new file mode 100644 index 0000000..8659973 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux30/RunCrossing30.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after-heightmap.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after-heightmap.jpg new file mode 100644 index 0000000000000000000000000000000000000000..45284b5093f1648a3bbaba8b7ef8717edc2d43b1 GIT binary patch literal 13838 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 SYs6dO?FRT1oYCi7@BaZ9vOw7Y literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4d2059cac407aceb70e56c4a6ca90aa499deef5a GIT binary patch literal 19073 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

IoSh=1 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 a4P2#eI-5CPmC@_gg-U9~zSN1>==vWHU9ckn literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-before.jpg new file mode 100644 index 0000000000000000000000000000000000000000..65bebf647822e2be337d6d5e9944a57855f1442b GIT binary patch literal 19598 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 Mtd~6tRF@n72Rf^hy#N3J literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-gradient-heightmap.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-gradient-heightmap.jpg new file mode 100644 index 0000000000000000000000000000000000000000..45284b5093f1648a3bbaba8b7ef8717edc2d43b1 GIT binary patch literal 13838 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 SYs6dO?FRT1oYCi7@BaZ9vOw7Y literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-hard-heightmap.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-hard-heightmap.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4c98a9058ae86ae400d45ecb96cd60cc4de28c3e GIT binary patch literal 5004 zcmeIuIS#@w6a>(*jj~wIVoNDfaf1jqNK_QGDYydS4%9gs1>y`WcfmMNC0u}FruhDU z3KuWj!V7hOI33r%^PA4EJi-$@WJ!`HY4%B;=UVGH$0x=Z^IgWx6lGBqah6q8IcIIt zG;O;K7xWOAMZ`?2MN-ci!2UGh44cV84oWyk2jvhRFiJpLWwC|6?x+9qq5uUbKmiI+ ZfC3bt00k&O0SZun0u-RYKMADa{szg>7WM!D literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-interleaved-heightmap.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-interleaved-heightmap.jpg new file mode 100644 index 0000000000000000000000000000000000000000..13e9af1626f3723ba04cf61106dd335eb3c0ea76 GIT binary patch literal 23118 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�jDnzlH`?`XK_rHTKof2g!Q5s%0193M@ z1AC}+#;K6WBhb?VyzEz8cC0)oe_{0b4sG;di?AEcAII$v0foMSU`#7GWLDX$1qga0 zwnbPt*^XC4@2Mldqh6Ui9eFER9QG>lg6Wvq8i4O?U(e|8P+tt_3|TL(X>r@-QKitW zcke2Ry;bO6a>m9H(5l79J?FPO00Jlh61X3!Wlp1`1a=rz z0%D;yQnLB6)hco5yn{ooxG4}vYDR_^5gU&A%itJr?aOe#sr^CuH=;0&gG^R-xdd*VL4T{w;8AO3y6k9GFuH>!rs_!fW%iqaS~EW7yS_ zyW;&ga$m9^#f&gANlvwQMMfr+5?MjxYkU&z2g%<0ZGm!KhM8O)Yh{ zjKO##TP&8cgk0J2ZYF7=WG@o%gubSF*VFcCg9#i`Hq+EXj%{*p>;+STMI7mc;_2b= zAdBYI-F8JXwnan_)#CE`WA`X&%1>Du8G5$)Ue(>Q7t9ZDIvl9xnDylShk#9|4>H%3 zSR3p(Cij|FSa)h}b%RaBjD?`~CrP<_iGb{&lbcdp)$sOOq)bSz<@C#98eZZWp~XQ#p4X&jt1XrO_K z%xzr{mc<)!F(CkLNIwm- zKm530oeV(_Xpp?E=R$|vH$Td$UTcPCfG%kJNFGv-VRl((1XUT&{WLh1&78HK*wYa6 zmKbx}RX0>R`^>@_!#XVu9y!U!>QWLF=LOfAbLpwWXuwxQHOp5RdzBN>-xTrlL-M?e zf@Sqnki|`B^aYg)vN(0xDiI=!@yUTn3lD0DqFgtbL7gA4XpLiblT(0oBboOoX z#5bunoy9ZKThlEP11e`a8D|5$1|*2DW|vEZ*4$XGrReR!qS}HT<6|~8@whe9(EPzt zZI-@}Y_a7A3MAF5bHCCPASeZXPUhG&7&xEXkTP#*r$BVs$`7Z^_yZsA_amsND!P%4 z9b#cRHRH1B9hmab5A{^fr}%QSg(3n^@UqXpmJEpqHfH|Nc$tBwkw(zcaV@l8Eg{2Q zbC5sOV(6kPL=ZkNEu_ zKsmpqgMq;kdQ_cWB*^Zbld2Xx1GD7=Gav#@L;1Xv`^Dv0Z%P1<2~q5xZp2mV=YvFC z!)=>)lBVQoizPLqZ%59#8LvTrg<@dT<+C5^GqGI>no!SsEOSPUMMPL-%(-DB`!=to z$prT86Ux*!FG!*?DoJ|8!y|8?pKaI46bd4f@y_=mhi%%GJs@3}P!P2(liL?%yekuX zKAfIwW@8`k!{S5(yLhPnpG&vdX|3P|Ob=8mG!n0&P3!}V>ou*v7+1+R#+751N6g^N zcoevpQA$6RGV_v{x@Sv8xPDAX45JEctca{%tD|xihSY}joLGMHB3N81N}1eaH$o(Y zM@*K_EZpOKt#uNUUQ!#%(a$N+rl%WL+wa&QW$hh#SP^DrzwO1+UM%y5z>mQt^qjr@ zaMS@}hzJ93k&Um-tihHKM?)jtQUTaOoPXSvB~_60LQ@QTLde8%u(1b_*}S3Zx82Ym z)lKB+R72j|v?{tNT_dd7$I?&i&5W%AB77y%k7u-}x~wzNby~g0iwOl3cRw2c3(U*C1Gi%a&9Gi|9VyjVq`hr-rgbdt1O`4V6v}v0Ch78?93s|Q zb96-E9W_Xx8}u@jeEJwzEtLjx&rN23f%#UBO__c^3YhXu4l-X9v1>xrDsj%3KFq54 znWxsCSD)H|y^ze|_asd9w=$c!7S*l=$})s@pnZf_3U`3{S~G1Naptr3@G(^;UX%f9 zrh_PcnkvBE9~RnReKB0%liS{l+Rd*Nwq4MZm}JJH+o8qRYw&665N{9x`MHTz*ulH-yE2YD4q z#9WY9QQ?z9w-yxgro=~z1z?M|UDi>iMjG?I5~uf+(aEoSFoX_`glYE1UTn-8kwN7k zYFex-!^TYhk!;TWR8VAl%39sk@O@r)S~-ArCS#YrF~V}l#@rhUS4vXOi1kqVsQ81*L^xbHa1|m+4lA9 zVu5g2%9^;Xe#*oq$S_h(;e{x%g=#|7F9&~dH(mzXR+ zR`4|q1JV$f3@|jQZKFFS3XQ>JK+>9vFz_k^h!R3>CU}6SQr#*!qZg4RyAIUYKt=_1 zi5T4YZdwl3bnJvM=(`Fl7MGPuq*ss>N-j_AJCFBDNC>Fx`2uUmpkZ-4^)+EI2xf#I z`s_dh5{kp#i51sWc!Gx5vieYgto$N`+JuoaP=tq44E;DAq|QwF1XTW|^YtoH;ZP_0 zPU|0E&xd_+Mjb_duYi%2Ob%#(J(bc;!K|IY`QBO-NJP22I~GHcC2C67j+^oOdgb1s zY`sn^%W+C0n#+HcA~<7D1bXYpQcvT@tv>H7AR7xR0N_+feG#?kKCI{KhQEIc>KBBe;OmU(XxW4I1}1Q9l9wxPp3IGv@5AxEg2k@br5{w^Qlh7qERLnMs#FWVPr_ zl?BNCpE^2wf@t-jx&23y458h=E2@MrvW!}{9w=?D@QB8-=NmoxJJSq)y}tJmIE?p$ z%jRppMp6e=s$gAGAdkCp;che4&bPN~nBQCJ{mFcOuifw6Mgn zVdxua1>MiTmz59V?S4v!Kh=9cC@Ajeu08=wcu>9zuIbw;=0d?nz-sL_a<703(si!k z#VZaQZ4YHu76g^`?ZYW2bv}2^O#Rl_6h~q0YM{`}ispz`3?me-sz@}e-82q8e;90s zNPrR%+OmU32p$4V=He!233$)$7C(Xg1GR5=;D?sJg*#I5>dCQmoKt?{ensE1ivQ6O aE-X4mTRpQXYy4(E2>2*0IGECx-v0naYh>vF literal 0 HcmV?d00001