diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 204242d..edeb226 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -892,6 +892,7 @@ namespace VoxelForgeCore inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2; inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3; inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV4 = 4; + inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV5 = 5; inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1; inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1; enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict }; @@ -918,6 +919,9 @@ namespace VoxelForgeCore std::array anchorPositionMicrometres{}; // Present only for fillSweptDeckSegment; defines its directed centerline. std::array, 2> sweepEndpointsMicrometres{}; + // V5 permits one continuous swept deck to carry its complete sampled centerline. + // Empty retains the V4 single-segment interpretation. + std::vector> sweepPathMicrometres; std::int64_t placementRadiusMicrometres = 0; std::array halfExtentsMicrometres{}; std::int64_t smoothWidthMicrometres = 0; @@ -938,6 +942,7 @@ namespace VoxelForgeCore ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid; std::array positionMicrometres{}; std::array, 2> sweepEndpointsMicrometres{}; + std::vector> sweepPathMicrometres; std::array boundsMinimumMicrometres{}; std::array boundsMaximumMicrometres{}; std::array halfExtentsMicrometres{}; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 5074335..4a47b04 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -599,6 +599,7 @@ namespace <<",\"irregularity_seed\":"<(stamp.yawQuarterTurns); if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment) + { out<<",\"sweep_endpoints_um\":[[" <=ResolvedLayoutStampVocabularyVersionV3) out<<",\"rounding_radius_um\":"< stampIds; for(const auto& stamp:stamps) { @@ -1184,9 +1199,30 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, -stamp.sweepEndpointsMicrometres[0][axis]; lengthSquared+=delta*delta; } - if(!(lengthSquared>0.0) - ||std::sqrt(lengthSquared)>2.0*stamp.halfExtentsMicrometres[0]+1.0) + if(stamp.sweepPathMicrometres.empty()&&(!(lengthSquared>0.0) + ||std::sqrt(lengthSquared)>2.0*stamp.halfExtentsMicrometres[0]+1.0)) add("stamp_sweep",stamp.stableId+" has invalid endpoints or longitudinal extent"); + if(!stamp.sweepPathMicrometres.empty()) + { + bool validPath=stampVocabularyVersion>=ResolvedLayoutStampVocabularyVersionV5 + &&stamp.sweepPathMicrometres.size()>=2 + &&stamp.sweepPathMicrometres.size()<=1025 + &&stamp.sweepPathMicrometres.front()==stamp.sweepEndpointsMicrometres[0] + &&stamp.sweepPathMicrometres.back()==stamp.sweepEndpointsMicrometres[1]; + for(std::size_t pointIndex=0;pointIndex=-1000000000000ll + &&coordinate<=1000000000000ll; + if(pointIndex>0) + validPath=validPath + &&stamp.sweepPathMicrometres[pointIndex-1][0](stamp.halfExtentsMicrometres[axis]+pad); + radiusSquared+=expanded*expanded; + } + const auto radius=static_cast(std::ceil(std::sqrt(radiusSquared))); + std::array minimum=stamp.sweepPathMicrometres.front(); + std::array maximum=minimum; + for(const auto& point:stamp.sweepPathMicrometres) + for(std::size_t axis=0;axis<3;++axis) + { + minimum[axis]=std::min(minimum[axis],point[axis]); + maximum[axis]=std::max(maximum[axis],point[axis]); + } + for(std::size_t axis=0;axis<3;++axis) + if(stamp.boundsMinimumMicrometres[axis]!=minimum[axis]-radius + ||stamp.boundsMaximumMicrometres[axis]!=maximum[axis]+radius) + add("stamp_bounds",stamp.stableId + +" conservative continuous-sweep bounds do not match its path"); + } else { const std::array extents={ @@ -1556,6 +1616,14 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( for(std::size_t axis=0;axis<3&&axis point{}; + const auto& values=value.AsArray(); + for(std::size_t axis=0;axis<3&&axis1000000000000ll) {error="stamp request "+request.stableId+" has an unbounded anchor";return false;} - if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment) + if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment + &&request.sweepPathMicrometres.empty()) { double lengthSquared=0.0; for(std::size_t axis=0;axis<3;++axis) @@ -1729,6 +1803,25 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t ||std::sqrt(lengthSquared)>2.0*request.halfExtentsMicrometres[0]+1.0) {error="stamp request "+request.stableId+" has invalid swept-segment extents";return false;} } + if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment + &&!request.sweepPathMicrometres.empty()) + { + if(request.sweepPathMicrometres.size()<2 + ||request.sweepPathMicrometres.size()>1025 + ||request.sweepPathMicrometres.front()!=request.sweepEndpointsMicrometres[0] + ||request.sweepPathMicrometres.back()!=request.sweepEndpointsMicrometres[1]) + {error="stamp request "+request.stableId+" has an invalid continuous deck path";return false;} + for(std::size_t pointIndex=0;pointIndex1000000000000ll) + {error="stamp request "+request.stableId+" has an unbounded path point";return false;} + if(pointIndex>0&&request.sweepPathMicrometres[pointIndex-1][0]>=point[0]) + {error="stamp request "+request.stableId+" path X coordinates must increase strictly";return false;} + } + } // The shared JSON value type stores numbers as IEEE-754 doubles; keep the // serialized seed in its exact integer range so the canonical round trip holds. @@ -1748,6 +1841,7 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t ResolvedLayoutStampV1 stamp;stamp.stableId=request.stableId; stamp.strateIndex=request.strateIndex;stamp.kind=request.kind; stamp.sweepEndpointsMicrometres=request.sweepEndpointsMicrometres; + stamp.sweepPathMicrometres=request.sweepPathMicrometres; stamp.halfExtentsMicrometres=request.halfExtentsMicrometres; stamp.smoothWidthMicrometres=request.smoothWidthMicrometres; stamp.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres; @@ -1769,7 +1863,8 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t stamp.positionMicrometres[axis]=request.anchorPositionMicrometres[axis]+offset[axis]; const std::int64_t pad=stamp.smoothWidthMicrometres +stamp.irregularityAmplitudeMicrometres; - if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment) + if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment + &&stamp.sweepPathMicrometres.empty()) { double radiusSquared=0.0; for(const auto extent:stamp.halfExtentsMicrometres) @@ -1788,6 +1883,30 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t stamp.boundsMaximumMicrometres[axis]=maximum+radius; } } + else if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment) + { + double radiusSquared=0.0; + for(std::size_t axis=1;axis<3;++axis) + { + const double expanded=static_cast(stamp.halfExtentsMicrometres[axis]+pad); + radiusSquared+=expanded*expanded; + } + const std::int64_t radius=static_cast( + std::ceil(std::sqrt(radiusSquared))); + std::array minimum=stamp.sweepPathMicrometres.front(); + std::array maximum=minimum; + for(const auto& point:stamp.sweepPathMicrometres) + for(std::size_t axis=0;axis<3;++axis) + { + minimum[axis]=std::min(minimum[axis],point[axis]); + maximum[axis]=std::max(maximum[axis],point[axis]); + } + for(std::size_t axis=0;axis<3;++axis) + { + stamp.boundsMinimumMicrometres[axis]=minimum[axis]-radius; + stamp.boundsMaximumMicrometres[axis]=maximum[axis]+radius; + } + } else { const std::array extents={ diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index b9a8c8a..ed036ec 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -3536,6 +3536,7 @@ private: const VoxelForgeCore::ResolvedLayoutStampV1* record=nullptr; std::array positionVoxels{},halfExtentsVoxels{}; std::array,2> sweepEndpointsVoxels{}; + std::vector> sweepPathVoxels; float smoothWidthVoxels=0.0f; float irregularityAmplitudeVoxels=0.0f; float irregularityWavelengthVoxels=0.0f; @@ -3710,6 +3711,15 @@ private: static_cast(stamp.sweepEndpointsMicrometres[endpoint][axis]) /Owner->CoreVoxelSizeMicrons); } + runtime.sweepPathVoxels.reserve(stamp.sweepPathMicrometres.size()); + for(const auto& pointMicrometres:stamp.sweepPathMicrometres) + { + std::array point{}; + for(std::size_t axis=0;axis<3;++axis) + point[axis]=static_cast(static_cast(pointMicrometres[axis]) + /Owner->CoreVoxelSizeMicrons); + runtime.sweepPathVoxels.push_back(point); + } const std::size_t xAxis=stamp.yawQuarterTurns%2?1:0; const std::size_t yAxis=stamp.yawQuarterTurns%2?0:1; runtime.halfExtentsVoxels={ @@ -5169,6 +5179,20 @@ private: } public: + static std::int32_t& CurrentStampSamplingStep() noexcept + { + static thread_local std::int32_t Step=0; + return Step; + } + struct FScopedStampSamplingStep + { + explicit FScopedStampSamplingStep(std::int32_t Step) + :Previous(CurrentStampSamplingStep()) + {CurrentStampSamplingStep()=std::max(0,Step);} + ~FScopedStampSamplingStep(){CurrentStampSamplingStep()=Previous;} + std::int32_t Previous=0; + }; + bool GetNativeSurfaceHeightsAt(float X,float Y,int32 ChunkZ, float& OutGroundZ,float& OutCeilingZ) const { @@ -7123,6 +7147,111 @@ private: const float top=std::max(StampEllipsoidSdf(x,y,z,halfSize),-z); return SmoothMinDistance(lower,top,round*2.0f); } + static float StampContinuousSweptDeckSdf(const FStampRuntime& stamp, + float x,float y,float z) + { + const auto& path=stamp.sweepPathVoxels; + if(path.size()<2)return std::numeric_limits::infinity(); + const float halfWidth=stamp.halfExtentsVoxels[1]; + const float halfThickness=stamp.halfExtentsVoxels[2]; + const float round=std::min(0.35f,std::min(halfWidth,halfThickness)*0.15f); + const float searchMargin=halfWidth+halfThickness+2.0f; + const auto first=std::lower_bound(path.begin(),path.end(),x-searchMargin, + [](const auto& point,float value){return point[0](first-path.begin()); + const std::size_t lastIndex=static_cast(last-path.begin()); + const std::size_t segmentBegin=std::min(path.size()-2, + firstIndex==0?std::size_t{0}:firstIndex-1); + const std::size_t segmentEnd=std::min(path.size()-2,lastIndex); + std::size_t nearestSegment=path.size(); + float nearestProjection=0.0f; + float nearestDistanceSquared=std::numeric_limits::infinity(); + for(std::size_t segment=segmentBegin;segment<=segmentEnd;++segment) + { + const auto& a=path[segment];const auto& b=path[segment+1]; + const float dx=b[0]-a[0],dy=b[1]-a[1]; + const float horizontalSquared=dx*dx+dy*dy; + if(!(horizontalSquared>1.0e-10f))continue; + const float projection=std::clamp(((x-a[0])*dx+(y-a[1])*dy) + /horizontalSquared,0.0f,1.0f); + const float nearestX=a[0]+projection*dx; + const float nearestY=a[1]+projection*dy; + const float offsetX=x-nearestX,offsetY=y-nearestY; + const float distanceSquared=offsetX*offsetX+offsetY*offsetY; + if(distanceSquared=path.size()-1)return std::numeric_limits::infinity(); + const auto evalCenterline=[&](float parameter,std::array& center, + std::array& tangent) + { + const auto& p0=path[nearestSegment]; + const auto& p1=path[nearestSegment+1]; + const auto& before=path[nearestSegment==0?nearestSegment:nearestSegment-1]; + const auto& after=path[nearestSegment+2=path.size()?p1[axis]-p0[axis] + :0.5f*(after[axis]-p0[axis]); + center[axis]=h00*p0[axis]+h10*m0+h01*p1[axis]+h11*m1; + tangent[axis]=d00*p0[axis]+d10*m0+d01*p1[axis]+d11*m1; + } + }; + std::array center{},tangent{}; + for(int refinement=0;refinement<2;++refinement) + { + evalCenterline(nearestProjection,center,tangent); + const float derivativeSquared=tangent[0]*tangent[0]+tangent[1]*tangent[1]; + if(!(derivativeSquared>1.0e-10f))return std::numeric_limits::infinity(); + const float offsetX=center[0]-x,offsetY=center[1]-y; + nearestProjection=std::clamp(nearestProjection + -(offsetX*tangent[0]+offsetY*tangent[1])/derivativeSquared,0.0f,1.0f); + } + evalCenterline(nearestProjection,center,tangent); + const float tangentLength=std::sqrt(tangent[0]*tangent[0] + +tangent[1]*tangent[1]+tangent[2]*tangent[2]); + if(!(tangentLength>1.0e-8f))return std::numeric_limits::infinity(); + for(float& component:tangent)component/=tangentLength; + const float horizontalLength=std::sqrt(tangent[0]*tangent[0] + +tangent[1]*tangent[1]); + if(!(horizontalLength>1.0e-8f))return std::numeric_limits::infinity(); + const std::array side={-tangent[1]/horizontalLength, + tangent[0]/horizontalLength,0.0f}; + const std::array up={-tangent[2]*side[1],tangent[2]*side[0], + tangent[0]*side[1]-tangent[1]*side[0]}; + const std::array relative={x-center[0],y-center[1],z-center[2]}; + const auto dot=[&](const std::array& axis) + {return relative[0]*axis[0]+relative[1]*axis[1]+relative[2]*axis[2];}; + const float lateral=dot(side),vertical=dot(up); + if((nearestSegment==0&&nearestProjection<=0.0f) + ||(nearestSegment+2==path.size()&&nearestProjection>=1.0f)) + return StampRoundedBoxSdf(dot(tangent),lateral,vertical, + {halfThickness+round,halfWidth,halfThickness},round); + const float qSide=std::abs(lateral)-halfWidth+round; + const float qUp=std::abs(vertical)-halfThickness+round; + const float outside=std::sqrt(std::max(qSide,0.0f)*std::max(qSide,0.0f) + +std::max(qUp,0.0f)*std::max(qUp,0.0f)); + return outside+std::min(std::max(qSide,qUp),0.0f)-round; + } float StampSdf(const FStampRuntime& stamp,float x,float y,float z) const { float localX=x-stamp.positionVoxels[0]; @@ -7161,6 +7290,11 @@ private: break; case VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment: { + if(!stamp.sweepPathVoxels.empty()) + { + distance=StampContinuousSweptDeckSdf(stamp,x,y,z); + break; + } std::array tangent{},side{},up{},delta{}; float lengthSquared=0.0f,horizontalSquared=0.0f; for(std::size_t axis=0;axis<3;++axis) @@ -7199,6 +7333,15 @@ private: static double StampAbsMinimum(double minimum,double maximum) {return minimum<=0.0&&maximum>=0.0?0.0:std::min(std::abs(minimum),std::abs(maximum));} + static bool UsesCoarsePillarProxy(const FStampRuntime& stamp,std::int32_t sampleStep) + { + if(!stamp.record||stamp.record->kind + !=VoxelForgeCore::ResolvedSdfStampKindV1::fillPillar + ||sampleStep<=0||stamp.irregularityAmplitudeVoxels>0.0f)return false; + return static_cast(sampleStep)>std::min(stamp.halfExtentsVoxels[0], + stamp.halfExtentsVoxels[1]) + ||static_cast(sampleStep)>stamp.halfExtentsVoxels[2]; + } static double StampRoundedBoxSdfDouble(const std::array& absolute, const std::array& halfSize,double rounding) { @@ -7300,6 +7443,7 @@ private: if(index>=StampRuntime.size())return; const auto& runtime=StampRuntime[index];const auto& record=*runtime.record; if(record.strateIndex!=bound->StrateIndex)return; + if(UsesCoarsePillarProxy(runtime,CurrentStampSamplingStep()))return; for(std::size_t axis=0;axis<3;++axis) if(worldMicrometres[axis]record.boundsMaximumMicrometres[axis])return; @@ -7317,6 +7461,154 @@ private: apply(static_cast(index)); return composed; } + +public: + std::uint32_t AppendCoarsePillarProxies(FVoxelMeshData& mesh, + const FIntVector& tileOrigin,std::int32_t tileExtent,std::int32_t sampleStep) const + { + if(!Owner||Owner->CoreVoxelSizeMicrons==0||tileExtent<=0||sampleStep<=0 + ||StampRuntime.empty())return 0; + constexpr int32 sideCount=24; + constexpr double TwoPi=6.2831853071795864769; + const int32 originalVertexCount=mesh.Vertices.Num(); + const int32 originalSurfaceVertices=std::clamp(mesh.NumSurfaceVertices,0, + originalVertexCount); + const bool hasNormals=mesh.Normals.Num()==originalVertexCount; + const bool hasUvs=mesh.UVs.Num()==originalVertexCount; + const bool hasColors=mesh.Colors.Num()==originalVertexCount; + const double voxelCentimetres=Owner->CoreVoxelSizeMicrons/10000.0; + const std::int64_t tileMinimum[3]={tileOrigin.X,tileOrigin.Y,tileOrigin.Z}; + const std::int64_t tileMaximum[3]={static_cast(tileOrigin.X)+tileExtent, + static_cast(tileOrigin.Y)+tileExtent, + static_cast(tileOrigin.Z)+tileExtent}; + std::vector proxyIndices; + std::uint32_t proxyCount=0; + const auto addVertex=[&](const FVector& position,const FVector& normal, + const FVector2f& uv) + { + const int32 index=mesh.Vertices.Add(position); + if(hasNormals)mesh.Normals.Add(normal); + if(hasUvs)mesh.UVs.Add(uv); + if(hasColors)mesh.Colors.Add(FColor::White); + return index; + }; + for(const auto& stamp:StampRuntime) + { + if(!UsesCoarsePillarProxy(stamp,sampleStep))continue; + const auto& center=stamp.positionVoxels; + if(center[0]=tileMaximum[0] + ||center[1]=tileMaximum[1] + ||center[2]=tileMaximum[2])continue; + const double radiusX=stamp.halfExtentsVoxels[0]; + const double radiusY=stamp.halfExtentsVoxels[1]; + const double bottomVoxel=center[2]-stamp.halfExtentsVoxels[2]-1.0; + const double topVoxel=center[2]+stamp.halfExtentsVoxels[2]; + const auto point=[&](double angle,double z) + { + return FVector((center[0]+radiusX*std::cos(angle))*voxelCentimetres, + (center[1]+radiusY*std::sin(angle))*voxelCentimetres, + z*voxelCentimetres); + }; + const int32 capTopCenter=addVertex(FVector(center[0]*voxelCentimetres, + center[1]*voxelCentimetres,topVoxel*voxelCentimetres), + FVector(0.0,0.0,1.0),FVector2f(0.5f,0.5f)); + const int32 capBottomCenter=addVertex(FVector(center[0]*voxelCentimetres, + center[1]*voxelCentimetres,bottomVoxel*voxelCentimetres), + FVector(0.0,0.0,-1.0),FVector2f(0.5f,0.5f)); + for(int32 sideIndex=0;sideIndex(sideIndex)/sideCount; + const double u1=static_cast(sideIndex+1)/sideCount; + const double a0=TwoPi*u0,a1=TwoPi*u1; + const FVector n0=FVector(std::cos(a0)/radiusX, + std::sin(a0)/radiusY,0.0).GetSafeNormal(); + const FVector n1=FVector(std::cos(a1)/radiusX, + std::sin(a1)/radiusY,0.0).GetSafeNormal(); + const int32 bottomA=addVertex(point(a0,bottomVoxel),n0,FVector2f(u0,0.0f)); + const int32 bottomB=addVertex(point(a1,bottomVoxel),n1,FVector2f(u1,0.0f)); + const int32 topB=addVertex(point(a1,topVoxel),n1,FVector2f(u1,1.0f)); + const int32 topA=addVertex(point(a0,topVoxel),n0,FVector2f(u0,1.0f)); + proxyIndices.insert(proxyIndices.end(),{bottomA,bottomB,topB, + bottomA,topB,topA}); + + const int32 topAIndex=addVertex(point(a0,topVoxel),FVector(0.0,0.0,1.0), + FVector2f(static_cast(0.5+0.5*std::cos(a0)), + static_cast(0.5+0.5*std::sin(a0)))); + const int32 topBIndex=addVertex(point(a1,topVoxel),FVector(0.0,0.0,1.0), + FVector2f(static_cast(0.5+0.5*std::cos(a1)), + static_cast(0.5+0.5*std::sin(a1)))); + proxyIndices.insert(proxyIndices.end(),{capTopCenter,topAIndex,topBIndex}); + + const int32 bottomAIndex=addVertex(point(a0,bottomVoxel),FVector(0.0,0.0,-1.0), + FVector2f(static_cast(0.5+0.5*std::cos(a0)), + static_cast(0.5+0.5*std::sin(a0)))); + const int32 bottomBIndex=addVertex(point(a1,bottomVoxel),FVector(0.0,0.0,-1.0), + FVector2f(static_cast(0.5+0.5*std::cos(a1)), + static_cast(0.5+0.5*std::sin(a1)))); + proxyIndices.insert(proxyIndices.end(), + {capBottomCenter,bottomBIndex,bottomAIndex}); + } + ++proxyCount; + } + if(proxyIndices.empty())return 0; + const int32 proxyVertexCount=mesh.Vertices.Num()-originalVertexCount; + const auto insertProxyVertices=[&](auto& values) + { + if(values.Num()!=originalVertexCount+proxyVertexCount)return; + using FArray=std::remove_reference_t; + FArray reordered; + reordered.Reserve(values.Num()); + for(int32 index=0;index(proxyIndices.size()/3); + TArray mergedIndices; + mergedIndices.Reserve(mesh.Triangles.Num()+static_cast(proxyIndices.size())); + for(int32 index=0;index=originalSurfaceVertices + ?oldIndex+proxyVertexCount:oldIndex); + } + for(const int32 index:proxyIndices) + mergedIndices.Add(originalSurfaceVertices+index-originalVertexCount); + for(int32 index=ceilingStart;index=originalSurfaceVertices + ?oldIndex+proxyVertexCount:oldIndex); + } + mesh.Triangles=MoveTemp(mergedIndices); + + TArray mergedMasks; + mergedMasks.Reserve(oldTriangleCount+proxyTriangleCount); + for(int32 triangle=0;triangle0) + std::printf("[CoarsePillarProxy] key=(%d,%d,%d,lod%d) sampling_step_vox=%d " + "pillars=%u mode=exact_capped_cylinder status=EMITTED\n", + key.coordinate.x,key.coordinate.y,key.coordinate.z,key.lod,MeshStep, + coarsePillarProxyCount); l77RefineStage.Restore(); out.densitySampleCount=static_cast(std::max(0,sampleCount)); diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/BuildL143Fixtures.py b/Standalone/Research/WorldAssemblerM3/Linux143/BuildL143Fixtures.py new file mode 100644 index 0000000..4a78407 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux143/BuildL143Fixtures.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Build the L143 liminal pictures and walk-audit controls.""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA", + ROOT / "Standalone/data/owner_world.json")) +OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l143")) + +FLAT_FIELD = { + "version": 1, + "kind": "flatField", + "minimum_arena_clearance_m": 0.0, +} + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures") + args = parser.parse_args() + args.out.mkdir(parents=True, exist_ok=True) + base = json.loads(OWNER.read_text(encoding="utf-8")) + + monolith = dict(base) + monolith["surface_profile_override"] = dict(FLAT_FIELD) + monolith["motif_family_override"] = { + "version": 1, + "strate_index": 0, + "placement_policy": "seed,strate,stable_id", + "motifs": [{ + "stable_id": "liminal/oversized-familiar-monolith-v1", + "kind": "cappedCylinder", + "radius_m": 1.6, + "height_m": 26.0, + "route_side_offset_m": 42.0, + "placement_radius_m": 0.0, + "fillet_radius_m": 0.6, + "blend_width_m": 1.2, + "minimum_spacing_m": 0.0, + "yaw_quarter_turns": 0, + "attachment": "seated_floor", + }], + } + + hills = dict(base) + hills["surface_profile_override"] = { + "version": 1, + "kind": "periodicHills", + "amplitude_x_m": 2.2, + "amplitude_y_m": 2.2, + "wavelength_x_m": 80.0, + "wavelength_y_m": 80.0, + "minimum_arena_clearance_m": 0.0, + } + + road_override = { + "stable_id": "liminal/road-clouds-v2", + "strate_index": 0, + "start_xy_m": [120.0, 340.0], + "path_length_m": 360.0, + "curve_control_offset_m": 84.0, + "deck_height_above_field_m": 44.0, + "rise_control_m": 22.0, + "half_width_m": 4.0, + "half_thickness_m": 1.0, + "sample_spacing_m": 1.0, + "support_spacing_m": 24.0, + "support_radius_m": 0.55, + } + road = dict(base) + road["surface_profile_override"] = dict(FLAT_FIELD) + road["liminal_road_override"] = dict(road_override) + + short_control = dict(base) + short_control["surface_profile_override"] = dict(FLAT_FIELD) + short_override = dict(road_override) + short_override["path_length_m"] = 180.0 + short_control["liminal_road_override"] = short_override + + pillars = dict(base) + pillars["surface_profile_override"] = dict(FLAT_FIELD) + pillars["liminal_pillar_grid_override"] = { + "stable_id": "liminal/horizon-pillar-grid-v1", + "strate_index": 0, + "center_xy_m": [700.0, 800.0], + "rows": 15, + "columns": 15, + "spacing_m": 76.0, + "radius_m": 5.5, + "height_m": 40.0, + } + + fixtures = ( + ("flatField-monolith", monolith), + ("periodicHills", hills), + ("road-in-clouds", road), + ("road-short-control", short_control), + ("pillar-grid", pillars), + ) + for name, fixture in fixtures: + path = args.out / f"liminal-{name}.json" + path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8") + print(f"[L143Fixture] name={name} file={path} status=PASS") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/CaptureL143.py b/Standalone/Research/WorldAssemblerM3/Linux143/CaptureL143.py new file mode 100644 index 0000000..f9822eb --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux143/CaptureL143.py @@ -0,0 +1,463 @@ +#!/usr/bin/env python3 +"""Run L143 liminal determinism checks, walk audits and multi-LOD captures.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import time + + +ROOT = Path(__file__).resolve().parents[4] +OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l143")) +CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +Q_PER_METRE = 1024.0 +FOG_COLOR = "0.66,0.73,0.77" + +sys.path.insert(0, str(ROOT / "Standalone/Tools/verify")) +from runner import bounded_run, current_stamp, first_stamp + + +def fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def run_step(name: str, command: list[object], log: Path, *, env: dict[str, str], + stamped: bool = False, timeout_seconds: int = 1740, + expected_exit: int = 0) -> str: + expected_stamp = current_stamp() if stamped else None + if stamped: + binary = Path(str(command[0])) + probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15) + if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected_stamp: + raise RuntimeError(f"{name} refused a stale executable: {binary}") + started = time.monotonic() + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, f"timeout", f"{timeout_seconds}s", + *map(str, command)], cwd=ROOT, env=env, + stdout=stream, stderr=subprocess.STDOUT, + timeout=timeout_seconds + 5, check=False) + elapsed = time.monotonic() - started + output = log.read_text(encoding="utf-8", errors="replace") + print(f"[L143Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}", + flush=True) + if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp): + raise RuntimeError(f"{name} source stamp mismatch; see {log}") + if result.returncode != expected_exit: + raise RuntimeError(f"{name} exit={result.returncode}, expected={expected_exit}; see {log}") + return output + + +def route_frame(manifest_path: Path) -> tuple[tuple[float, float, float], + tuple[float, float, float]]: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + strata = [entry for entry in manifest["plan"]["strates"] + if entry.get("route") and len(entry["route"]) >= 2] + if not strata: + raise RuntimeError(f"no planned SurfaceWorld route in {manifest_path}") + route = strata[0]["route"] + points = [tuple(float(value) / Q_PER_METRE for value in item["position_q"]) + for item in route] + lengths = [math.dist(a, b) for a, b in zip(points, points[1:])] + total = sum(lengths) + if total <= 1.0e-6: + raise RuntimeError(f"degenerate planned route in {manifest_path}") + + def at(distance: float) -> tuple[float, float, float]: + remaining = max(0.0, min(total, distance)) + for index, (a, b, length) in enumerate(zip(points, points[1:], lengths)): + if remaining <= length or index == len(lengths) - 1: + if length <= 1.0e-9: + return b + t = remaining / length + return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3)) + remaining -= length + return points[-1] + + center = at(total * 0.5) + before, after = at(total * 0.5 - 1.0), at(total * 0.5 + 1.0) + dx, dy = after[0] - before[0], after[1] - before[1] + length = math.hypot(dx, dy) + if length <= 1.0e-9: + raise RuntimeError(f"degenerate route tangent in {manifest_path}") + return center, (dx / length, dy / length, 0.0) + + +def camera_angles(camera, target) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy))) + + +def point_toward(camera, target) -> tuple[float, ...]: + yaw, pitch = camera_angles(camera, target) + return (*camera, yaw, pitch) + + +def stamp(layout: dict, stable_id: str) -> dict: + for record in layout.get("stamps", []): + if record.get("stable_id") == stable_id: + return record + raise RuntimeError(f"missing resolved stamp {stable_id}") + + +def make_views(name: str, manifest_path: Path, layout_path: Path): + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + layout = json.loads(layout_path.read_text(encoding="utf-8")) + center, tangent = route_frame(manifest_path) + tx, ty, _ = tangent + nx, ny = -ty, tx + base_height = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE + if name == "flatField-monolith": + monolith = stamp(layout, "liminal/oversized-familiar-monolith-v1") + sx, sy, _ = (value / 1.0e6 for value in monolith["position_um"]) + floor = monolith["floor_um"] / 1.0e6 + eye_camera = (sx - tx * 36.0, sy - ty * 36.0, floor + 1.6) + eye_target = (sx, sy, floor + 10.5) + wide_camera = (sx - tx * 42.0 - nx * 18.0, + sy - ty * 42.0 - ny * 18.0, floor + 42.0) + wide_target = (sx, sy, floor + 8.0) + return { + "eye": (eye_camera, eye_target, 20), + "wide": (wide_camera, wide_target, 24), + "height_center": floor, + "height_range": 38.0, + } + if name == "periodicHills": + # Route points are capsule centers: +0.72 m places the view at 1.60 m above ground. + eye_camera = (center[0], center[1], center[2] + 0.72) + eye_target = (center[0] - tx * 360.0, center[1] - ty * 360.0, + center[2] + 0.72) + wide_camera = (center[0] - tx * 36.0 - nx * 28.0, + center[1] - ty * 36.0 - ny * 28.0, center[2] + 30.0) + wide_target = (center[0] - tx * 310.0, center[1] - ty * 310.0, + center[2] + 0.72) + return { + "eye": (eye_camera, eye_target, 20), + "wide": (wide_camera, wide_target, 28), + "height_center": base_height, + "height_range": 6.0, + } + if name == "road-in-clouds": + deck = stamp(layout,"liminal/road-clouds-v2/deck/000") + path_um = deck.get("sweep_path_um", []) + if len(path_um) < 2: + raise RuntimeError("road layout has no resolved continuous swept path") + points = [tuple(value / 1.0e6 for value in point) for point in path_um] + half_thickness = deck["half_extents_um"][2] / 1.0e6 + top = lambda point: point[2] + half_thickness + index = max(1, len(points) // 12) + camera_dx = points[index + 1][0] - points[index - 1][0] + camera_dy = points[index + 1][1] - points[index - 1][1] + camera_tangent_length = math.hypot(camera_dx, camera_dy) + camera_tx, camera_ty = (camera_dx / camera_tangent_length, + camera_dy / camera_tangent_length) + camera_nx, camera_ny = -camera_ty, camera_tx + half_width = deck["half_extents_um"][1] / 1.0e6 + eye_target_index = min(len(points) - 1, index + 25) + eye_camera = (points[index][0] + camera_nx * (half_width + 2.0), + points[index][1] + camera_ny * (half_width + 2.0), + top(points[index]) + 1.6) + eye_target = (points[eye_target_index][0] + camera_nx * (half_width - 0.4), + points[eye_target_index][1] + camera_ny * (half_width - 0.4), + top(points[eye_target_index]) + 0.3) + middle = len(points) // 2 + dx = points[middle + 1][0] - points[middle - 1][0] + dy = points[middle + 1][1] - points[middle - 1][1] + tangent_length = math.hypot(dx, dy) + tx, ty = dx / tangent_length, dy / tangent_length + nx, ny = -ty, tx + wide_target_index = min(len(points) - 1, middle + 30) + wide_camera = (points[middle][0] - tx * 35.0 + nx * 30.0, + points[middle][1] - ty * 35.0 + ny * 30.0, + top(points[middle]) + 25.0) + wide_target = (points[wide_target_index][0], points[wide_target_index][1], + top(points[wide_target_index]) - 5.0) + cloud_camera = (points[middle][0] - tx * 18.0 + nx * 12.0, + points[middle][1] - ty * 18.0 + ny * 12.0, + base_height + 1.6) + cloud_target = (points[middle][0] + tx * 8.0, + points[middle][1] + ty * 8.0, + points[middle][2] - half_thickness * 0.35) + return { + "eye": (eye_camera, eye_target, 3), + "wide": (wide_camera, wide_target, 3), + "cloud-up": (cloud_camera, cloud_target, 3), + "height_center": base_height + 23.0, + "height_range": 60.0, + "road_deck": top(points[middle]), + "road_width": 2.0 * half_width, + } + if name == "pillar-grid": + pillars = [row for row in layout.get("stamps", []) + if row.get("stable_id", "").startswith("liminal/horizon-pillar-grid-v1/pillar/")] + if len(pillars) < 9: + raise RuntimeError("pillar-grid layout has too few resolved supports") + xs = [row["position_um"][0] / 1.0e6 for row in pillars] + ys = [row["position_um"][1] / 1.0e6 for row in pillars] + floor = min((row["position_um"][2] - row["half_extents_um"][2]) / 1.0e6 + for row in pillars) + cx, cy = (min(xs) + max(xs)) * 0.5, (min(ys) + max(ys)) * 0.5 + eye_camera = (min(xs) - 78.0, cy, floor + 1.6) + eye_target = (max(xs) + 90.0, cy, floor + 1.6) + wide_camera = (cx - 280.0, cy - 420.0, floor + 230.0) + wide_target = (cx, cy, floor + 6.0) + return { + "eye": (eye_camera, eye_target, 3), + "wide": (wide_camera, wide_target, 3), + "height_center": floor + 12.0, + "height_range": 42.0, + } + raise RuntimeError(f"no capture framing for fixture {name}") + + +def headless_command(binary: Path, fixture: Path, camera, mesh: Path, + manifest: Path, layout: Path, workers: int, + clip_radius: int, *, hash_tiles: bool = False, + audit_road: bool = False, max_level: int = 0, + far_lods: bool = False, + max_clip_override: int | None = None) -> list[object]: + max_clip_level = (max_clip_override if max_clip_override is not None else + 8 if far_lods else max_level) + command: list[object] = [binary, fixture, f"--workers={workers}", + "--seed=0", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--force-generate", f"--clip-radius={clip_radius}", + f"--max-clip-level={max_clip_level}", + f"--max-level={max_level}", "--render-distance-chunks=0", + f"--camera={fmt(point_toward(camera, (camera[0], camera[1] + 1.0, camera[2])))}", + f"--mesh-export={mesh}", f"--world-assembler-manifest-output={manifest}", + f"--resolved-layout-output={layout}"] + if hash_tiles: + command.append("--stream-hash-tiles") + if audit_road: + command.append("--audit-liminal-road-walk") + return command + + +def determinism_digest(output: str, manifest_path: Path, + layout_path: Path) -> tuple[str, str, str]: + tile_hashes = sorted(line for line in output.splitlines() + if line.startswith("[PipelineTileHash] ")) + if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes: + raise RuntimeError("determinism run omitted manifest, layout or per-tile hashes") + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + layout = json.loads(layout_path.read_text(encoding="utf-8")) + manifest_digest = (manifest["content_hash"],manifest["recipe"]["content_hash"], + manifest["plan"]["content_hash"]) + return ("|".join(manifest_digest),layout["stable_identity"],"\n".join(tile_hashes)) + + +def run_determinism(name: str, fixture: Path, view, *, binaries: dict[str, Path], + out: Path, env: dict[str, str], max_level: int = 0, + far_lods: bool = False, max_clip_override: int | None = None) -> None: + camera, _, _ = view["eye"] + clip_radius = 1 + max_clip = 0 + baseline = None + cells = 0 + for compiler, workers in ((compiler, workers) + for compiler in ("clang", "gcc") + for workers in (1, 8, 12)): + tag = f"{name}-{compiler}-w{workers}" + run_dir = out / "determinism" / tag + run_dir.mkdir(parents=True, exist_ok=True) + mesh = run_dir / "feature.vfmesh" + manifest = run_dir / "manifest.json" + layout = run_dir / "layout.json" + command = headless_command(binaries[compiler], fixture,camera,mesh,manifest, + layout,workers,max_clip,hash_tiles=True,max_level=max_level, + far_lods=far_lods,max_clip_override=max_clip_override) + output = run_step(tag,command,out/"logs"/f"determinism-{tag}.log", + env=env,stamped=True) + digest = determinism_digest(output,manifest,layout) + if baseline is None: + baseline = digest + elif digest != baseline: + raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs") + cells += 1 + print(f"[L143Determinism] fixture={name} compiler_worker_cells={cells} identical=yes status=PASS", + flush=True) + + +def export_and_capture(name: str, fixture: Path, views: dict, *, + headless: Path, renderer: Path, out: Path, + artifact_dir: Path, env: dict[str, str]) -> None: + view_names = ("eye", "wide", "cloud-up") if name == "road-in-clouds" else ("eye", "wide") + for view_name in view_names: + camera, target, clip_radius = views[view_name] + clip_radius = min(clip_radius,3) + stem = f"{name}-{view_name}" + mesh = out / "capture-meshes" / f"{stem}.vfmesh" + manifest = out / "capture-manifests" / f"{stem}.json" + layout = out / "capture-layouts" / f"{stem}.json" + png = out / "capture-png" / f"{stem}.png" + image = artifact_dir / f"{stem}.jpg" + for path in (mesh,manifest,layout,png,image): + path.parent.mkdir(parents=True,exist_ok=True) + command = headless_command(headless,fixture,camera,mesh,manifest,layout,12, + clip_radius,max_level=-1,far_lods=True, + max_clip_override=2 if name == "road-in-clouds" else None) + output = run_step(f"capture-{stem}-export",command, + out/"logs"/f"capture-{stem}-export.log",env=env,stamped=True) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{stem} produced an empty mesh") + yaw, pitch = camera_angles(camera,target) + height_center = views["height_center"] + height_range = views["height_range"] + if name == "road-in-clouds": + fog_start,fog_end,fog_color=(24,260,"0.88,0.93,0.95") + elif name == "pillar-grid": + fog_start,fog_end,fog_color=220,1250,"0.82,0.88,0.91" + elif name == "periodicHills": + fog_start,fog_end,fog_color=210,1000,"0.78,0.84,0.88" + else: + fog_start,fog_end,fog_color=30,105,FOG_COLOR + renderer_command = [renderer,mesh,png,"--size=1600x900","--fov-deg=72", + f"--camera-m={fmt(camera)}",f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}","--tile-skirts=on","--shade=readable", + "--headlamp=off","--height-tint=on",f"--height-center-m={height_center:.6f}", + f"--height-range-m={height_range:.6f}","--light-direction=-0.42,-0.58,0.70", + "--fog=on",f"--fog-start-m={fog_start}",f"--fog-end-m={fog_end}", + f"--fog-color={fog_color}"] + run_step(f"capture-{stem}-render",renderer_command, + out/"logs"/f"capture-{stem}-render.log",env=env,stamped=True) + run_step(f"capture-{stem}-jpeg",["ffmpeg","-nostdin","-hide_banner","-loglevel", + "error","-y","-i",png,"-frames:v","1","-q:v","4",image], + out/"logs"/f"capture-{stem}-jpeg.log",env=env) + quality = run_step(f"capture-{stem}-quality", + [sys.executable,ROOT/"Standalone/Tools/verify/capture_quality.py",image], + out/"logs"/f"capture-{stem}-quality.log",env=env) + if "[CaptureQuality]" not in quality or not image.is_file() \ + or image.stat().st_size > 300_000: + raise RuntimeError(f"{stem} capture failed quality or size checks") + eye_height = "1.600m" if view_name in ("eye", "cloud-up") else "wide" + print(f"[L143Capture] fixture={name} view={view_name} eye_height={eye_height} " + f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) bytes={image.stat().st_size} " + f"sha256={sha256(image)} status=PASS",flush=True) + + +def run_road_walk_audit(fixture: Path, view, *, headless: Path, + out: Path, env: dict[str, str]) -> None: + camera = view["eye"][0] + run_dir = out / "road-walk-audit" + run_dir.mkdir(parents=True,exist_ok=True) + output = run_step("road-walk-audit",headless_command(headless,fixture,camera, + run_dir/"audit.vfmesh",run_dir/"manifest.json",run_dir/"layout.json",12,1, + audit_road=True),out/"logs"/"road-walk-audit.log",env=env,stamped=True) + if not re.search(r"\[LiminalRoadWalkAudit\].*status=PASS",output): + raise RuntimeError("raised road failed the Core capsule walk audit") + + +def run_road_short_negative_control(fixture: Path, view, *, headless: Path, + out: Path, env: dict[str, str]) -> None: + camera = view["eye"][0] + run_dir = out / "road-short-negative-control" + run_dir.mkdir(parents=True,exist_ok=True) + output = run_step("road-short-negative-control", + headless_command(headless,fixture,camera,run_dir/"control.vfmesh", + run_dir/"manifest.json",run_dir/"layout.json",12,1,audit_road=True), + out/"logs"/"road-short-negative-control.log",env=env,stamped=True, + expected_exit=14) + match = re.search(r"\[LiminalRoadWalkAudit\].*path_length_m=([0-9.]+).*status=FAIL",output) + if not match or float(match.group(1)) >= 300.0: + raise RuntimeError("short-road negative control did not falsify the length gate") + print(f"[L143RoadNegativeControl] path_length_m={match.group(1)} " + "expected=FAIL status=PASS",flush=True) + + +def retain_unchanged_captures(artifact_dir: Path) -> None: + reference_dir = Path(__file__).resolve().parents[1] / "Linux142" / "captures" + for name in ("flatField-monolith", "periodicHills"): + for view in ("eye", "wide"): + filename = f"{name}-{view}.jpg" + source, destination = reference_dir / filename, artifact_dir / filename + if not source.is_file(): + raise RuntimeError(f"missing L142 reference capture {source}") + shutil.copy2(source,destination) + if sha256(source) != sha256(destination): + raise RuntimeError(f"unchanged reference capture copy differs: {filename}") + print(f"[L143Capture] fixture={name} view={view} retained_from=Linux142 " + f"sha256={sha256(destination)} status=PASS",flush=True) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=OUT_DEFAULT) + parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT) + parser.add_argument("--headless-clang", type=Path, required=True) + parser.add_argument("--headless-gcc", type=Path, required=True) + parser.add_argument("--renderer", type=Path, required=True) + args = parser.parse_args() + + out = args.out.resolve() + artifact_dir = args.artifact_dir.resolve() + fixture_dir = out / "fixtures" + for directory in (out,fixture_dir,artifact_dir): + directory.mkdir(parents=True,exist_ok=True) + env = os.environ.copy() + env["VOXELFORGE_VERIFY_OUT"] = str(out) + env["VOXELFORGE_OWNER_DATA"] = str(ROOT/"Standalone/data/owner_world.json") + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(out/"xdg-cache") + run_step("build-fixtures",[sys.executable, + ROOT/"Standalone/Research/WorldAssemblerM3/Linux143/BuildL143Fixtures.py", + f"--out={fixture_dir}"],out/"logs"/"build-fixtures.log",env=env) + + binaries = {"clang":args.headless_clang.resolve(),"gcc":args.headless_gcc.resolve()} + fixtures = { + "flatField-monolith":fixture_dir/"liminal-flatField-monolith.json", + "periodicHills":fixture_dir/"liminal-periodicHills.json", + "road-in-clouds":fixture_dir/"liminal-road-in-clouds.json", + "pillar-grid":fixture_dir/"liminal-pillar-grid.json", + } + probed = {} + for name,fixture in fixtures.items(): + probe_dir=out/"plan-probes"/name + probe_dir.mkdir(parents=True,exist_ok=True) + camera=(0.0,0.0,-500.0) + output=run_step(f"plan-probe-{name}",headless_command(binaries["clang"],fixture, + camera,probe_dir/"probe.vfmesh",probe_dir/"manifest.json", + probe_dir/"layout.json",12,1),out/"logs"/f"plan-probe-{name}.log", + env=env,stamped=True) + if "[WorldAssemblerFullManifest]" not in output: + raise RuntimeError(f"{name} probe omitted its full plan manifest") + probed[name]=make_views(name,probe_dir/"manifest.json",probe_dir/"layout.json") + + for name,fixture in fixtures.items(): + run_determinism(name,fixture,probed[name],binaries=binaries,out=out,env=env) + run_determinism("pillar-grid-far-lod",fixtures["pillar-grid"],probed["pillar-grid"], + binaries=binaries,out=out,env=env,max_level=-1,far_lods=True,max_clip_override=8) + run_road_walk_audit(fixtures["road-in-clouds"],probed["road-in-clouds"], + headless=binaries["clang"],out=out,env=env) + run_road_short_negative_control(fixture_dir/"liminal-road-short-control.json", + probed["road-in-clouds"],headless=binaries["clang"],out=out,env=env) + retain_unchanged_captures(artifact_dir) + for name in ("road-in-clouds", "pillar-grid"): + fixture=fixtures[name] + export_and_capture(name,fixture,probed[name],headless=binaries["clang"], + renderer=args.renderer.resolve(),out=out,artifact_dir=artifact_dir,env=env) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L143Capture] status=FAIL error={error}",file=sys.stderr,flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux143/REPORT.md new file mode 100644 index 0000000..cd177f6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux143/REPORT.md @@ -0,0 +1,64 @@ +# L143 — liminal polish + +## Captures + +### Road + +Eye-height: + +![Road deck at eye height](captures/road-in-clouds-eye.jpg) + +Wide: + +![Road deck and supports, wide view](captures/road-in-clouds-wide.jpg) + +Looking up from the cloud floor: + +![Road deck underside from the cloud floor](captures/road-in-clouds-cloud-up.jpg) + +### Pillar grid + +Eye-height: + +![Pillar grid at eye height](captures/pillar-grid-eye.jpg) + +Wide: + +![Pillar grid, wide view](captures/pillar-grid-wide.jpg) + +### Kept from L142 + +These four captures are byte-identical to L142. + +![Monolith, eye view](captures/flatField-monolith-eye.jpg) + +![Monolith, wide view](captures/flatField-monolith-wide.jpg) + +![Periodic hills, eye view](captures/periodicHills-eye.jpg) + +![Periodic hills, wide view](captures/periodicHills-wide.jpg) + +## Result + +The road is one continuous swept deck, and the distant pillar silhouettes remain complete and consistent. The monolith and periodic hills were retained unchanged. + +## Changes and causes + +- The road edge broke where independently capped deck segments overlapped. The road now resolves to one swept-deck stamp with a shared cubic-Hermite centerline and continuous cross-section; the centerline is sampled at 1 m spacing (361 points). The 8 m deck is 376.829 m long and retains 17 support posts. The sweep path is serialized in stamp-vocabulary V5. +- Pillars disappeared or narrowed when the coarse marching-cubes lattice skipped their cross-section. The pillar stamp's interval proof is unsupported, so it conservatively kept tiles but did not protect the mesh from undersampling. At LOD4 the sample spacing is 32 voxels (8 m), already greater than the 5.5 m pillar radius; spacing rises to 64 and 128 voxels at LOD5 and LOD6. The shared meshing path now emits deterministic 24-sided capped-cylinder proxies for regular `fillPillar` stamps once the sample step exceeds their radius or half-height. The same rule applies outside the liminal fixture. The radial approximation error for this fixture is about 4.7 cm, below one 25 cm voxel. + +## Measurements + +- Road walk audit covered the full 376.829 m route at 1,081 surface stations, with 68,103 capsule samples and 7,096 support samples: 0 blocked, 0 unsupported, 0 grade failures, and 0 surface misses. Measured maximum grade was 8.512° against a 15° limit; deck width was 8 m against a 3 m minimum. `road-short-negative-control` failed the walk audit as expected at 210.351 m. +- Determinism covered five fixtures—monolith, periodic hills, road, pillar grid, and far-LOD pillar grid—across Clang/GCC and W1/W8/W12 (six compiler/worker cells per fixture). All five reported identical outputs. +- Clang and GCC builds passed on source stamp `1baa3a711f7f96df2e70fc93579f41ad0b0211eb42fcb51dabf522e33d888877`. The capture runner and capture-quality checks passed for all requested views. +- Fast tier passed in 292.3 s. Its pinned L130-v227 reach-unit owner hashes match exactly: 12/12 rows for each owner fixture, with W1 and W12 identical. Historical L46/L53 hash comparisons remain diagnostic-only and are not the current owner-hash gate. + +## Checks and limits + +- Passed: road audit and short-road negative control; five-fixture Clang/GCC determinism; Clang and GCC builds; capture quality; fast tier; `git diff --check`. +- Not run: Unreal build (no Unreal `Source/` files changed) and L80 performance timing (this round made no performance claim). +- The coarse proxy is limited to regular `fillPillar` stamps; irregular pillars and other stamp kinds continue through their existing SDF path. The proxy was exercised at LOD4–6 in the pillar-grid captures. +- Unreal `Source/` files changed: none. Changed standalone source files: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/src/ShapeWorldInternal.h`, `Standalone/Core/src/VoxelForgeCore.cpp`, `Standalone/Source/StreamBenchmarkHeadless.cpp`, `Standalone/Source/StreamBenchmarkPipeline.h`, and `Standalone/Source/WorldAssemblyPrototype.h`. + +Detailed logs are in `Saved/Linux/l143/`; the capture and audit transcript is `Saved/Linux/l143/logs/capture-runner.log`, the walk audit is `Saved/Linux/l143/logs/road-walk-audit.log`, and the fast-tier summary is `Saved/Linux/l143/fast-summary.json`. diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/captures/flatField-monolith-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux143/captures/flatField-monolith-eye.jpg new file mode 100644 index 0000000..13eb052 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux143/captures/flatField-monolith-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/captures/flatField-monolith-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux143/captures/flatField-monolith-wide.jpg new file mode 100644 index 0000000..3975e8a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux143/captures/flatField-monolith-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/captures/periodicHills-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux143/captures/periodicHills-eye.jpg new file mode 100644 index 0000000..92ba34a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux143/captures/periodicHills-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/captures/periodicHills-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux143/captures/periodicHills-wide.jpg new file mode 100644 index 0000000..4c066a5 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux143/captures/periodicHills-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/captures/pillar-grid-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux143/captures/pillar-grid-eye.jpg new file mode 100644 index 0000000..9aad182 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux143/captures/pillar-grid-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/captures/pillar-grid-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux143/captures/pillar-grid-wide.jpg new file mode 100644 index 0000000..94bc81e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux143/captures/pillar-grid-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/captures/road-in-clouds-cloud-up.jpg b/Standalone/Research/WorldAssemblerM3/Linux143/captures/road-in-clouds-cloud-up.jpg new file mode 100644 index 0000000..eaa9982 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux143/captures/road-in-clouds-cloud-up.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/captures/road-in-clouds-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux143/captures/road-in-clouds-eye.jpg new file mode 100644 index 0000000..4e7def9 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux143/captures/road-in-clouds-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux143/captures/road-in-clouds-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux143/captures/road-in-clouds-wide.jpg new file mode 100644 index 0000000..80984a3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux143/captures/road-in-clouds-wide.jpg differ diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index d8ed139..8051136 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -1935,15 +1935,24 @@ int main(int argc,char** argv) std::sort(deck.begin(),deck.end(),[](const auto* a,const auto* b) {return a->stableIdstableId;}); double pathLength=0.0,maxGradeDegrees=0.0; - for(std::size_t i=0;i> centerline; + if(deck.size()==1&&!deck.front()->sweepPathMicrometres.empty()) + centerline=deck.front()->sweepPathMicrometres; + else { - const auto* segment=deck[i]; - const double dx=(segment->sweepEndpointsMicrometres[1][0] - -segment->sweepEndpointsMicrometres[0][0])/1.0e6; - const double dy=(segment->sweepEndpointsMicrometres[1][1] - -segment->sweepEndpointsMicrometres[0][1])/1.0e6; - const double dz=std::abs(segment->sweepEndpointsMicrometres[1][2] - -segment->sweepEndpointsMicrometres[0][2])/1.0e6; + for(std::size_t i=0;isweepEndpointsMicrometres[0]); + centerline.push_back(segment->sweepEndpointsMicrometres[1]); + } + } + for(std::size_t i=1;ihalfExtentsMicrometres[1]/1.0e6; std::printf("[LiminalRoadResolved] vocabulary=%u id=liminal/road-clouds-v2 " - "strate=%d shape=swept_deck_segments deck_segments=%zu supports=%zu " + "strate=%d shape=continuous_swept_deck deck_stamps=%zu centerline_points=%zu supports=%zu " "path_length_m=%.3f width_m=%.3f grade_max_deg=%.3f " "placement=free route_keepout=PASS band_clearance=PASS status=%s\n", resolvedLayout.stampVocabularyVersion, - deck.empty()?-1:deck.front()->strateIndex,deck.size(),supportCount,pathLength, - width,maxGradeDegrees,deck.size()>1&&pathLength>=300.0&&width>=3.0 + deck.empty()?-1:deck.front()->strateIndex,deck.size(),centerline.size(), + supportCount,pathLength,width,maxGradeDegrees, + deck.size()==1&¢erline.size()>=2&&pathLength>=300.0&&width>=3.0 &&maxGradeDegrees<=15.0?"PASS":"FAIL"); - if(deck.size()<2||pathLength<80.0||width<3.0||maxGradeDegrees>15.0) + if(deck.size()!=1||centerline.size()<2||pathLength<80.0 + ||width<3.0||maxGradeDegrees>15.0) {std::fprintf(stderr,"Liminal road resolved outside the walkable road bounds\n");return 2;} } if(hasLiminalPillarGrid) diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index dab5001..8cb1410 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -2981,28 +2981,48 @@ namespace StandaloneStreamBenchmark constexpr double HalfCapsuleHeightMetres=CapsuleHeightMetres*0.5; constexpr double SlopeCapsuleClearanceMetres=0.12; std::vector> roadPath; - roadPath.reserve(deck.size()*4); std::vector> centerline; - centerline.reserve(deck.size()+1); - bool surfacePathValid=true; - for(std::size_t i=0;ikind + ==VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment + &&deck.front()->sweepPathMicrometres.size()>=2; + bool surfacePathValid=continuousDeck; + if(continuousDeck) { - const auto* segment=deck[i]; - if(segment->kind!=VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment) - surfacePathValid=false; - const auto point=[&](std::size_t endpoint) - {return std::array{ - segment->sweepEndpointsMicrometres[endpoint][0]/1.0e6, - segment->sweepEndpointsMicrometres[endpoint][1]/1.0e6, - segment->sweepEndpointsMicrometres[endpoint][2]/1.0e6};}; - const auto a=point(0),b=point(1); - if(i==0)centerline.push_back(a); - else if(std::abs(centerline.back()[0]-a[0])>1.0e-6 - ||std::abs(centerline.back()[1]-a[1])>1.0e-6 - ||std::abs(centerline.back()[2]-a[2])>1.0e-6) - surfacePathValid=false; - centerline.push_back(b); + centerline.reserve(deck.front()->sweepPathMicrometres.size()); + for(const auto& point:deck.front()->sweepPathMicrometres) + centerline.push_back({point[0]/1.0e6,point[1]/1.0e6,point[2]/1.0e6}); + surfacePathValid=surfacePathValid + &¢erline.front()[0] + ==deck.front()->sweepEndpointsMicrometres[0][0]/1.0e6 + &¢erline.back()[0] + ==deck.front()->sweepEndpointsMicrometres[1][0]/1.0e6; } + else + { + centerline.reserve(deck.size()+1); + surfacePathValid=true; + for(std::size_t i=0;ikind + !=VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment) + surfacePathValid=false; + const auto point=[&](std::size_t endpoint) + {return std::array{ + segment->sweepEndpointsMicrometres[endpoint][0]/1.0e6, + segment->sweepEndpointsMicrometres[endpoint][1]/1.0e6, + segment->sweepEndpointsMicrometres[endpoint][2]/1.0e6};}; + const auto a=point(0),b=point(1); + if(i==0)centerline.push_back(a); + else if(std::abs(centerline.back()[0]-a[0])>1.0e-6 + ||std::abs(centerline.back()[1]-a[1])>1.0e-6 + ||std::abs(centerline.back()[2]-a[2])>1.0e-6) + surfacePathValid=false; + centerline.push_back(b); + } + } + roadPath.reserve(centerline.size()*4); double pathLengthMetres=0.0,maxGradeDegrees=0.0; std::uint64_t surfaceProbes=0,surfaceMisses=0; const auto surfaceTopAt=[&](double xMetres,double yMetres, @@ -3010,11 +3030,18 @@ namespace StandaloneStreamBenchmark { ++surfaceProbes; double low=nominalTop-0.25,high=nominalTop+1.0; - if(Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres, - low/coreMetres})>=0.0f - ||Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres, - high/coreMetres})<0.0f) - {++surfaceMisses;return false;} + const float lowDensity=Shape.densityAt({xMetres/coreMetres, + yMetres/coreMetres,low/coreMetres}); + const float highDensity=Shape.densityAt({xMetres/coreMetres, + yMetres/coreMetres,high/coreMetres}); + if(lowDensity>=0.0f||highDensity<0.0f) + { + ++surfaceMisses; + std::printf("[LiminalRoadSurfaceProbeMiss] x_m=%.6f y_m=%.6f " + "nominal_top_m=%.6f low_density=%.6f high_density=%.6f\n", + xMetres,yMetres,nominalTop,lowDensity,highDensity); + return false; + } for(int iteration=0;iteration<20;++iteration) { const double middle=(low+high)*0.5; @@ -3059,8 +3086,7 @@ namespace StandaloneStreamBenchmark const FWalkAudit roadAudit=auditPlayerPath(Shape,roadPath, 0.125,0.125,1.125,3,0.45); const double widthMetres=2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6; - passed=std::all_of(deck.begin(),deck.end(),[](const auto* road) - {return road->kind==VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment;}) + passed=continuousDeck &&pathLengthMetres>=300.0&&widthMetres>=3.0 &&maxGradeDegrees<=15.0&&surfacePathValid&&roadAudit.passed; std::printf("[LiminalRoadWalkAudit] id=liminal/road-clouds-v2 " diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index 8897078..b0aac21 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -707,26 +707,16 @@ namespace StandaloneWorldAssemblyPrototype requests.push_back(std::move(request)); }; const std::int64_t deckSmoothUm=250000; - const std::int64_t segmentOverlapUm=350000; - for(std::uint32_t i=1;i(b[0]-a[0]); - const double dy=static_cast(b[1]-a[1]); - const double dz=static_cast(b[2]-a[2]); - const std::int64_t halfLengthUm=static_cast(std::ceil( - std::sqrt(dx*dx+dy*dy+dz*dz)*0.5))+segmentOverlapUm; - ResolvedLayoutStampRequestV1 request; - request.stableId=*id+"/deck/"+std::to_string(10000+i-1).substr(1); - request.strateIndex=strateIndex; - request.kind=ResolvedSdfStampKindV1::fillSweptDeckSegment; - request.anchorPositionMicrometres={ - (a[0]+b[0])/2,(a[1]+b[1])/2,(a[2]+b[2])/2}; - request.sweepEndpointsMicrometres={a,b}; - request.halfExtentsMicrometres={halfLengthUm,halfWidthUm,halfThicknessUm}; - request.smoothWidthMicrometres=deckSmoothUm; - requests.push_back(std::move(request)); - } + ResolvedLayoutStampRequestV1 deck; + deck.stableId=*id+"/deck/000"; + deck.strateIndex=strateIndex; + deck.kind=ResolvedSdfStampKindV1::fillSweptDeckSegment; + deck.anchorPositionMicrometres=path[path.size()/2]; + deck.sweepEndpointsMicrometres={path.front(),path.back()}; + deck.sweepPathMicrometres=path; + deck.halfExtentsMicrometres={500000,halfWidthUm,halfThicknessUm}; + deck.smoothWidthMicrometres=deckSmoothUm; + requests.push_back(std::move(deck)); const std::int64_t supportBaseUm=fieldFloorMicrometres-2000000; const std::int64_t supportRadiusUm=metresToMicrometres(supportRadius); double distanceSinceSupport=supportSpacing;