diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9a4bcbd..beac98b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -570,6 +570,15 @@ ApplyModification` (BlueprintCallable) + `EditorCarveSphere/EditorFillSphere` (C driven by `EditorBrush*` props. ### 8.10 Performance invariants (DON'T regress) + +- **Far display mesh budget (L15)** (`WorldAssemblyEvaluatorVersion` 7): recipes may explicitly + declare `far_display_mesh` for LOD3+ display generation. With no declaration, the L14 mesh path + remains byte-for-byte selected. This setting must never alter LOD0–2, exact field queries, + collision, edits, or server state. A declared reduction must pass the same-LOD screen gate at the + closest displayed distance: at most 1.0 px geometry displacement, at most 1/255 normalized + shading/material delta, no worse swap pop, and no new holes, cracks, or floating pieces. Connector + wall repairs are safety geometry: retain every station/ray needed to close the existing gaps. Keep + the setting in the shared Core path so all recipe routes use the same declaration and evaluator. - **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set only when the player crosses a level-0 chunk boundary, an anchor moves, or a rebuild is forced; use the stamped desired map for the cull and idle via `bAllChunksLoaded`. On every rebuild, pending requests whose @@ -1840,6 +1849,17 @@ fill and check page tiles against the evaluator they actually sample. This is th the Unreal adapter and its asynchronous MPSC/Epoch flow are unchanged. See [`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md). +### 8.37 Screen-gated far display budget — World Assembler M3 L15 + +Evaluator version 7 adds the optional, serialized `far_display_mesh` recipe declaration for +display-only LOD3+ refinement and connector repair. A missing declaration selects the existing L14 +path; exact queries, LOD0–2, collision, edits, and route binding stay on their existing shared Core +evaluators. The acceptance gate compares against today's same-LOD display mesh, reports geometry and +fixed-light/material shading deltas, checks every displayed tile, and measures each adjacent LOD +swap before and after. It rejects any new hole, crack, or floating piece, including lost connector +panels. No tested L15 candidate met the full budget, so no declaration is enabled by default. See +[`Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md). + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/CODEMAP.md b/CODEMAP.md index 2d0f7e7..ec02069 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1132,11 +1132,21 @@ Implementation and evidence: [`Standalone/Research/WorldAssemblerM3/Linux9/REPOR | Symbol | Canonical file | Role | |---|---|---| -| `FStandaloneShapeWorld::SetSharedDensitySource`, `SharedDensitySource` | `Standalone/Core/src/ShapeWorldInternal.h`, bound before publication in `Standalone/Core/src/VoxelForgeCore.cpp` | Makes compiled analytic pages delegate field, lattice, detail, classification, and solid queries to the Core world's base shape; `WorldAssemblyEvaluatorVersion` is 6. | +| `FStandaloneShapeWorld::SetSharedDensitySource`, `SharedDensitySource` | `Standalone/Core/src/ShapeWorldInternal.h`, bound before publication in `Standalone/Core/src/VoxelForgeCore.cpp` | Makes compiled analytic pages delegate field, lattice, detail, classification, and solid queries to the Core world's base shape; `WorldAssemblyEvaluatorVersion` is 7, including optional far display-mesh recipe settings. | The base manifest field is authoritative for tile generation and exact queries. See ARCHITECTURE §8.36 and [`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md). +### 3.23 Screen-gated far display mesh — World Assembler M3 L15 + +`ApplyFarDisplayMeshDeclaration` in `Standalone/Source/WorldAssemblyPrototype.h` parses the optional +`far_display_mesh` recipe block; `WorldAssemblyEvaluatorVersion` 7 serializes and hashes it. +`VoxelForgeCore::World::generateTile` consumes its display-only refinement and connector settings. +No declaration keeps the L14 output path. `Standalone/Source/LinuxScreenSpaceGate.h` compares every +desired LOD3–8 tile against today's same-LOD mesh and audits geometry, shading, swaps, and connector +coverage. The measured candidates and null result are in +[`Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index d46e08d..d8a1328 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -184,6 +184,12 @@ namespace VoxelForgeCore std::uint64_t farConnectorColumnEvaluations = 0; double farStratumColumnComputeMilliseconds = 0.0; double farConnectorColumnComputeMilliseconds = 0.0; + // Opt-in quality audit at the L14 route-wall stations and four axial rays. + std::uint64_t farConnectorAuditRays = 0; + std::uint64_t farConnectorAuditHoles = 0; + std::uint64_t farConnectorAuditFloating = 0; + std::vector farConnectorAuditHoleIds; + std::vector farConnectorAuditFloatingIds; std::uint64_t farShellTrianglesAdded = 0; std::uint64_t horizonGroundTriangles = 0; std::uint64_t horizonGroundTrianglesRemoved = 0; @@ -250,7 +256,8 @@ namespace VoxelForgeCore inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256; inline constexpr std::uint32_t WorldAssemblySchemaVersion = 1; // Version 6 binds every compiled tile page to the world's base density evaluator. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 6; + // Version 7 adds optional, display-only far-mesh quality declarations. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 7; enum class FieldChannel : std::uint32_t { @@ -312,6 +319,17 @@ namespace VoxelForgeCore std::int64_t biomeClimateReliefWavelengthSubvoxels = 256 * 3333; std::int64_t biomeClimateMoistureWavelengthSubvoxels = 256 * 2500; std::int64_t biomeClimateReliefContrastQ16 = 2ll << 16; + // Optional display-only mesh budget. Zero leaves the L14 far-mesh path unchanged. + // Budget is in 1/1000 pixel at the closest display distance. + std::uint32_t farDisplayGeometryBudgetMillipixels = 0; + // Scale/mask change only display refinement density at the selected LODs. + // A zero scale or mask leaves the current refinement grids intact. + std::uint32_t farRefinementGridScale = 0; + std::uint32_t farRefinementLodMask = 0; + // Connector scale 0 and ray mask 0 preserve the current station/ray pattern. + std::uint32_t farConnectorStationScale = 0; + std::uint32_t farConnectorRayMask = 0; + bool farConnectorReuseStationRadii = false; std::vector biomes; std::uint64_t contentHash = 0; }; @@ -1099,6 +1117,16 @@ namespace VoxelForgeCore // Incremente quand le plafond lointain change. Les hashes LOD4-8 incluent cette version; // densite et hashes LOD0-3 restent inchanges. std::uint32_t farHorizonVersion = 3; + // Research-only override of the recipe declaration; used by same-LOD controls. + bool overrideFarDisplayMesh = false; + std::uint32_t farDisplayGeometryBudgetMillipixels = 0; + std::uint32_t farRefinementGridScale = 0; + std::uint32_t farRefinementLodMask = 0; + std::uint32_t farConnectorStationScale = 0; + std::uint32_t farConnectorRayMask = 0; + bool farConnectorReuseStationRadii = false; + bool diagnosticRemoveFarConnectorPanels = false; + bool diagnosticAuditFarConnectorCoverage = false; // Linux research controls for reproducing L5 negative controls. Defaults preserve output. std::uint32_t diagnosticGlobalOctaveDrop = 0; std::uint32_t diagnosticCellStride = 1; diff --git a/Standalone/Core/src/FarShellsInternal.h b/Standalone/Core/src/FarShellsInternal.h index da08747..a6bf5fe 100644 --- a/Standalone/Core/src/FarShellsInternal.h +++ b/Standalone/Core/src/FarShellsInternal.h @@ -242,7 +242,8 @@ inline bool FindFarConnectorRadiusNearNominal(const FStandaloneShapeWorld& Shape } inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key, - int32 Step,int32 Cells,FVoxelMeshData& Mesh) + int32 Step,int32 Cells,FVoxelMeshData& Mesh,int32 StationScale=1, + uint32 RayMask=0x0fu,bool bReuseStationRadii=false) { if(Key.Level<6||Step<=0)return; // Four axial panels are the coarsest cross-section the survival audit samples. @@ -313,7 +314,8 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV if(!bIntersects||TMax-TMin<1.0e-6)continue; // LOD7 and LOD8 share a 256-voxel connector station grid; near LOD keeps 128-voxel checks. // LOD7 et LOD8 partagent des stations aux 256 voxels; pres du joueur, garder les pas de 128. - const double ConnectorStationStep=Key.Level>=7?std::max(256.0,static_cast(Step)):128.0; + const double ConnectorStationStep=(Key.Level>=7?std::max(256.0,static_cast(Step)):128.0) + *std::max(1,StationScale); const int32 AxialSampleCount=std::max(1,static_cast(std::ceil(Length/ConnectorStationStep))); std::vector MissingMasks(static_cast(AxialSampleCount),0); std::vector> SourceRadii(static_cast(AxialSampleCount)); @@ -332,6 +334,7 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV const double DetectionTolerance=MatchTolerance*0.98; for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) { + if((RayMask&(1u<(std::lround( + T*AxialSampleCount-0.5)),0,AxialSampleCount-1); + const double ReusedRadius=bReuseStationRadii + ?SourceRadii[static_cast(NearestStation)][DirectionIndex]:0.0; + if(ReusedRadius>0.0) + Radius=ReusedRadius; + else if(!FindFarConnectorRadiusNearNominal(Shape,Center, RadialDirections[DirectionIndex],Connector.Radius,Radius,RadiusUncertainty) &&!FindFarConnectorSurfaceRadius(Shape,Center,RadialDirections[DirectionIndex], Connector.Radius,Radius)) @@ -432,7 +441,8 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV } } std::vector Rings; - const double MaxRingStep=Key.Level>=7?std::max(256.0,static_cast(Step)):128.0; + const double MaxRingStep=(Key.Level>=7?std::max(256.0,static_cast(Step)):128.0) + *std::max(1,StationScale); double CurrentT=RunMin; Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask)); while(CurrentT HoleIds; + std::vector FloatingIds; +}; + +inline std::uint64_t FarConnectorAuditRayId(const FVoxelTileKey& Key,uint32 Connector, + int32 Station,int32 Direction) +{ + std::uint64_t Hash=14695981039346656037ull; + const auto Add=[&](std::uint64_t Value,int Bytes) + { + for(int Byte=0;Byte(Value>>(Byte*8));Hash*=1099511628211ull;} + }; + Add(static_cast(Key.Coord.X),4);Add(static_cast(Key.Coord.Y),4); + Add(static_cast(Key.Coord.Z),4);Add(static_cast(Key.Level),4); + Add(Connector,4);Add(static_cast(Station),4);Add(static_cast(Direction),4); + return Hash?Hash:1; +} + +inline FFarConnectorCoverageAudit AuditFarConnectorCoverage(const FStandaloneShapeWorld& Shape, + const FVoxelTileKey& Key,int32 Step,int32 Cells,const FVoxelMeshData& Mesh) +{ + FFarConnectorCoverageAudit Audit; + if(Key.Level<6||Step<=0||Mesh.Triangles.Num()<3)return Audit; + const FIntVector TileOrigin=Key.OriginVoxels(); + const int32 TileExtent=Key.ExtentVoxels(); + FFarMeshSegmentIndex MeshIndex(Mesh,TileOrigin,TileExtent,Cells,Step); + const auto& Connectors=Shape.GetFarConnectorSegments(); + std::vector CandidateIndices; + Shape.FindFarConnectorCandidates( + FIntVector(TileOrigin.X-64,TileOrigin.Y-64,TileOrigin.Z-64), + FIntVector(TileOrigin.X+TileExtent+64,TileOrigin.Y+TileExtent+64,TileOrigin.Z+TileExtent+64), + CandidateIndices); + for(const uint32 ConnectorIndex:CandidateIndices) + { + if(ConnectorIndex>=Connectors.size())continue; + const auto& Connector=Connectors[ConnectorIndex]; + const FVector Delta=Connector.End-Connector.Start; + const double Length=std::sqrt(Delta.X*Delta.X+Delta.Y*Delta.Y+Delta.Z*Delta.Z); + if(Length<=1.0e-6)continue; + const FVector Axis=Delta/Length; + FVector U=FVector::CrossProduct(Axis,FVector::UpVector); + if(U.SizeSquared()<0.01)U=FVector::CrossProduct(Axis,FVector::RightVector); + U.Normalize(); + FVector V=FVector::CrossProduct(Axis,U);V.Normalize(); + const FVector Directions[4]={U,U*-1.0,V,V*-1.0}; + const double Deltas[3]={Delta.X,Delta.Y,Delta.Z}; + const double Starts[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z}; + const double Low[3]={TileOrigin.X-Connector.Radius,TileOrigin.Y-Connector.Radius, + TileOrigin.Z-Connector.Radius}; + const double High[3]={TileOrigin.X+TileExtent+Connector.Radius, + TileOrigin.Y+TileExtent+Connector.Radius,TileOrigin.Z+TileExtent+Connector.Radius}; + double TMin=0.0,TMax=1.0; + bool bIntersects=true; + for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex) + { + if(std::abs(Deltas[AxisIndex])<1.0e-9) + {if(Starts[AxisIndex]High[AxisIndex])bIntersects=false;continue;} + double A=(Low[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex]; + double B=(High[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex]; + if(A>B)std::swap(A,B); + TMin=std::max(TMin,A);TMax=std::min(TMax,B); + if(TMin>TMax){bIntersects=false;break;} + } + if(!bIntersects||TMax-TMin<1.0e-6)continue; + const double StationStep=Key.Level>=7?std::max(256.0,static_cast(Step)):128.0; + const int32 StationCount=std::max(1,static_cast(std::ceil(Length/StationStep))); + const double RayLength=Connector.Radius*2.0+32.0; + const double MatchTolerance=std::max(8.0,static_cast(Connector.Radius)*0.35); + const double DetectionTolerance=MatchTolerance*0.98; + const double SourceRadiusBand=std::max(16.0,static_cast(Connector.Radius)*0.75); + for(int32 Station=0;Station(Station)+0.5)/StationCount); + if(!ContainsFarTilePoint(Key,Center))continue; + for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) + { + if(!ContainsFarTilePoint(Key,Center+Directions[DirectionIndex]*RayLength))continue; + double SourceRadius=0.0,Uncertainty=0.0; + const bool bSource=FindFarConnectorRadiusNearNominal(Shape,Center, + Directions[DirectionIndex],Connector.Radius,SourceRadius,Uncertainty) + ||FindFarConnectorSurfaceRadius(Shape,Center,Directions[DirectionIndex], + Connector.Radius,SourceRadius); + const double MeshDifference=MeshIndex.NearestHitDifference(Center, + Directions[DirectionIndex],RayLength,bSource?SourceRadius:Connector.Radius); + if(bSource&&std::abs(SourceRadius-Connector.Radius)<=SourceRadiusBand) + { + ++Audit.Rays; + if(!std::isfinite(MeshDifference)||MeshDifference>DetectionTolerance) + Audit.HoleIds.push_back(FarConnectorAuditRayId(Key,ConnectorIndex,Station,DirectionIndex)); + } + else if(std::isfinite(MeshDifference)&&MeshDifference<=DetectionTolerance) + Audit.FloatingIds.push_back(FarConnectorAuditRayId(Key,ConnectorIndex,Station,DirectionIndex)); + } + } + } + Audit.Holes=Audit.HoleIds.size(); + Audit.Floating=Audit.FloatingIds.size(); + return Audit; +} + struct FFarMeshColumnIndex { FFarMeshColumnIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent, diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 1952e96..6f27997 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -343,7 +343,7 @@ namespace VoxelForgeCore FNativeCapRefinementStats RefineFarNativeGround(const FStandaloneOwnerWorld& Owner, const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,int32 MeshStepVoxels, - int32 InteriorStepOverride,FVoxelMeshData& Mesh) + int32 InteriorStepOverride,int32 RefinementGridScale,FVoxelMeshData& Mesh) { FNativeCapRefinementStats Stats; if(Key.Level<5||!Owner.Generator)return Stats; @@ -354,11 +354,12 @@ namespace VoxelForgeCore const int32 RequestedStep=Key.Level==8 ?(InteriorStepOverride>0?InteriorStepOverride:MeshStepVoxels) :std::min(BoundaryStepVoxels,1< GridCoordinates= BuildNativeHorizonGridCoordinates(TileExtent,GridStepVoxels); @@ -644,7 +645,7 @@ namespace VoxelForgeCore FNativeCapRefinementStats RefineFarNativeCap(const FStandaloneOwnerWorld& Owner, const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,int32 MeshStepVoxels, - int32 InteriorStepOverride,FVoxelMeshData& Mesh) + int32 InteriorStepOverride,int32 RefinementGridScale,FVoxelMeshData& Mesh) { FNativeCapRefinementStats Stats; if(Key.Level<4||!Owner.Generator)return Stats; @@ -654,11 +655,12 @@ namespace VoxelForgeCore :(Key.Level<5?std::min(BoundaryStepVoxels,1< GridCoordinates= @@ -1606,6 +1608,37 @@ namespace VoxelForgeCore options.legacyDirectMappedColumnCache||key.lod<=2); if(!impl_||!impl_->owner) { error="resolved world snapshot is empty";return std::nullopt; } + const WorldAssemblyRecipeV1* FarRecipe=impl_->worldAssembly?&impl_->worldAssembly->recipe:nullptr; + std::uint32_t FarDisplayBudget=FarRecipe?FarRecipe->farDisplayGeometryBudgetMillipixels:0; + std::uint32_t FarRefinementScale=FarRecipe?FarRecipe->farRefinementGridScale:0; + std::uint32_t FarRefinementLodMask=FarRecipe?FarRecipe->farRefinementLodMask:0; + std::uint32_t FarConnectorStationScale=FarRecipe?FarRecipe->farConnectorStationScale:0; + std::uint32_t FarConnectorRayMask=FarRecipe?FarRecipe->farConnectorRayMask:0; + bool bFarConnectorReuseStationRadii=FarRecipe&&FarRecipe->farConnectorReuseStationRadii; + if(options.overrideFarDisplayMesh) + { + FarDisplayBudget=options.farDisplayGeometryBudgetMillipixels; + FarRefinementScale=options.farRefinementGridScale; + FarRefinementLodMask=options.farRefinementLodMask; + FarConnectorStationScale=options.farConnectorStationScale; + FarConnectorRayMask=options.farConnectorRayMask; + bFarConnectorReuseStationRadii=options.farConnectorReuseStationRadii; + } + const auto ValidFarScale=[](std::uint32_t Value) + {return Value==0||Value==2||Value==4||Value==8;}; + if((FarDisplayBudget!=0&&FarDisplayBudget!=1000) + ||!ValidFarScale(FarRefinementScale) + ||!ValidFarScale(FarConnectorStationScale) + ||(FarRefinementLodMask&~0x1f0u)!=0||FarConnectorRayMask>0x0fu + ||(FarRefinementScale!=0&&FarRefinementLodMask==0)) + {error="far display mesh override is outside the recipe's supported 1 px settings";return std::nullopt;} + const std::uint32_t ActiveFarRefinementScale=FarDisplayBudget==1000 + &&(FarRefinementLodMask&(1u<(FarConnectorStationScale):1; + const uint32 ActiveConnectorRayMask=FarDisplayBudget==1000 + &&FarConnectorRayMask!=0?FarConnectorRayMask:0x0fu; const bool bShapeReady=impl_->shapeReady.load(std::memory_order_acquire); const bool bNativeOnly=!bShapeReady||options.diagnosticForceNativeOnly; if(!bShapeReady&&!options.diagnosticForceNativeOnly&&!isTileShapeFree(key)) @@ -2135,7 +2168,10 @@ namespace VoxelForgeCore BeforeConnectorColumnComputeMilliseconds=Columns.columnComputeMilliseconds; } const auto ConnectorStart=std::chrono::steady_clock::now(); - AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh); + if(!options.diagnosticRemoveFarConnectorPanels) + AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh, + ActiveConnectorStationScale,ActiveConnectorRayMask, + FarDisplayBudget==1000&&bFarConnectorReuseStationRadii); const auto AfterConnector=FStandaloneShapeWorld::GetThreadClassificationStats(); out.diagnostics.farConnectorShellMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-ConnectorStart).count(); @@ -2161,6 +2197,16 @@ namespace VoxelForgeCore } out.diagnostics.farShellTrianglesAdded+=static_cast( std::max(0,mesh.Triangles.Num()/3-BeforeConnectorTriangles)); + if(options.diagnosticAuditFarConnectorCoverage) + { + const auto Audit=AuditFarConnectorCoverage(farShape,nativeKey,step, + world.CoarseTileCells,mesh); + out.diagnostics.farConnectorAuditRays=Audit.Rays; + out.diagnostics.farConnectorAuditHoles=Audit.Holes; + out.diagnostics.farConnectorAuditFloating=Audit.Floating; + out.diagnostics.farConnectorAuditHoleIds=Audit.HoleIds; + out.diagnostics.farConnectorAuditFloatingIds=Audit.FloatingIds; + } } out.diagnostics.farShellMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-FarShellStart).count(); @@ -2168,12 +2214,14 @@ namespace VoxelForgeCore { const auto HorizonStart=std::chrono::steady_clock::now(); FNativeCapRefinementStats GroundStats; + const int32 ActiveRefinementScale=ActiveFarRefinementScale>1 + ?static_cast(ActiveFarRefinementScale):1; const double ColumnMillisecondsBeforeGround= VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; const auto GroundStart=std::chrono::steady_clock::now(); if(key.lod>=4) GroundStats=RefineFarNativeGround(world,*impl_->worldAssembly,nativeKey,step, - options.lod8HorizonInteriorStepVoxels,mesh); + options.lod8HorizonInteriorStepVoxels,ActiveRefinementScale,mesh); const double GroundWallMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-GroundStart).count(); const double ColumnMillisecondsAfterGround= @@ -2186,7 +2234,7 @@ namespace VoxelForgeCore VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; const auto CapStart=std::chrono::steady_clock::now(); const auto CapStats=RefineFarNativeCap(world,*impl_->worldAssembly,nativeKey,step, - options.lod8HorizonInteriorStepVoxels,mesh); + options.lod8HorizonInteriorStepVoxels,ActiveRefinementScale,mesh); const double CapWallMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-CapStart).count(); const double ColumnMillisecondsAfterCap= diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h index 23175d0..1b4bbbe 100644 --- a/Standalone/Core/src/WorldAssemblerInternal.h +++ b/Standalone/Core/src/WorldAssemblerInternal.h @@ -244,6 +244,24 @@ namespace world_assembly_detail || recipe.biomeBorderBlendSubvoxels < 0 || recipe.biomeBorderBlendSubvoxels > recipe.biomeCellSizeSubvoxels) { error = "world assembly biome-map parameters are outside the supported fixed-scale range"; return false; } + if (recipe.evaluatorVersion >= 7) + { + constexpr std::uint32_t FarLodMask = 0x1f0u; // LOD4..8. + const bool anyFarSetting = recipe.farDisplayGeometryBudgetMillipixels != 0 + || recipe.farRefinementGridScale != 0 || recipe.farRefinementLodMask != 0 + || recipe.farConnectorStationScale != 0 || recipe.farConnectorRayMask != 0 + || recipe.farConnectorReuseStationRadii; + const auto validScale = [](std::uint32_t value) + { return value == 0 || value == 2 || value == 4 || value == 8; }; + if ((anyFarSetting && recipe.farDisplayGeometryBudgetMillipixels != 1000) + || recipe.farDisplayGeometryBudgetMillipixels > 1000 + || !validScale(recipe.farRefinementGridScale) + || (recipe.farRefinementLodMask & ~FarLodMask) != 0 + || !validScale(recipe.farConnectorStationScale) + || recipe.farConnectorRayMask > 0x0fu + || (recipe.farRefinementGridScale != 0 && recipe.farRefinementLodMask == 0)) + { error = "far display mesh declaration must use the 1 px budget and supported LOD, scale, and ray values"; return false; } + } std::vector ids; ids.reserve(recipe.biomes.size()); for (const auto& biome : recipe.biomes) @@ -431,6 +449,16 @@ namespace world_assembly_detail HashWord(hash, static_cast(recipe.biomeClimateMoistureWavelengthSubvoxels), 8); HashWord(hash, static_cast(recipe.biomeClimateReliefContrastQ16), 8); } + if (recipe.evaluatorVersion >= 7) + { + HashWord(hash, 0x4641524449535037ull, 8); + HashWord(hash, recipe.farDisplayGeometryBudgetMillipixels, 4); + HashWord(hash, recipe.farRefinementGridScale, 4); + HashWord(hash, recipe.farRefinementLodMask, 4); + HashWord(hash, recipe.farConnectorStationScale, 4); + HashWord(hash, recipe.farConnectorRayMask, 4); + HashWord(hash, recipe.farConnectorReuseStationRadii ? 1u : 0u, 1); + } HashWord(hash, recipe.biomes.size(), 4); for (const auto& biome : recipe.biomes) { @@ -641,6 +669,16 @@ namespace world_assembly_detail || !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels) || !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16))) { error = "world assembly recipe is missing evaluator-2 climate parameters"; return false; } + if (recipe.evaluatorVersion >= 7 + && (!ReadU32(value.Get("far_display_budget_millipixels"), recipe.farDisplayGeometryBudgetMillipixels) + || !ReadU32(value.Get("far_refinement_grid_scale"), recipe.farRefinementGridScale) + || !ReadU32(value.Get("far_refinement_lod_mask"), recipe.farRefinementLodMask) + || !ReadU32(value.Get("far_connector_station_scale"), recipe.farConnectorStationScale) + || !ReadU32(value.Get("far_connector_ray_mask"), recipe.farConnectorRayMask) + || !std::holds_alternative(value.Get("far_connector_reuse_station_radii").Data))) + { error = "world assembly recipe is missing evaluator-7 far display mesh parameters"; return false; } + if (recipe.evaluatorVersion >= 7) + recipe.farConnectorReuseStationRadii = std::get(value.Get("far_connector_reuse_station_radii").Data); for (const auto& item : value.Get("biomes").AsArray()) { WorldAssemblyBiomeProfileV1 biome; @@ -702,6 +740,14 @@ namespace world_assembly_detail out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels << ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels << ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16; + if (recipe.evaluatorVersion >= 7) + out << ",\"far_display_budget_millipixels\":" << recipe.farDisplayGeometryBudgetMillipixels + << ",\"far_refinement_grid_scale\":" << recipe.farRefinementGridScale + << ",\"far_refinement_lod_mask\":" << recipe.farRefinementLodMask + << ",\"far_connector_station_scale\":" << recipe.farConnectorStationScale + << ",\"far_connector_ray_mask\":" << recipe.farConnectorRayMask + << ",\"far_connector_reuse_station_radii\":" + << (recipe.farConnectorReuseStationRadii ? "true" : "false"); out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":["; for (std::size_t i = 0; i < recipe.biomes.size(); ++i) { diff --git a/Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md new file mode 100644 index 0000000..d17d8a4 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md @@ -0,0 +1,195 @@ +# Linux15 report + +## Result + +Null result. No far-mesh shortcut passed the full 1 px, shading, swap, and topology gates, so no +recipe enables one by default. Evaluator version 6→7 adds the optional `far_display_mesh` declaration; +omitting it keeps the L14 output path. The controls are in the shared Core layer and do not touch +LOD0–2, exact queries, collision, edits, or server state. + +Candidates measured: + +- Refinement grid scale ×2 at LOD4, 5, 6, 7, and 8. The all-tile LOD4 and LOD5 runs reject; the + remaining per-LOD probes exceed 1 px or change shading, and the LOD6–8 candidates worsen at + least one adjacent swap. +- Reusing connector station radii leaves geometry p99 at 1.07 px but reaches 15.23 px maximum; + shading delta reaches 0.667 and the normal changes by 73.3°. +- Coarsening refinement at LOD4–8 is the largest measured saving, but exceeds the display budget. + Halving connector station density also saves time but opens gaps. Removing + any one ray direction also opens gaps, and the removed-panel control finds 99 new holes. + +## Screen-space gate + +The gate samples 65×65 surface grids across all 1,145 displayed LOD3–8 tiles (5,208,134 samples; +skirts excluded). Geometry is projected normal displacement against today's same-LOD mesh at its +closest display distance, using 90° horizontal FOV and 2,560 px (0.000613592 rad/px). Shading delta +is the maximum of fixed-light normalized Lambert change and material selector/blend-channel change; +the limit is 1/255. Each number below is p50/p99/max. Exact-path baseline values are all zero. +The gate rejects geometry max above 1 px, shading max above 1/255, any worse per-sample swap, +missing surface, or new hole/crack/floater. Its own validation fails if S13, stride-2, missing +surface, floating surface, missing fine-ring, or removed-panel controls are not rejected; the +panel control must expose at least one new connector hole. + +| LOD | Tiles | Geometry px: baseline → refine×2 all | Shading delta: baseline → refine×2 all | Refine×2 holes / cracks / floating | +|---:|---:|---:|---:|---:| +| 3 | 384 | 0/0/0 → 0/0/0 | 0/0/0 → 0/0/0 | 0 / 0 / 0 | +| 4 | 320 | 0/0/0 → 0/2.370/606.562 | 0/0/0 → 0/0.023529/1 | 254 / 400 / 254 | +| 5 | 144 | 0/0/0 → 0/12.728/95.453 | 0/0/0 → 0/1/1 | 1,183 / 76 / 138 | +| 6 | 96 | 0/0/0 → 0.023/13.408/799.596 | 0/0/0 → 0/1/1 | 476 / 0 / 201 | +| 7 | 48 | 0/0/0 → 0/13.242/248.905 | 0/0/0 → 0/1/1 | 30 / 0 / 1 | +| 8 | 153 | 0/0/0 → 0.158/19.250/187.465 | 0/0/0 → 0.003922/1/1 | 45 / 0 / 946 | + +The selected-patch swap report compares each baseline with refine×2 all. Values are p50/p99/max px. + +| Swap (distance) | Before | After refine×2 all | +|---|---:|---:| +| 3→2 (96 m) | 0.611/3.767/6.045 | 0.611/3.767/6.045 | +| 4→3 (192 m) | 0.752/4.630/7.513 | 0.752/4.630/7.513 | +| 5→4 (384 m) | 0/9.580/31.783 | 2.435/32.321/86.615 | +| 6→5 (768 m) | 0/9.754/70.340 | 0.343/19.142/78.077 | +| 7→6 (1,536 m) | 0/9.040/42.355 | 0.200/15.109/50.994 | +| 8→7 (3,072 m) | 0.826/18.030/47.182 | 1.528/24.547/64.449 | + +The station×2 LOD6 screen scan measures 0/2/813.571 px, shading 0/0/1, with 1,349 holes and 1,690 +floaters. Its connector audit counts missing ray stations by stable tile/ray/station ID: holes 8→221 +(216 new), floating classifications 464→405 (111 new IDs). In the refine×2 all row above, the +screen scan reports those all-tile holes, cracks, and floaters. Station×2 leaves all six selected-patch +swap p50/p99/max rows at baseline values, including 6→5 at 0/9.754/70.340 px. All four ray directions are required: +the four one-ray-removed +controls create 1,008, 1,022, 1,171, and 1,158 new holes. The explicit panel-removal control creates +99 new holes. The exact-path audit has zero new holes, cracks, or floating pieces; its raw connector +audit counts remain 8 holes and 464 pre-existing floating classifications. + +The LOD4×2 and LOD5×2 candidates also fail in isolation. Reusing station radii misses the LOD6 +geometry and shading limits. The four one-ray-removed masks add 1,008, 1,022, 1,171, and 1,158 +connector holes, respectively. + +## Negative controls + +- The S13 global-octave-drop and stride-2 column-interpolation controls are rejected at every LOD3–8. +- Missing displayed surface, a floating surface, and missing fine-ring surface controls are rejected. +- Removing connector panels is rejected with 99 new holes. The exact connector path has zero new + station IDs classified as holes, cracks, or floaters. + +## Speed + +Five interleaved W12 pairs per candidate, hashing off, same 2,553-tile L1 view. +`whole_view_ready` is the metric. Both candidates fail the quality gate. + +Station×2 connector stations: + +| Pair | L14 baseline | Station×2 | Change | +|---:|---:|---:|---:| +| 1 | 1.320 s | 1.304 s | −1.2% | +| 2 | 1.363 s | 1.330 s | −2.4% | +| 3 | 1.371 s | 1.332 s | −2.8% | +| 4 | 1.393 s | 1.341 s | −3.7% | +| 5 | 1.391 s | 1.345 s | −3.3% | +| Median | 1.371 s | 1.332 s | −2.8% (3.3% median paired gain) | + +Refinement grid scale ×2 at LOD4–8: + +| Pair | L14 baseline | Refine×2 all | Change | +|---:|---:|---:|---:| +| 1 | 1.305 s | 1.164 s | −10.8% | +| 2 | 1.336 s | 1.177 s | −11.9% | +| 3 | 1.362 s | 1.169 s | −14.2% | +| 4 | 1.378 s | 1.178 s | −14.5% | +| 5 | 1.384 s | 1.190 s | −14.0% | +| Median | 1.362 s | 1.177 s | −13.6% (14.0% median paired gain) | + +## Hashes and invariants + +The undeclared/default recipe path matches L14. LOD0–2 density/mesh/payload/topology hashes: + +| LOD | Density | Mesh | Payload | Topology | +|---:|---|---|---|---| +| 0 | `769f2a706d585a72` | `33eeeb39fa443816` | `645621a1d968983f` | `c2457ed905036c9d` | +| 1 | `69cc5a5c0a2c60af` | `b48335f5410c57d5` | `d15c5480162ab8ba` | `feca1bd025823604` | +| 2 | `85deed5683a77202` | `53469895987aeea2` | `e55f8ae7e1532033` | `d5feb12a0e85c09b` | + +Exact queries (density / unclamped / detail / collision) remain +`e4d4a8eb95b35933 / e4d4a8eb95b35933 / efcba20c0c70c415 / e604823a249029bf`. The 24 m crossing's +432 moved tiles match L14 at W1/W12 with Clang off and GCC default: +`337ca62131576728 / 7612f48e2ddbbaab / 69a67ed4add83b87 / f11ac1ebd0f7ddb8` (D/M/P/T). +Its LOD0–2 per-LOD hashes also match the three rows above. + +Determinism passed 28/28 configurations: SurfaceWorld seed 0 and FloatingIslands seed 0, Clang 19 +FP-contract off and GCC 14 default, W1/W8/W12 and five W12 repeats. All per-LOD and whole-view +hashes match L14. L13 route parity passed for SurfaceWorld seeds 0, 1, and 17 and FloatingIslands +seed 0; page, native-only, higher-LOD, edit, and history mismatch counts are zero. + +The L3 FP-contract preamble/self-test, L4 `FLazy`, L7 shared cache, L9/L12 proofs, L11 readiness and +crash fix, L13 route binding, and CLAUDE.md async rules remain in place. The evaluator and optional +recipe declaration are shared by all recipes. Builds and runs used CPUs 0–11, up to W12, normal +priority, 6 GiB address-space limits, and the unchanged `powersave` governor. Peak RSS was 904,400 KB +(GCC build); peak runtime RSS was 736,432 KB. No run exceeded the time or memory limits. + +## Capture pairs + +JPEGs are 1280×720 and each is under 74 KB. Before is L14; after is the rejected refinement×2 +candidate applied at LOD4–8. +The L1 view uses `(0,-35,-487.94) m`, yaw 0°, pitch −22°. Horizon uses the same location at pitch −2°. +The mesa frame aims from the L1 location at the L6 mesa band (`(-472,-1432,-463.151) m` in the +gate) with yaw −161° and pitch 0.9°. + +| View | Before | After refine×2 all | +|---|---|---| +| L1 camera | [l1-camera-before.jpg](captures/l1-camera-before.jpg) (59 KB) | [l1-camera-after-refineall.jpg](captures/l1-camera-after-refineall.jpg) (57 KB) | +| Horizon | [horizon-before.jpg](captures/horizon-before.jpg) (63 KB) | [horizon-after-refineall.jpg](captures/horizon-after-refineall.jpg) (60 KB) | +| LOD6 mesa rim | [lod6-mesa-rim-before.jpg](captures/lod6-mesa-rim-before.jpg) (74 KB) | [lod6-mesa-rim-after-refineall.jpg](captures/lod6-mesa-rim-after-refineall.jpg) (67 KB) | + +## Failed and retried runs + +- The first Clang build wrapper exited 127 because `/usr/bin/time` is absent; the direct Clang + `-ffp-contract=off` build succeeded. GCC 14 default also built successfully. +- A later sequential-build monitor was interrupted after Clang completed and before its GCC step + launched. Clang and GCC were rebuilt separately with the same compiler settings and 6 GiB cap. +- The first hash matrix omitted `--seed=0` and was discarded. A second FloatingIslands matrix used + the SurfaceWorld spawn camera; it was rerun with the L14 auto-surface camera. The corrected matrix + passed all 28 configurations. +- Initial SurfaceWorld route probes used no camera for seed 0 (the 2,370-tile bound), omitted + `--region-crossing` for seed 0 (shape compiler safety bound), and used the fixed seed-0 camera for + seed 1 (LOD2/exact-query parity mismatch; seed 17 was not run with that setup). They were rerun + with L14's camera selection and region-crossing option; all four final route audits passed. The + first 24 m crossing also omitted `--region-crossing` and exceeded the shape-build safety bound; + all four corrected compiler/worker combinations passed. +- The first connector audit compared aggregate counts. It was replaced with stable per-ray station + ID comparison before the final gate. The initial L4 selected-tile gate missed a changed tile; the + all-desired-tile coverage gate found and rejected it. The full-view gate is used in the table above. +- The first JPEG encoding pass did not set CPU affinity or an address-space cap. All six files were + re-encoded under CPUs 0–11 and the 6 GiB cap; sizes and pixels stayed the same. +- The first five speed pairs included screen-gate color capture on ordinary streaming tiles. That + diagnostic copy is now enabled only for gate runs; all five cost pairs were rerun and the table + above uses only the final logs. The final exact gate passed and station×2 remained rejected. +- No builds, captures, final audits, or cost runs timed out or breached their memory limits. The + intentional candidate rejections are listed above, not treated as infrastructure failures. + +Evidence logs are in `Saved/Linux`: the final baseline and station×2 all-tile gates are +`l15-final-gatecolors-exact.log` and `l15-final-gatecolors-station2.log`; the best-payoff +refinement×2 all-tile gate is `l15-final-gatecolors-refineall.log`. Single-LOD refinement coverage is in +`l15-gate-alltiles-refine-l4x2.log` and `l15-gate-alltiles-refine-l5x2.log`. Single-LOD refinement +and connector-ray probes are `l15-gate-refine-l{x}x2.log`, `l15-gate-reuse.log`, and +`l15-gate-rays{14,13,11,7}.log`. Hash, route, and crossing evidence is in +`l15-final-hash-audit.txt`, `l15-final-route-*.log`, and `l15-final-crossing-24m-*.log`; final +station×2 speed pairs are `l15-final-speed-pair{1..5}-{before,after}.log`; refinement×2 pairs are +`l15-final-speed-refineall-pair{1..5}-{before,after}.log`. + +## Note for the owner + +The default far terrain stays the same because every measured reduction exceeded the 1 px geometry +or shading limit, worsened a swap, or opened holes. Refinement×2 across LOD4–8 saves 13.6% by the +median whole-view ratio, but creates hundreds-pixel errors and visible gaps, so the captures show it +for review only and it is not enabled. + +## Orchestrator review (Claude) + +- I looked at the LOD6 mesa-rim pair. After refine×2, the jagged crests are visibly smoothed, black + holes appear in the cap/ceiling, and a dark sliver shows on the left horizon. That confirms the + gate's rejection: the instrument is right this time, not the world. +- **Not merged into `assembler/world`:** the candidate knobs and the evaluator 6→7 bump add no default + output and would invalidate version stamps for nothing. The full L15 code (the extended gate with + shading, swap and connector-ID audits, and the candidate controls) is kept on side branch + `wip/l15-far-budget`. `assembler/world` stays at L14 code plus this report. +- Conclusion of L14+L15: far terrain has no exact or within-budget saving left with today's design. The + remaining far cost is the price of the current refinement and connector-repair geometry. diff --git a/Standalone/Research/WorldAssemblerM3/Linux15/captures/horizon-after-refineall.jpg b/Standalone/Research/WorldAssemblerM3/Linux15/captures/horizon-after-refineall.jpg new file mode 100644 index 0000000..e4e36c4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux15/captures/horizon-after-refineall.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux15/captures/horizon-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux15/captures/horizon-before.jpg new file mode 100644 index 0000000..37784a6 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux15/captures/horizon-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux15/captures/l1-camera-after-refineall.jpg b/Standalone/Research/WorldAssemblerM3/Linux15/captures/l1-camera-after-refineall.jpg new file mode 100644 index 0000000..f01fc92 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux15/captures/l1-camera-after-refineall.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux15/captures/l1-camera-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux15/captures/l1-camera-before.jpg new file mode 100644 index 0000000..0622003 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux15/captures/l1-camera-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux15/captures/lod6-mesa-rim-after-refineall.jpg b/Standalone/Research/WorldAssemblerM3/Linux15/captures/lod6-mesa-rim-after-refineall.jpg new file mode 100644 index 0000000..0fc76dd Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux15/captures/lod6-mesa-rim-after-refineall.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux15/captures/lod6-mesa-rim-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux15/captures/lod6-mesa-rim-before.jpg new file mode 100644 index 0000000..baae9d2 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux15/captures/lod6-mesa-rim-before.jpg differ diff --git a/Standalone/Source/LinuxScreenSpaceGate.h b/Standalone/Source/LinuxScreenSpaceGate.h index 10c56b0..e0d6f7f 100644 --- a/Standalone/Source/LinuxScreenSpaceGate.h +++ b/Standalone/Source/LinuxScreenSpaceGate.h @@ -42,6 +42,7 @@ namespace StandaloneScreenSpaceGate double MaximumSlopeDegrees=0.0; std::array SlopeSamples{}; std::array SlopeP99{},SlopeMaximum{}; + std::vector SamplePixels; bool Valid=false; }; @@ -52,6 +53,14 @@ namespace StandaloneScreenSpaceGate double SlopeY=0.0; }; + struct FMeshSurfaceHit + { + double Z=0.0; + std::array Normal{}; + std::array Color{}; + bool Ceiling=false; + }; + struct FViewBasis { std::array Right{}; @@ -162,7 +171,7 @@ namespace StandaloneScreenSpaceGate { int Side=65; double MinimumX=0.0,MaximumX=0.0,MinimumY=0.0,MaximumY=0.0; - std::vector> Hits; + std::vector> Hits; FHeightGrid()=default; FHeightGrid(int InSide,double MinX,double MaxX,double MinY,double MaxY) @@ -175,13 +184,13 @@ namespace StandaloneScreenSpaceGate double Y(int Iy) const {return MinimumY+(MaximumY-MinimumY)*Iy/(Side-1);} - void Add(int Ix,int Iy,double Z) + void Add(int Ix,int Iy,const FMeshSurfaceHit& Hit) { auto& Values=Hits[static_cast(Iy)*Side+Ix]; - Values.push_back(Z); + Values.push_back(Hit); } - const std::vector& At(int Ix,int Iy) const + const std::vector& At(int Ix,int Iy) const {return Hits[static_cast(Iy)*Side+Ix];} }; @@ -246,6 +255,8 @@ namespace StandaloneScreenSpaceGate { if(!Tile||Tile->Indices.size()<3)continue; const std::size_t TriangleCount=Tile->Indices.size()/3; + const std::size_t CeilingStart=TriangleCount + -std::min(TriangleCount,Tile->CeilingTriangleCount); for(std::size_t Triangle=0;TriangleTriangleSkirtFaceMasks.size() @@ -274,15 +285,33 @@ namespace StandaloneScreenSpaceGate const double W1=((C[1]-A[1])*(X-C[0])+(A[0]-C[0])*(Y-C[1]))/Denominator; const double W2=1.0-W0-W1; if(W0<-1e-6||W1<-1e-6||W2<-1e-6)continue; - Grid.Add(Ix,Iy,W0*A[2]+W1*B[2]+W2*C[2]); + const auto& VA=Tile->Vertices[I0]; + const auto& VB=Tile->Vertices[I1]; + const auto& VC=Tile->Vertices[I2]; + std::array Normal{}; + for(int Axis=0;Axis<3;++Axis) + Normal[Axis]=W0*VA.Normal[Axis]+W1*VB.Normal[Axis]+W2*VC.Normal[Axis]; + const double NormalLength=std::sqrt(Normal[0]*Normal[0] + +Normal[1]*Normal[1]+Normal[2]*Normal[2]); + if(NormalLength>1.0e-12) + for(double& Component:Normal)Component/=NormalLength; + std::array Color{}; + if(Tile->VertexColors.size()==Tile->Vertices.size()) + for(int Channel=0;Channel<4;++Channel) + Color[Channel]=static_cast(std::clamp(std::lround( + W0*Tile->VertexColors[I0][Channel] + +W1*Tile->VertexColors[I1][Channel] + +W2*Tile->VertexColors[I2][Channel]),0l,255l)); + Grid.Add(Ix,Iy,{W0*A[2]+W1*B[2]+W2*C[2],Normal,Color,Triangle>=CeilingStart}); } } } for(auto& Values:Grid.Hits) { - std::sort(Values.begin(),Values.end()); - Values.erase(std::unique(Values.begin(),Values.end(),[](double A,double B) - {return std::abs(A-B)<0.02;}),Values.end()); + std::stable_sort(Values.begin(),Values.end(),[](const FMeshSurfaceHit& A, + const FMeshSurfaceHit& B){return A.Z& Hits,const std::vector& Used, + inline bool MatchNearest(const std::vector& Hits,const std::vector& Used, double Root,std::size_t& OutIndex,double& OutError) { double Best=std::numeric_limits::infinity();std::size_t BestIndex=Hits.size(); for(std::size_t I=0;I=Used.size()||!Used[I]) { - const double Error=std::abs(Hits[I]-Root); + const double Error=std::abs(Hits[I].Z-Root); if(Error UsedReference(Reference.size(),false),UsedMesh(Mesh.size(),false); for(std::size_t ReferenceIndex=0;ReferenceIndexZMeters-ReferenceZ)<1.0) Root=*BestRoot; } - AddError(Accumulator,Mesh[MeshIndex]-Reference[ReferenceIndex], + AddError(Accumulator,Mesh[MeshIndex].Z-Reference[ReferenceIndex].Z, Root,PixelBudgetMeters,View,ReferenceGrid.X(Ix),ReferenceGrid.Y(Iy)); } for(std::size_t I=0;I GeometryPixels,ShadeValues,NormalAngles; + bool Valid=false; + }; + + inline std::array UnitNormal(const FMeshSurfaceHit& Hit) + { + std::array N=Hit.Normal; + const double Length=std::sqrt(N[0]*N[0]+N[1]*N[1]+N[2]*N[2]); + if(Length<=1.0e-12)return {0.0,0.0,Hit.Ceiling?-1.0:1.0}; + for(double& Component:N)Component/=Length; + return N; + } + + inline FMeshComparisonRow CompareMeshes(const FHeightGrid& Reference, + const FHeightGrid& Candidate,const FVoxelTileKey& Patch,double VoxelMeters, + double PixelBudgetMeters,const FViewBasis& View) + { + FMeshComparisonRow Row;Row.Valid=Reference.Side==Candidate.Side; + if(!Row.Valid)return Row; + FErrorAccumulator Geometry; + std::vector ShadeValues,NormalAngles; + const std::size_t CellCount=static_cast(Reference.Side)*Reference.Side; + ShadeValues.reserve(CellCount);NormalAngles.reserve(CellCount); + const double LightLength=std::sqrt(0.3*0.3+(-0.4)*(-0.4)+0.85*0.85); + const std::array Light={0.3/LightLength,-0.4/LightLength,0.85/LightLength}; + for(int Iy=0;Iy Used(After.size(),false); + for(std::size_t I=0;I(A.Color[1])-B.Color[1]), + std::abs(static_cast(A.Color[2])-B.Color[2]))/255.0; + ShadeValues.push_back(std::max(std::abs(DiffuseA-DiffuseB),MaterialDelta)); + const double NX=NA[2]<0.0?NA[0]/std::max(1.0e-12,-NA[2]) + :-NA[0]/std::max(1.0e-12,NA[2]); + const double NY=NA[2]<0.0?NA[1]/std::max(1.0e-12,-NA[2]) + :-NA[1]/std::max(1.0e-12,NA[2]); + AddError(Geometry,B.Z-A.Z,{A.Z,NX,NY},PixelBudgetMeters,View, + Reference.X(Ix),Reference.Y(Iy)); + } + for(std::size_t I=0;I(Iy)*Reference.Side+Ix; + const auto CheckEdge=[&](int NX,int NY) + { + if(NX>=Reference.Side||NY>=Reference.Side)return; + const auto& A=Reference.Hits[Index]; + const auto& B=Reference.At(NX,NY); + const auto& CA=Candidate.Hits[Index]; + const auto& CB=Candidate.At(NX,NY); + if(!A.empty()&&!B.empty()&&(CA.empty()!=CB.empty()))++Row.Cracks; + }; + CheckEdge(Ix+1,Iy);CheckEdge(Ix,Iy+1); + } + Row.Geometry.Lod=Patch.Level;Row.Geometry.Valid=Row.Valid; + Row.Geometry.Missing=Row.Holes;Row.Geometry.Floating=Row.Floating; + FinalizeError(Row.Geometry,Geometry); + Row.ShadeP50=Percentile(ShadeValues,0.50); + Row.ShadeP99=Percentile(ShadeValues,0.99); + Row.ShadeMaximum=ShadeValues.empty()?0.0:*std::max_element(ShadeValues.begin(),ShadeValues.end()); + Row.NormalDegreesP50=Percentile(NormalAngles,0.50); + Row.NormalDegreesP99=Percentile(NormalAngles,0.99); + Row.NormalDegreesMaximum=NormalAngles.empty()?0.0 + :*std::max_element(NormalAngles.begin(),NormalAngles.end()); + Row.GeometryPixels=std::move(Geometry.Pixels); + Row.ShadeValues=std::move(ShadeValues); + Row.NormalAngles=std::move(NormalAngles); + return Row; + } + inline FPopRow ComparePop(const FHeightGrid& Coarse,const FHeightGrid& Fine, const FVoxelTileKey& Patch,double VoxelMeters,double PixelBudgetMeters, const VoxelForgeCore::WorldAssemblyManifest& Assembly,const FViewBasis& View) @@ -452,11 +585,12 @@ namespace StandaloneScreenSpaceGate } CoarseUsed[CoarseIndex]=true; FineUsed[FineIndex]=true; - AddError(Accumulator,CoarseHits[CoarseIndex]-FineHits[FineIndex], + AddError(Accumulator,CoarseHits[CoarseIndex].Z-FineHits[FineIndex].Z, Root,PixelBudgetMeters,View,Coarse.X(Ix),Coarse.Y(Iy)); ++Row.Samples; } } + Row.SamplePixels=Accumulator.Pixels; FErrorRow Stats;Stats.Lod=Patch.Level;Stats.Valid=true; FinalizeError(Stats,Accumulator); Row.Samples=Stats.Samples;Row.P50=Stats.P50;Row.P99=Stats.P99;Row.Maximum=Stats.Maximum; @@ -472,6 +606,16 @@ namespace StandaloneScreenSpaceGate return Row; } + inline bool PopNotWorseAtAnySample(const FPopRow& Before,const FPopRow& After) + { + if(!Before.Valid||!After.Valid||Before.Samples<16 + ||Before.SamplePixels.size()!=After.SamplePixels.size() + ||After.Cracks>Before.Cracks||After.Floating>Before.Floating)return false; + for(std::size_t I=0;IBefore.SamplePixels[I]+1.0e-4)return false; + return true; + } + inline std::array ClosestDisplayedDistancesMeters( const FStandaloneOwnerWorld& World,const FVector& CameraVoxels, std::uint32_t VoxelSizeMicrons) @@ -544,6 +688,7 @@ namespace StandaloneScreenSpaceGate inline bool Run(const std::array,9>& SampleTiles, FStandaloneStreamingGenerator& Streamer, + const std::vector& DesiredKeys, const VoxelForgeCore::WorldAssemblyManifest& Assembly, const std::array& Distances,std::uint32_t VoxelSizeMicrons,int MaxLod, const std::array& PixelBudgetsSubvoxels, @@ -552,17 +697,26 @@ namespace StandaloneScreenSpaceGate const double VoxelMeters=static_cast(VoxelSizeMicrons)/1000000.0; const FViewBasis View=MakeViewBasis(CameraYawDegrees,CameraPitchDegrees); std::array Budgets{}; - std::array ErrorRows{}; - std::array PopRows{}; + VoxelForgeCore::TileGenerationOptions BaselineOptions; + BaselineOptions.overrideFarDisplayMesh=true; + VoxelForgeCore::TileGenerationOptions CandidateOptions; + CandidateOptions.overrideFarDisplayMesh=true; + CandidateOptions.farDisplayGeometryBudgetMillipixels=Assembly.recipe.farDisplayGeometryBudgetMillipixels; + CandidateOptions.farRefinementGridScale=Assembly.recipe.farRefinementGridScale; + CandidateOptions.farRefinementLodMask=Assembly.recipe.farRefinementLodMask; + CandidateOptions.farConnectorStationScale=Assembly.recipe.farConnectorStationScale; + CandidateOptions.farConnectorRayMask=Assembly.recipe.farConnectorRayMask; + CandidateOptions.farConnectorReuseStationRadii=Assembly.recipe.farConnectorReuseStationRadii; bool GatePass=true; - std::printf("[ScreenSpaceGate] angular_pixel=%.9f rad (horizontal_fov=90deg,width=2560) lod>=3_limit=1.0px sampling=65x65 skirts=excluded metric=local_surface_normal_projected field_reference=direct-Heightfield2D-roots normal_probe_vox=1 view_yaw_deg=%.3f view_pitch_deg=%.3f\n", + constexpr double ShadeLimit=1.0/255.0; + std::printf("[ScreenSpaceGate] angular_pixel=%.9f rad (horizontal_fov=90deg,width=2560) lod>=3_geometry_limit=1.0px same_lod_reference=today mesh sampling=65x65 skirts=excluded shading=max(lambert-delta,material-selector-or-blend-delta) shade_limit=1/255 normal_probe_vox=1 view_yaw_deg=%.3f view_pitch_deg=%.3f\n", OnePixelRadians,CameraYawDegrees,CameraPitchDegrees); for(int Lod=0;Lod<=MaxLod&&Lod<9;++Lod) { Budgets[Lod]=Distances[Lod]*OnePixelRadians; std::printf("[ScreenSpaceDistance] lod=%d closest_m=%.3f pixel_budget_m=%.6f pixel_budget_cm=%.3f pixel_budget_subvoxels=%lld rule=%s\n", Lod,Distances[Lod],Budgets[Lod],Budgets[Lod]*100.0, - static_cast(PixelBudgetsSubvoxels[Lod]),Lod<3?"exact-2cm":"1px"); + static_cast(PixelBudgetsSubvoxels[Lod]),Lod<3?"exact-2cm":"same_lod_1px"); } const double LOD3CellMeters=static_cast(CHUNK_SIZE*(1<<3))*VoxelMeters/64.0; PrintKnownFailureControl(LOD3CellMeters,Distances[3],View); @@ -578,115 +732,117 @@ namespace StandaloneScreenSpaceGate const FStandaloneCpuTile* CoarsePtr=&CoarseTile; const std::span CoarseSpan(&CoarsePtr,1); const FHeightGrid CoarseGrid=Rasterize(CoarseSpan,CoarseTile.Key,VoxelMeters); - const FStandaloneCpuTile ExactTile=Streamer.GenerateDiagnostic( - CoarseTile.Key,0,1,true); - const FStandaloneCpuTile* ExactPointer=&ExactTile; - const std::span ExactSpan(&ExactPointer,1); - const FHeightGrid ExactGrid=Rasterize(ExactSpan,ExactTile.Key,VoxelMeters); - ErrorRows[Lod]=CompareToReference(CoarseGrid,ExactGrid,CoarseTile.Key,VoxelMeters, - Budgets[Lod],Assembly,View); - const FErrorRow& Error=ErrorRows[Lod]; + FStandaloneCpuTile BaselineTile=Streamer.GenerateDiagnostic( + CoarseTile.Key,0,1,true,&BaselineOptions,true); + const FStandaloneCpuTile* BaselinePointer=&BaselineTile; + const std::span BaselineSpan(&BaselinePointer,1); + const FHeightGrid BaselineGrid=Rasterize(BaselineSpan,BaselineTile.Key,VoxelMeters); + const FMeshComparisonRow MeshDelta=CompareMeshes(BaselineGrid,CoarseGrid, + CoarseTile.Key,VoxelMeters,Budgets[Lod],View); + const FErrorRow& Error=MeshDelta.Geometry; const bool LodPass=Error.Valid&&Error.Missing==0&&Error.Floating==0 - &&Error.Samples>=16&&Error.Maximum<=1.0; + &&MeshDelta.Cracks==0&&Error.Samples>=16&&Error.Maximum<=1.0 + &&MeshDelta.ShadeMaximum<=ShadeLimit; GatePass=GatePass&&LodPass; - std::printf("[ScreenSpaceGateLOD] lod=%d key=(%d,%d,%d) samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xy_m=(%.3f,%.3f) max_slope_deg=%.2f missing=%llu floating=%llu status=%s\n", + std::printf("[ScreenSpaceGateLOD] lod=%d key=(%d,%d,%d) samples=%llu geometry_px_p50=%.4f p99=%.4f max=%.4f shading_delta_p50=%.6f p99=%.6f max=%.6f normal_deg_p50=%.4f p99=%.4f max=%.4f material_mismatch=%llu holes=%llu cracks=%llu floating=%llu status=%s\n", Lod,CoarseTile.Key.Coord.X,CoarseTile.Key.Coord.Y,CoarseTile.Key.Coord.Z, static_cast(Error.Samples),Error.P50,Error.P99,Error.Maximum, - Error.VerticalP50,Error.VerticalP99,Error.VerticalMaximum, - Error.MaximumNormalDistanceMeters,Error.MaximumX,Error.MaximumY, - Error.MaximumSlopeDegrees, - static_cast(Error.Missing),static_cast(Error.Floating), + MeshDelta.ShadeP50,MeshDelta.ShadeP99,MeshDelta.ShadeMaximum, + MeshDelta.NormalDegreesP50,MeshDelta.NormalDegreesP99,MeshDelta.NormalDegreesMaximum, + static_cast(MeshDelta.MaterialMismatches), + static_cast(MeshDelta.Holes), + static_cast(MeshDelta.Cracks), + static_cast(MeshDelta.Floating), LodPass?"PASS":"REJECT"); - PrintSlopeTail("same_lod",Lod,Error.SlopeSamples,Error.SlopeP99,Error.SlopeMaximum); const FErrorRow FieldError=CompareToExact(CoarseGrid,CoarseTile.Key,VoxelMeters, Budgets[Lod],Assembly,View); const bool FieldPass=FieldError.Valid&&FieldError.Missing==0&&FieldError.Floating==0 &&FieldError.Samples>=16&&FieldError.Maximum<=1.0; - GatePass=GatePass&&FieldPass; - std::printf("[ScreenSpaceGateFieldLOD] lod=%d samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xyz_m=(%.3f,%.3f,%.3f) max_slope_deg=%.2f missing=%llu floating=%llu status=%s\n", + std::printf("[ScreenSpaceGateFieldLOD] lod=%d samples=%llu field_geometry_px_p50=%.4f p99=%.4f max=%.4f missing=%llu floating=%llu status=INFO_ONLY\n", Lod,static_cast(FieldError.Samples),FieldError.P50, - FieldError.P99,FieldError.Maximum,FieldError.VerticalP50,FieldError.VerticalP99, - FieldError.VerticalMaximum,FieldError.MaximumNormalDistanceMeters, - FieldError.MaximumX,FieldError.MaximumY,FieldError.MaximumDisplayedZ, - FieldError.MaximumSlopeDegrees, + FieldError.P99,FieldError.Maximum, static_cast(FieldError.Missing), - static_cast(FieldError.Floating),FieldPass?"PASS":"REJECT"); - PrintSlopeTail("field",Lod,FieldError.SlopeSamples,FieldError.SlopeP99, - FieldError.SlopeMaximum); + static_cast(FieldError.Floating)); bool FaultControlsPass=true; - const FHeightGrid MissingGrid(ExactGrid.Side,ExactGrid.MinimumX,ExactGrid.MaximumX, - ExactGrid.MinimumY,ExactGrid.MaximumY); - const FErrorRow MissingError=CompareToReference(MissingGrid,ExactGrid, - CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View); - const bool MissingRejected=MissingError.Missing>0&&MissingError.Maximum>1.0; + const FHeightGrid MissingGrid(BaselineGrid.Side,BaselineGrid.MinimumX,BaselineGrid.MaximumX, + BaselineGrid.MinimumY,BaselineGrid.MaximumY); + const FMeshComparisonRow MissingError=CompareMeshes(BaselineGrid,MissingGrid, + CoarseTile.Key,VoxelMeters,Budgets[Lod],View); + const bool MissingRejected=MissingError.Holes>0&&MissingError.Geometry.Maximum>1.0; FaultControlsPass=FaultControlsPass&&MissingRejected; - std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-displayed-surface missing=%llu status=%s\n", - Lod,static_cast(MissingError.Missing), + std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-displayed-surface holes=%llu status=%s\n", + Lod,static_cast(MissingError.Holes), MissingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE"); - FStandaloneCpuTile FloatingTile=CoarseTile; + FStandaloneCpuTile FloatingTile=BaselineTile; const std::uint32_t FloatingVertexBase=static_cast(FloatingTile.Vertices.size()); - for(FStandaloneCpuVertex Vertex:CoarseTile.Vertices) + for(FStandaloneCpuVertex Vertex:BaselineTile.Vertices) { Vertex.Position[2]+=2.0f; FloatingTile.Vertices.push_back(Vertex); } - for(const std::uint32_t Index:CoarseTile.Indices) + FloatingTile.VertexColors.insert(FloatingTile.VertexColors.end(), + BaselineTile.VertexColors.begin(),BaselineTile.VertexColors.end()); + for(const std::uint32_t Index:BaselineTile.Indices) FloatingTile.Indices.push_back(FloatingVertexBase+Index); FloatingTile.TriangleSkirtFaceMasks.insert(FloatingTile.TriangleSkirtFaceMasks.end(), - CoarseTile.TriangleSkirtFaceMasks.begin(),CoarseTile.TriangleSkirtFaceMasks.end()); + BaselineTile.TriangleSkirtFaceMasks.begin(),BaselineTile.TriangleSkirtFaceMasks.end()); const FStandaloneCpuTile* FloatingPointer=&FloatingTile; const std::span FloatingSpan(&FloatingPointer,1); - const FHeightGrid FloatingGrid=Rasterize(FloatingSpan,CoarseTile.Key,VoxelMeters); - const FErrorRow FloatingError=CompareToReference(FloatingGrid,ExactGrid, - CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View); - const bool FloatingRejected=FloatingError.Floating>0||FloatingError.Maximum>1.0; + const FHeightGrid FloatingGrid=Rasterize(FloatingSpan,BaselineTile.Key,VoxelMeters); + const FMeshComparisonRow FloatingError=CompareMeshes(BaselineGrid,FloatingGrid, + CoarseTile.Key,VoxelMeters,Budgets[Lod],View); + const bool FloatingRejected=FloatingError.Floating>0||FloatingError.Geometry.Maximum>1.0; FaultControlsPass=FaultControlsPass&&FloatingRejected; std::printf("[ScreenSpaceFaultInjection] lod=%d case=2m-floating-surface floating=%llu max_px=%.4f status=%s\n", - Lod,static_cast(FloatingError.Floating),FloatingError.Maximum, + Lod,static_cast(FloatingError.Floating),FloatingError.Geometry.Maximum, FloatingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE"); - std::vector FineTiles; - FineTiles.reserve(8); + std::vector FineBeforeTiles,FineAfterTiles; + FineBeforeTiles.reserve(8);FineAfterTiles.reserve(8); for(int Dz=0;Dz<=1;++Dz) for(int Dy=0;Dy<=1;++Dy) for(int Dx=0;Dx<=1;++Dx) { const FVoxelTileKey FineKey(FIntVector(2*CoarseTile.Key.Coord.X+Dx, 2*CoarseTile.Key.Coord.Y+Dy,2*CoarseTile.Key.Coord.Z+Dz),Lod-1); - FStandaloneCpuTile FineTile=Streamer.GenerateDiagnostic(FineKey); - if(!FineTile.Vertices.empty())FineTiles.push_back(std::move(FineTile)); + FStandaloneCpuTile FineBefore=Streamer.GenerateDiagnostic( + FineKey,0,1,true,&BaselineOptions,true); + FStandaloneCpuTile FineAfter=Streamer.GenerateDiagnostic( + FineKey,0,1,true,nullptr,true); + if(!FineBefore.Vertices.empty())FineBeforeTiles.push_back(std::move(FineBefore)); + if(!FineAfter.Vertices.empty())FineAfterTiles.push_back(std::move(FineAfter)); } - std::vector FinePointers; - FinePointers.reserve(FineTiles.size()); - for(const auto& Tile:FineTiles)FinePointers.push_back(&Tile); - const auto FineSpan=std::span( - FinePointers.data(),FinePointers.size()); - const FHeightGrid FineGrid=Rasterize(FineSpan,CoarseTile.Key,VoxelMeters); - PopRows[Lod]=ComparePop(CoarseGrid,FineGrid,CoarseTile.Key,VoxelMeters, + std::vector FineBeforePointers,FineAfterPointers; + FineBeforePointers.reserve(FineBeforeTiles.size());FineAfterPointers.reserve(FineAfterTiles.size()); + for(const auto& Tile:FineBeforeTiles)FineBeforePointers.push_back(&Tile); + for(const auto& Tile:FineAfterTiles)FineAfterPointers.push_back(&Tile); + const auto FineBeforeSpan=std::span( + FineBeforePointers.data(),FineBeforePointers.size()); + const auto FineAfterSpan=std::span( + FineAfterPointers.data(),FineAfterPointers.size()); + const FHeightGrid FineBeforeGrid=Rasterize(FineBeforeSpan,CoarseTile.Key,VoxelMeters); + const FHeightGrid FineAfterGrid=Rasterize(FineAfterSpan,CoarseTile.Key,VoxelMeters); + const FPopRow PopBefore=ComparePop(BaselineGrid,FineBeforeGrid,CoarseTile.Key,VoxelMeters, Budgets[Lod],Assembly,View); - const FPopRow& Pop=PopRows[Lod]; - const bool PopPass=Pop.Valid&&Pop.Cracks==0&&Pop.Floating==0&&Pop.Maximum<=1.0; + const FPopRow PopAfter=ComparePop(CoarseGrid,FineAfterGrid,CoarseTile.Key,VoxelMeters, + Budgets[Lod],Assembly,View); + const bool PopPass=PopNotWorseAtAnySample(PopBefore,PopAfter); GatePass=GatePass&&PopPass; - std::printf("[ScreenSpaceSwap] lod=%d_to_%d samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xyz_m=(%.3f,%.3f,%.3f) max_slope_deg=%.2f cracks=%llu floating=%llu status=%s\n", - Lod,Lod-1,static_cast(Pop.Samples), - Pop.P50,Pop.P99,Pop.Maximum,Pop.VerticalP50,Pop.VerticalP99,Pop.VerticalMaximum, - Pop.MaximumNormalDistanceMeters,Pop.MaximumX,Pop.MaximumY, - Pop.MaximumDisplayedZ, - Pop.MaximumSlopeDegrees,static_cast(Pop.Cracks), - static_cast(Pop.Floating), + std::printf("[ScreenSpaceSwap] swap=%d_to_%d distance_m=%.3f samples_before=%llu pop_before_px_p50=%.4f p99=%.4f max=%.4f pop_after_px_p50=%.4f p99=%.4f max=%.4f missing_root_samples_before=%llu after=%llu status=%s\n", + Lod,Lod-1,Distances[Lod],static_cast(PopBefore.Samples), + PopBefore.P50,PopBefore.P99,PopBefore.Maximum,PopAfter.P50,PopAfter.P99,PopAfter.Maximum, + static_cast(PopBefore.Cracks),static_cast(PopAfter.Cracks), PopPass?"PASS":"REJECT"); - PrintSlopeTail("swap",Lod,Pop.SlopeSamples,Pop.SlopeP99,Pop.SlopeMaximum); - const FHeightGrid MissingFineGrid(CoarseGrid.Side,CoarseGrid.MinimumX, - CoarseGrid.MaximumX,CoarseGrid.MinimumY,CoarseGrid.MaximumY); - const FPopRow MissingFineError=ComparePop(CoarseGrid,MissingFineGrid, + const FHeightGrid MissingFineGrid(BaselineGrid.Side,BaselineGrid.MinimumX, + BaselineGrid.MaximumX,BaselineGrid.MinimumY,BaselineGrid.MaximumY); + const FPopRow MissingFineError=ComparePop(BaselineGrid,MissingFineGrid, CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View); const bool CrackRejected=MissingFineError.Cracks>0&&MissingFineError.Maximum>1.0; FaultControlsPass=FaultControlsPass&&CrackRejected; - GatePass=GatePass&&FaultControlsPass; std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-fine-ring-surface cracks=%llu max_px=%.4f status=%s\n", Lod,static_cast(MissingFineError.Cracks), MissingFineError.Maximum,CrackRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE"); @@ -695,25 +851,170 @@ namespace StandaloneScreenSpaceGate const auto CheckNegative=[&](const char* Name,std::uint32_t Drop,std::uint32_t Stride) { FStandaloneCpuTile Bad=Streamer.GenerateDiagnostic( - CoarseTile.Key,Drop,Stride,true); + CoarseTile.Key,Drop,Stride,true,nullptr,true); const FStandaloneCpuTile* BadPointer=&Bad; const std::span BadSpan(&BadPointer,1); const FHeightGrid BadGrid=Rasterize(BadSpan,Bad.Key,VoxelMeters); - const FErrorRow BadError=CompareToReference(BadGrid,ExactGrid, - CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View); - const bool Rejected=BadError.Missing>0||BadError.Floating>0 - ||BadError.Samples<16||BadError.Maximum>1.0; + const FMeshComparisonRow BadError=CompareMeshes(BaselineGrid,BadGrid, + CoarseTile.Key,VoxelMeters,Budgets[Lod],View); + const bool Rejected=BadError.Holes>0||BadError.Floating>0||BadError.Cracks>0 + ||BadError.Geometry.Samples<16||BadError.Geometry.Maximum>1.0 + ||BadError.ShadeMaximum>ShadeLimit; NegativeControlsPass=NegativeControlsPass&&Rejected; - std::printf("[ScreenSpaceNegative] lod=%d case=%s p50_px=%.4f p99_px=%.4f max_px=%.4f missing=%llu floating=%llu status=%s\n", - Lod,Name,BadError.P50,BadError.P99,BadError.Maximum, - static_cast(BadError.Missing), + std::printf("[ScreenSpaceNegative] lod=%d case=%s p50_px=%.4f p99_px=%.4f max_px=%.4f shade_max=%.6f holes=%llu cracks=%llu floating=%llu status=%s\n", + Lod,Name,BadError.Geometry.P50,BadError.Geometry.P99,BadError.Geometry.Maximum, + BadError.ShadeMaximum,static_cast(BadError.Holes), + static_cast(BadError.Cracks), static_cast(BadError.Floating), Rejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE"); }; CheckNegative("s13-global-octave-drop",1,1); CheckNegative("stride-2-column-interpolation",0,2); - GatePass=GatePass&&NegativeControlsPass; + GatePass=GatePass&&FaultControlsPass&&NegativeControlsPass; } + + struct FCoverageRow + { + std::uint64_t Tiles=0,Holes=0,Cracks=0,Floating=0,MaterialMismatches=0; + std::vector GeometryPixels,ShadeValues,NormalAngles; + }; + std::array Coverage; + for(const FVoxelTileKey& Key:DesiredKeys) + { + const int Lod=Key.Level; + if(Lod<3||Lod>MaxLod||Lod>=9)continue; + FStandaloneCpuTile BaselineTile=Streamer.GenerateDiagnostic( + Key,0,1,true,&BaselineOptions,true); + FStandaloneCpuTile CandidateTile=Streamer.GenerateDiagnostic( + Key,0,1,true,&CandidateOptions,true); + const FStandaloneCpuTile* BaselinePointer=&BaselineTile; + const FStandaloneCpuTile* CandidatePointer=&CandidateTile; + const std::span BaselineSpan(&BaselinePointer,1); + const std::span CandidateSpan(&CandidatePointer,1); + const FHeightGrid BaselineGrid=Rasterize(BaselineSpan,Key,VoxelMeters); + const FHeightGrid CandidateGrid=Rasterize(CandidateSpan,Key,VoxelMeters); + const FMeshComparisonRow Delta=CompareMeshes(BaselineGrid,CandidateGrid, + Key,VoxelMeters,Budgets[Lod],View); + auto& Row=Coverage[Lod]; + ++Row.Tiles; + Row.Holes+=Delta.Holes;Row.Cracks+=Delta.Cracks;Row.Floating+=Delta.Floating; + Row.MaterialMismatches+=Delta.MaterialMismatches; + Row.GeometryPixels.insert(Row.GeometryPixels.end(), + Delta.GeometryPixels.begin(),Delta.GeometryPixels.end()); + Row.ShadeValues.insert(Row.ShadeValues.end(),Delta.ShadeValues.begin(),Delta.ShadeValues.end()); + Row.NormalAngles.insert(Row.NormalAngles.end(),Delta.NormalAngles.begin(),Delta.NormalAngles.end()); + } + for(int Lod=3;Lod<=MaxLod&&Lod<9;++Lod) + { + auto& Row=Coverage[Lod]; + const double GeometryP50=Percentile(Row.GeometryPixels,0.50); + const double GeometryP99=Percentile(Row.GeometryPixels,0.99); + const double GeometryMaximum=Row.GeometryPixels.empty()?0.0 + :*std::max_element(Row.GeometryPixels.begin(),Row.GeometryPixels.end()); + const double ShadeP50=Percentile(Row.ShadeValues,0.50); + const double ShadeP99=Percentile(Row.ShadeValues,0.99); + const double ShadeMaximum=Row.ShadeValues.empty()?0.0 + :*std::max_element(Row.ShadeValues.begin(),Row.ShadeValues.end()); + const double NormalP50=Percentile(Row.NormalAngles,0.50); + const double NormalP99=Percentile(Row.NormalAngles,0.99); + const double NormalMaximum=Row.NormalAngles.empty()?0.0 + :*std::max_element(Row.NormalAngles.begin(),Row.NormalAngles.end()); + const bool LodPass=Row.Tiles>0&&Row.GeometryPixels.size()>=16 + &&GeometryMaximum<=1.0&&ShadeMaximum<=ShadeLimit + &&Row.Holes==0&&Row.Cracks==0&&Row.Floating==0; + GatePass=GatePass&&LodPass; + std::printf("[ScreenSpaceGateCoverageLOD] lod=%d tiles=%llu geometry_samples=%llu geometry_px_p50=%.4f p99=%.4f max=%.4f shading_delta_p50=%.6f p99=%.6f max=%.6f normal_deg_p50=%.4f p99=%.4f max=%.4f material_mismatch=%llu holes=%llu cracks=%llu floating=%llu status=%s\n", + Lod,static_cast(Row.Tiles), + static_cast(Row.GeometryPixels.size()), + GeometryP50,GeometryP99,GeometryMaximum,ShadeP50,ShadeP99,ShadeMaximum, + NormalP50,NormalP99,NormalMaximum, + static_cast(Row.MaterialMismatches), + static_cast(Row.Holes), + static_cast(Row.Cracks), + static_cast(Row.Floating),LodPass?"PASS":"REJECT"); + } + + std::uint64_t RepairOpportunities=0,BaselineWallHoles=0,BaselineWallFloating=0; + std::uint64_t CandidateWallRays=0,CandidateWallHoles=0,CandidateWallFloating=0; + std::vector BaselineHoleIds,CandidateHoleIds; + std::vector BaselineFloatingIds,CandidateFloatingIds; + std::vector CandidatePanelRemovalHoleIds; + bool bRanPanelRemovalControl=false; + for(const FVoxelTileKey& Key:DesiredKeys) + { + if(Key.Level<6||Key.Level>8)continue; + VoxelForgeCore::TileGenerationOptions NoPanelBaseline=BaselineOptions; + NoPanelBaseline.diagnosticRemoveFarConnectorPanels=true; + NoPanelBaseline.diagnosticAuditFarConnectorCoverage=true; + const FStandaloneCpuTile BaseNoPanels=Streamer.GenerateDiagnostic(Key,0,1,true,&NoPanelBaseline); + RepairOpportunities+=BaseNoPanels.CoreDiagnostics.farConnectorAuditHoles; + + VoxelForgeCore::TileGenerationOptions AuditedBaseline=BaselineOptions; + AuditedBaseline.diagnosticAuditFarConnectorCoverage=true; + const FStandaloneCpuTile BaseRepaired=Streamer.GenerateDiagnostic(Key,0,1,true,&AuditedBaseline); + BaselineWallHoles+=BaseRepaired.CoreDiagnostics.farConnectorAuditHoles; + BaselineWallFloating+=BaseRepaired.CoreDiagnostics.farConnectorAuditFloating; + BaselineHoleIds.insert(BaselineHoleIds.end(), + BaseRepaired.CoreDiagnostics.farConnectorAuditHoleIds.begin(), + BaseRepaired.CoreDiagnostics.farConnectorAuditHoleIds.end()); + BaselineFloatingIds.insert(BaselineFloatingIds.end(), + BaseRepaired.CoreDiagnostics.farConnectorAuditFloatingIds.begin(), + BaseRepaired.CoreDiagnostics.farConnectorAuditFloatingIds.end()); + + VoxelForgeCore::TileGenerationOptions AuditedCandidate=CandidateOptions; + AuditedCandidate.diagnosticAuditFarConnectorCoverage=true; + const FStandaloneCpuTile CandidateRepaired=Streamer.GenerateDiagnostic(Key,0,1,true,&AuditedCandidate); + CandidateWallRays+=CandidateRepaired.CoreDiagnostics.farConnectorAuditRays; + CandidateWallHoles+=CandidateRepaired.CoreDiagnostics.farConnectorAuditHoles; + CandidateWallFloating+=CandidateRepaired.CoreDiagnostics.farConnectorAuditFloating; + CandidateHoleIds.insert(CandidateHoleIds.end(), + CandidateRepaired.CoreDiagnostics.farConnectorAuditHoleIds.begin(), + CandidateRepaired.CoreDiagnostics.farConnectorAuditHoleIds.end()); + CandidateFloatingIds.insert(CandidateFloatingIds.end(), + CandidateRepaired.CoreDiagnostics.farConnectorAuditFloatingIds.begin(), + CandidateRepaired.CoreDiagnostics.farConnectorAuditFloatingIds.end()); + + if(!bRanPanelRemovalControl&&BaseNoPanels.CoreDiagnostics.farConnectorAuditHoles>0) + { + AuditedCandidate.diagnosticRemoveFarConnectorPanels=true; + const FStandaloneCpuTile CandidateNoPanels=Streamer.GenerateDiagnostic( + Key,0,1,true,&AuditedCandidate); + CandidatePanelRemovalHoleIds=CandidateNoPanels.CoreDiagnostics.farConnectorAuditHoleIds; + bRanPanelRemovalControl=true; + } + } + std::sort(BaselineHoleIds.begin(),BaselineHoleIds.end()); + std::sort(CandidateHoleIds.begin(),CandidateHoleIds.end()); + std::sort(BaselineFloatingIds.begin(),BaselineFloatingIds.end()); + std::sort(CandidateFloatingIds.begin(),CandidateFloatingIds.end()); + const auto IsSubset=[](const std::vector& A,const std::vector& B) + {return std::includes(B.begin(),B.end(),A.begin(),A.end());}; + std::sort(CandidatePanelRemovalHoleIds.begin(),CandidatePanelRemovalHoleIds.end()); + const bool bNoNewHoles=IsSubset(CandidateHoleIds,BaselineHoleIds); + const bool bNoNewFloating=IsSubset(CandidateFloatingIds,BaselineFloatingIds); + const bool bPanelRemovalDetected=bRanPanelRemovalControl + &&!IsSubset(CandidatePanelRemovalHoleIds,CandidateHoleIds); + std::vector NewHoleIds,NewFloatingIds,PanelRemovalNewHoleIds; + std::set_difference(CandidateHoleIds.begin(),CandidateHoleIds.end(), + BaselineHoleIds.begin(),BaselineHoleIds.end(),std::back_inserter(NewHoleIds)); + std::set_difference(CandidateFloatingIds.begin(),CandidateFloatingIds.end(), + BaselineFloatingIds.begin(),BaselineFloatingIds.end(),std::back_inserter(NewFloatingIds)); + std::set_difference(CandidatePanelRemovalHoleIds.begin(),CandidatePanelRemovalHoleIds.end(), + CandidateHoleIds.begin(),CandidateHoleIds.end(),std::back_inserter(PanelRemovalNewHoleIds)); + const bool bRepairRowsPass=RepairOpportunities>0&&bNoNewHoles&&bNoNewFloating + &&bPanelRemovalDetected; + GatePass=GatePass&&bRepairRowsPass; + std::printf("[ScreenSpaceConnectorGate] lods=6..8 baseline_missing_ray_stations=%llu candidate_audited_rays=%llu baseline_holes=%llu candidate_holes=%llu new_holes=%llu baseline_floating=%llu candidate_floating=%llu new_floating=%llu panel_removal_new_holes=%llu status=%s\n", + static_cast(RepairOpportunities), + static_cast(CandidateWallRays), + static_cast(BaselineWallHoles), + static_cast(CandidateWallHoles), + static_cast(NewHoleIds.size()), + static_cast(BaselineWallFloating), + static_cast(CandidateWallFloating), + static_cast(NewFloatingIds.size()), + static_cast(PanelRemovalNewHoleIds.size()), + bRepairRowsPass?"PASS":"REJECT"); std::printf("[ScreenSpaceGateResult] status=%s\n",GatePass?"PASS":"REJECT"); return GatePass; } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 4d93c4f..c5b1139 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -412,6 +412,7 @@ namespace StandaloneStreamBenchmark FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel, bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap, FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache); + Streamer.SetDisplayAttributeCaptureEnabled(bScreenSpaceGate); Streamer.SetUniformBlockProofAuditEnabled(bAuditUniformBlockProofs); if(bScreenSpaceGate||bReportLodWorkProfile)Streamer.SetMesherStageProfilingEnabled(true); const auto StreamStart=Clock::now(); @@ -1480,7 +1481,7 @@ namespace StandaloneStreamBenchmark { if(Assembly) bScreenSpaceGatePassing=StandaloneScreenSpaceGate::Run(GateSampleTiles, - Streamer,*Assembly,ClosestDisplayDistances,World.CoreVoxelSizeMicrons, + Streamer,Desired.Tiles,*Assembly,ClosestDisplayDistances,World.CoreVoxelSizeMicrons, World.MaxClipLevel,PixelBudgetsSubvoxels,CameraYawDegrees, CameraPitchDegrees); else diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index b27dd7e..27005b2 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -43,9 +43,11 @@ struct FStandaloneCpuTile FVoxelTileKey Key; uint64 RequestedCrossing=0; std::vector Vertices; + std::vector> VertexColors; std::vector Indices; std::vector TriangleSkirtFaceMasks; uint32 SurfaceVertexCount=0; + uint32 CeilingTriangleCount=0; uint32 SkirtTriangleCount=0; uint64 DensitySamples=0; double WorkerSeconds=0.0; @@ -653,12 +655,17 @@ public: {bProfileMesherStages=Enabled;} void SetUniformBlockProofAuditEnabled(bool Enabled) {bAuditUniformBlockProofs=Enabled;} + void SetDisplayAttributeCaptureEnabled(bool Enabled) + {bCaptureDisplayAttributes=Enabled;} FStandaloneCpuTile GenerateDiagnostic(const FVoxelTileKey& Key, std::uint32_t GlobalOctaveDrop=0,std::uint32_t CellStride=1, - bool ForceGenerate=false) + bool ForceGenerate=false, + const VoxelForgeCore::TileGenerationOptions* FarDisplayOverride=nullptr, + bool CaptureDisplayAttributes=false) { FStandaloneTileJob Job;Job.Key=Key;Job.StartedAt=FClock::now();Job.QueuedAt=Job.StartedAt; - return Generate(Job,GlobalOctaveDrop,CellStride,ForceGenerate); + return Generate(Job,GlobalOctaveDrop,CellStride,ForceGenerate,FarDisplayOverride, + CaptureDisplayAttributes); } private: @@ -778,7 +785,9 @@ private: } FStandaloneCpuTile Generate(const FStandaloneTileJob& Job, std::uint32_t DiagnosticGlobalOctaveDrop=0,std::uint32_t DiagnosticCellStride=1, - bool ForceGenerate=false) + bool ForceGenerate=false, + const VoxelForgeCore::TileGenerationOptions* FarDisplayOverride=nullptr, + bool CaptureDisplayAttributes=false) { const auto StartTime=Job.StartedAt; FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing; @@ -829,6 +838,18 @@ private: Options.diagnosticCellStride=std::max(1,DiagnosticCellStride); Options.diagnosticColumnInterpolationStride= std::max(1,DiagnosticCellStride); + if(FarDisplayOverride) + { + Options.overrideFarDisplayMesh=FarDisplayOverride->overrideFarDisplayMesh; + Options.farDisplayGeometryBudgetMillipixels=FarDisplayOverride->farDisplayGeometryBudgetMillipixels; + Options.farRefinementGridScale=FarDisplayOverride->farRefinementGridScale; + Options.farRefinementLodMask=FarDisplayOverride->farRefinementLodMask; + Options.farConnectorStationScale=FarDisplayOverride->farConnectorStationScale; + Options.farConnectorRayMask=FarDisplayOverride->farConnectorRayMask; + Options.farConnectorReuseStationRadii=FarDisplayOverride->farConnectorReuseStationRadii; + Options.diagnosticRemoveFarConnectorPanels=FarDisplayOverride->diagnosticRemoveFarConnectorPanels; + Options.diagnosticAuditFarConnectorCoverage=FarDisplayOverride->diagnosticAuditFarConnectorCoverage; + } std::string Error; auto Result=CoreWorld->generateTile( {{Job.Key.Coord.X,Job.Key.Coord.Y,Job.Key.Coord.Z},Job.Key.Level},Options,Error); @@ -842,6 +863,7 @@ private: Out.DensitySamples=Result->densitySampleCount; Out.CoreDiagnostics=Result->diagnostics; Out.SurfaceVertexCount=Result->groups.surfaceVertexCount; + Out.CeilingTriangleCount=Result->groups.ceilingTriangleCount; Out.SkirtTriangleCount=Result->groups.skirtTriangleCount; Out.MeshPayloadAllocatedBytes=Result->meshPayloadAllocatedBytes; Out.DensityHash=Result->densityHash;Out.MeshHash=Result->meshHash; @@ -849,12 +871,18 @@ private: Out.bHasDensityHash=Result->hasDensityHash;Out.bHasMeshHash=Result->hasMeshHash; Out.bHasPayloadHash=Result->hasPayloadHash;Out.bHasTopologyHash=Result->hasTopologyHash; Out.Vertices.reserve(Result->vertices.size()); + const bool bCaptureAttributes=CaptureDisplayAttributes||bCaptureDisplayAttributes; + if(bCaptureAttributes)Out.VertexColors.reserve(Result->vertices.size()); for(const auto& Vertex:Result->vertices) + { Out.Vertices.push_back({ {static_cast(Vertex.position.x*0.01),static_cast(Vertex.position.y*0.01), static_cast(Vertex.position.z*0.01)}, {static_cast(Vertex.normal.x),static_cast(Vertex.normal.y), static_cast(Vertex.normal.z)}}); + if(bCaptureAttributes) + Out.VertexColors.push_back({Vertex.color.r,Vertex.color.g,Vertex.color.b,Vertex.color.a}); + } Out.Indices=std::move(Result->indices); Out.TriangleSkirtFaceMasks=std::move(Result->skirtFaceMasks); if(bProfile) @@ -1001,6 +1029,7 @@ private: bool bSkipUV=false; bool bSkipColors=false; bool bCaptureColumnCoordinates=false; + bool bCaptureDisplayAttributes=false; bool bSharedColumnCache=true; int32 Lod8HorizonInteriorStepVoxels=0; bool bProfileMesherStages=false; diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index 90c6cdd..76bba6f 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -30,6 +30,34 @@ namespace StandaloneWorldAssemblyPrototype return std::max(1,WorldAssemblyFixed(wavelength)); } + inline bool ApplyFarDisplayMeshDeclaration(const VFJson::Value& root, + VoxelForgeCore::WorldAssemblyRecipeV1& recipe,std::string& error) + { + const VFJson::Value& declaration=root.Get("far_display_mesh"); + if(std::holds_alternative(declaration.Data))return true; + if(!declaration.IsObject()) + {error="far_display_mesh must be an object when declared";return false;} + const auto ReadUnsigned=[&](const char* key,std::uint32_t& output) + { + const VFJson::Value& value=declaration.Get(key); + if(!value.IsNumber()||!std::isfinite(value.Number())||value.Number()<0.0 + ||value.Number()>static_cast(UINT32_MAX) + ||std::floor(value.Number())!=value.Number())return false; + output=static_cast(value.Number());return true; + }; + if(!ReadUnsigned("geometry_budget_millipixels",recipe.farDisplayGeometryBudgetMillipixels) + ||!ReadUnsigned("refinement_grid_scale",recipe.farRefinementGridScale) + ||!ReadUnsigned("refinement_lod_mask",recipe.farRefinementLodMask) + ||!ReadUnsigned("connector_station_scale",recipe.farConnectorStationScale) + ||!ReadUnsigned("connector_ray_mask",recipe.farConnectorRayMask)) + {error="far_display_mesh declaration has a missing or non-integer parameter";return false;} + const VFJson::Value& reuse=declaration.Get("connector_reuse_station_radii"); + if(!std::holds_alternative(reuse.Data)) + {error="far_display_mesh connector_reuse_station_radii must be boolean";return false;} + recipe.farConnectorReuseStationRadii=std::get(reuse.Data); + return true; + } + inline uint64 WorldAssemblyFeatureId(uint32 seed,int64 x,int64 y,uint32 kind) { uint64 hash=14695981039346656037ull; @@ -223,6 +251,7 @@ namespace StandaloneWorldAssemblyPrototype recipe.biomeClimateReliefWavelengthSubvoxels=WorldAssemblyWavelength(map.ReliefFrequency,3333.0); recipe.biomeClimateMoistureWavelengthSubvoxels=WorldAssemblyWavelength(map.MoistureFrequency,2500.0); recipe.biomeClimateReliefContrastQ16=WorldAssemblyQ16(map.ReliefContrast); + if(!ApplyFarDisplayMeshDeclaration(root,recipe,error))return false; const auto makeProfile=[&](uint32 stableId,const FSurfaceGenerationParams& params, const FBiomeResolved* climate) {