diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9a4bcbd..f37b266 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -73,7 +73,7 @@ is the only density path; a degenerate (zero-height) strate has none and is open | Maze | `FMazeGenerationParams` | `BuildMazeStack` | origin-directed spanning-tree corridors on a 3D lattice, with capped loop edges and a thread-local per-cell capsule cache | | SurfaceWorld | `FSurfaceGenerationParams` | `BuildSurfaceStack` | heightfield terrain: domain-warped continents+ridged mtns+detail, a low-freq **relief map** (`M`) that scales mountains/elevation for plains↔highland variety, **F20 heightfield terrain ops** (`Surface|Ops` — all default off ⇒ byte-identical): **Cliff** (slope-gated steepening — where the analytic structural slope > `CliffSlopeThreshold`, push the height away from the local mean by `CliffSharpness` ⇒ gentle slopes become sheer walls/canyon faces that hug real steep ground, gentle areas untouched; 4 structural resamples only when enabled), **Terrace** (relief-gated plateau quantize + `TerraceHardness` soft-round↔crisp-mesa), **LayerLines** (sedimentary sine shelves, slope-expressed) — pure per-column height remaps in the single oracle `ComputeSurfaceTerrainZ` (`SampleSurfaceStructuralZ` = pre-op raw height, re-sampled at an XY offset for Cliff's slope), biome-selected + border-blended for free via each biome's `SurfaceParams` + the height output-lerp; plus **phase-2 Overhang** (the first VOLUMETRIC op — real jutting shelves like a cliff lip): in `FOverhangShelfMod`, for AIR voxels in the window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height and unioned in. Low in the window the shift is ~0 (borrows nearby low rock ⇒ stays air over the void); high up it reaches the far cliff (solid) ⇒ a shelf attached to the cliff, tapering out over the void with air underneath. Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill dir `(DirX,DirY)` are resolved once in `ComputeSurfaceColumn` — the gradient sampled at the REACH scale (`OverhangReach`) so a point out over the void can "see" the cliff to know which way is uphill — and cached on `FSurfaceColumn`. It's genuine 3D (per-voxel structural re-eval), so it's gated hard to steep overhang columns; the union only ADDS rock (never removes), capped at `TerrainZ+OverhangHeight`, so `ClassifyTile` forces Mixed only in `(TerrainZ, TerrainZ+OverhangMargin]` (upward-only; margin = max `OverhangHeight`) — a shelf never holes a trivially-skipped tile, and only the thin cliff-edge band of air tiles is woken (NOT far-field like phase-3 spikes/holes). Overhang undersides classify as ground rock by the F17 rule (down-facing but below `TerrainZ`). Known v1 limit: applies at all LODs (Step-agnostic) — may alias far; gate to fine tiles later. Beaches at water line, high sky-cap ceiling. The cap is shapeable terrain in its own right (`ComputeSurfaceCeiling`, `Surface|Sky` params: `CeilingUndulation` broad inverted hills/valleys, `CeilingRidgeStrength` hanging ridgelines, `CeilingRoughness`+freq fine bumps, `CeilingWarp*`) — defaults (strengths 0, freq 0.04) = old flat-ish cap. (`Surface|Macro` params = the cheap precursor to biomes; `ReliefStrength=0` ⇒ old uniform terrain.) **Sky-cap vs ground is a PER-TRIANGLE surface class (F17), not a per-tile verdict**: the mesher classifies each unique vertex on the **worker** — only down-facing verts (`N.Z<-0.1`) pay a memoized `GetSurfaceHeightAt` column query; nearer `CeilSurf` ⇒ sky-cap, nearer `TerrainZ` ⇒ terrain overhang stays ground (a future SurfaceWorld cave roof — down-facing but below `TerrainZ` — also lands ground by the same rule; non-surface strates always ground). Triangles take the majority class of their 3 verts and are packed as **two contiguous index runs** (ground then cap, `FVoxelMeshData::NumCeilingTriangles`; skirts inherit their source triangle's run) → RMC **polygroups 0/1** → one **section per non-empty group** (`ApplyMeshToTile`, material slot = group index): slot 0 = strate `OverrideMaterial`/default (min-corner chunk), slot 1 = `CeilingMaterial` resolved at the tile's **TOP chunk** (mid as gap fallback, then min; fallback: ground material) — a coarse tile is 2^level chunks tall, so its min corner can sit in a lower strate/gap while the cap belongs to the strate above (this was the "far cap = ground material" residue), so the shadowless overhead rock is tinted separately instead of reading flat/bright. Shadow is **per section** via `FRealtimeMeshSectionConfig::bCastsShadow` (NOT the component `SetCastShadow` — RMC's proxy ignores the component flag; this is also why level≥2 far tiles only stopped casting once the section flag was wired): ground casts at level≤1, the cap section never casts, so the rock ceiling never shadows the terrain below it. History: v1 was a game-thread centre height-oracle (misclassified coarse far tiles → terrain material on the cap underside); v2 a whole-tile worker normal VOTE — which painted **mixed coarse tiles** (one far tile spanning terrain AND cap) entirely with the winner's material, and put sky material under terrain overhangs. The per-triangle class fixes both and is the identity channel caves/F8 will reuse. | | VerticalShafts | `FVerticalShaftParams` | `BuildVerticalShaftStack` | full-height shafts + horizontal connectors + partial ledges | -| FloatingIslands | `FFloatingIslandParams` | `BuildFloatingIslandStack` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, in an open void | +| FloatingIslands | `FFloatingIslandParams` | `BuildFloatingIslandStack` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, beneath the shared ceiling at its configured strate upper boundary | | Underwater | `FStrateGenerationParams` + water | (reuses `BuildTunnelNetworkStack`) | tunnel rock + high water table | All stacks share the convention: internal **positive=solid**, and `AppendStructuralPost` ends every @@ -1431,8 +1431,9 @@ density roots, including a 100 cm datum-offset negative control. See evaluators keep their recorded cap behavior. The clip joins the exact near density root at the owning boundary without changing LOD0–3 density hashes. Future families with planar roofs must declare their cap height and bounds through the family plan/provider contract; the shared tile generator consumes - that declaration. FloatingIslands declares no planar roof, so open sky above a surface island is the - expected view. + that declaration. FloatingIslands uses the same resolved strate-boundary data flow: its upper seal + follows its plan's chunk range and the shared boundary-seal primitive, while SurfaceWorld keeps its + existing heightfield cap. For each requested far SurfaceWorld XY column at LOD4–8, desired-tile selection adds cap-height bands only when they do not overlap an existing or already accepted 3D clipmap tile. The original @@ -1471,8 +1472,9 @@ neighborhood have installed collision (or are explicitly collision-not-required) now** — the active view has ground, horizon and the family-appropriate ceiling, with no exposed coverage hole or unexpected sky; (3) **refined soon** — each target LOD resolves within its stated time or travel distance after entering range. While moving, report the largest frame/time gap in -enclosure and maximum distance that unresolved work trails the pawn. FloatingIslands has open sky by -design, so its enclosure check excludes a planar roof. +enclosure and maximum distance that unresolved work trails the pawn. FloatingIslands is enclosed by a +planar roof at the upper boundary of its configured strate range; a taller strate places that roof +farther above its islands. The shared `AVoxelWorld::UpdateChunksAroundPosition` scheduler serves both the Core snapshot path and Core-off legacy path. It admits the collision-critical 3×3 ring first. The measured S5 order remains @@ -1840,6 +1842,20 @@ 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 Shared strate boundaries and FloatingIslands lower seal — World Assembler L18 + +`FStandaloneShapeWorld::GetResolvedStratumBounds` supplies one physical lower/upper-bound descriptor +to layer ranges, far-field sample protection, and far-shell continuity for every strate. SurfaceWorld +retains its exact `topChunkZ * 32` Core-voxel mapping and native heightfield cap. Native FloatingIslands +takes its resolved bounds from its plan, whose chunk range is converted through the serialized +owner/Core voxel ratio. Its version-3 field removes only the lower all-XY seal and calls the same +`VF_ApplyUpperBoundarySeal` primitive used by the shared two-sided `VF_ApplyBoundarySeal` for its upper +boundary. The ceiling therefore sits at that strate's own upper boundary, with the common +`boundarySealSubvoxels` thickness; there is no FloatingIslands-specific height. The recipe range is +un-inset to match the Unreal adapter. Versions 1 and 2 retain their serialized two-sided seals. +SurfaceWorld's field and output hashes remain unchanged. See +[`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux18/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..433dfb3 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -208,14 +208,16 @@ all eight archetypes; there is no other path. See [Docs/archive/OPSTACK-PLAN.md] | `FVoxelBoxHypotheses` + `FVoxelBoxSdfInterval` + `VF_ForceHypotheses` / `VF_FoldEffect` / `VF_FoldOp` | The fold that turns a stack into an `EVoxelTileClass`, carrying the SDF interval between independent writers and consumers. Unknown is biased toward `Mixed`; the mapping and soundness rule are written out in the header. | ### 3.2c Structural primitives — `Public/VoxelDensityPrimitives.h` -`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` — +`VF_ApplyOriginSpine` · `VF_ApplyBoundarySeal` · `VF_ApplyUpperBoundarySeal` · `VF_ApplyPassageCarving` · `VF_ApplyXYEdgeSeal` — the four world invariants every archetype appends, with landing/floor geometry joined by the manager passage post, **moved here 2026-07-27** so the generator and the operator stack share ONE copy. The XY helper is a radial smooth ramp in actor-space and is a true no-op when `WorldRadiusVoxels == 0`; `VF_XYEdgeSealForcedMarginOverBox` is the shared sound proof for `ClassifyBox` and T1.d's global shell skip. `VoxelGenerator.cpp` keeps same-named `static FORCEINLINE` forwarders for `ApplyBoundarySeal` and `ApplyOriginSpine`; the passage and -edge helpers are called directly. **Convention: INTERNAL (positive = solid); `VF_ApplyXYEdgeSealMC` is the output wrapper.** +edge helpers are called directly. `VF_ApplyUpperBoundarySealValue` contains the common upper-band +math used by both one-sided and two-sided seals. **Convention: INTERNAL (positive = solid); +`VF_ApplyXYEdgeSealMC` is the output wrapper.** ### 3.2d Operator stack — `Public/VoxelDensityOpStack.h` + `Private/VoxelDensityOpStack.cpp` ⚠️ **This IS the game's generation path.** `GetDensityAt` builds the stack in its per-chunk refetch @@ -1137,6 +1139,17 @@ Implementation and evidence: [`Standalone/Research/WorldAssemblerM3/Linux9/REPOR 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 Shared strate boundaries and FloatingIslands seal — World Assembler M3 Linux18 + +| Symbol | Canonical file | Role | +|---|---|---| +| `FStandaloneShapeWorld::FResolvedStratumBounds`, `MakeResolvedStratumBounds`, `GetResolvedStratumBounds` | `Standalone/Core/src/ShapeWorldInternal.h` | Resolves each strate's physical lower/upper range and seal width for layer ranges, far-field sample protection, and far-shell continuity. Native FloatingIslands uses its plan's owner/Core ratio; SurfaceWorld retains its `32` Core voxels per slot chunk. | +| `VF_ApplyUpperBoundarySeal` / `VF_ApplyUpperBoundarySealValue` | `Standalone/Source/VoxelForge/Public/VoxelDensityPrimitives.h` and `Source/VoxelForge/Public/VoxelDensityPrimitives.h` | Shared one-sided top seal used by version-3 native FloatingIslands; the two-sided seal reuses the same upper-band math. | + +Geometry version 3 removes the FI lower all-XY seal while retaining its ceiling through the shared +upper-bound path. Versions 1 and 2 retain their serialized two-sided behavior. See ARCHITECTURE §8.37 +and [`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp index 7a3f9f0..3a9064e 100644 --- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp @@ -450,7 +450,7 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager, IslandPlan.topChunkZ = Slot.TopChunkZ; IslandPlan.bottomChunkZ = Slot.BottomChunkZ; auto& Recipe = IslandPlan.manifest.recipe; - Recipe.geometryVersion = 2; + Recipe.geometryVersion = 3; Recipe.seed = static_cast(StrateManager->GetWorldSeed()); Recipe.ownerVoxelSizeMicrons = static_cast(std::llround(OwnerVoxelSizeCm * 10000.0)); Recipe.coreVoxelSizeMicrons = static_cast(std::llround(CoreVoxelSizeCm * 10000.0)); diff --git a/Source/VoxelForge/Public/VoxelDensityPrimitives.h b/Source/VoxelForge/Public/VoxelDensityPrimitives.h index 7fb17f4..8e2c4c3 100644 --- a/Source/VoxelForge/Public/VoxelDensityPrimitives.h +++ b/Source/VoxelForge/Public/VoxelDensityPrimitives.h @@ -41,21 +41,27 @@ // SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée. // C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la // raison pour laquelle cette méthode existe. -FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ, - float StrateTopZ, float StrateBottomZ, - float Thickness, float BaseDensity) +FORCEINLINE void VF_ApplyUpperBoundarySealValue(float& Density, float WorldZ, + float StrateTopZ, float Thickness, float BaseDensity) { - if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return; - - const float DistTop = StrateTopZ - WorldZ; // + si on est sous le plafond - const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol - + const float DistTop = StrateTopZ - WorldZ; if (DistTop >= 0.0f && DistTop < Thickness) { float SealFactor = 1.0f - (DistTop / Thickness); SealFactor = SmoothStep01(SealFactor); Density = FMath::Max(Density, SealFactor * BaseDensity); } +} + +FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ, + float StrateTopZ, float StrateBottomZ, + float Thickness, float BaseDensity) +{ + if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return; + + const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol + + VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity); if (DistBot >= 0.0f && DistBot < Thickness) { float SealFactor = 1.0f - (DistBot / Thickness); @@ -64,6 +70,16 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ, } } +// The same structural seal, limited to a strate's upper boundary. Native finite fields use this +// when their lower boundary intentionally has no seal; keeping the math here makes that ceiling +// use the same shared density primitive as every two-sided strate seal. +FORCEINLINE void VF_ApplyUpperBoundarySeal(float& Density, float WorldZ, + float StrateTopZ, float Thickness, float BaseDensity) +{ + if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return; + VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity); +} + //============================================================================= // CARVE DE PASSAGE / PASSAGE CARVING //============================================================================= diff --git a/Standalone/Core/API.md b/Standalone/Core/API.md index a83d569..fa08deb 100644 --- a/Standalone/Core/API.md +++ b/Standalone/Core/API.md @@ -153,8 +153,13 @@ radius range, top flattening, taper, bounded contour warp, roughness, blend, con route constraints. `PlanFloatingIslandCluster` emits stable island and connector IDs, a connected cluster graph, conservative influence AABBs, and a centerline route. At least the primary chain is always planned; `islandDensityQ16` controls up to two optional side sites. Connectors carry a sharp-tag -ready bit for a later hybrid-mesher adapter. The schema accepts geometry version 1 and 2. Manifests -without a geometry version retain version-1 evaluation and canonical hashes. +ready bit for a later hybrid-mesher adapter. The schema accepts geometry versions 1 through 3. +Version 3 keeps the version-2 island form, removes the all-XY lower seal, and obtains its ceiling from +the shared upper-strate boundary seal. The resolved upper bound comes from the strate's own chunk +range; `boundarySealSubvoxels` sets the shared seal thickness and remains a planning margin for island +features. It does not define a FloatingIslands-specific ceiling height. Versions 1 and 2 retain their +recorded two-sided boundary seals. Manifests without a geometry version retain version-1 evaluation +and canonical hashes. `FloatingIslandFieldV1::densitySubvoxels` evaluates the canonical island and connector field. `interval` returns unknown for boxes overlapping any influence AABB and a certified all-air range diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index d46e08d..4c85c4d 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -392,7 +392,9 @@ namespace VoxelForgeCore std::uint32_t schemaVersion = 1; // Profile 1 preserves the original shallow pads. Profile 2 adds a thick crust, // bounded lobed roots and arched spans without changing the feature-family schema. - std::uint32_t geometryVersion = 2; + // Version 3 keeps the version-2 island shapes, removes the all-XY lower boundary seal, + // and routes its upper seal through the shared strate-boundary primitive. + std::uint32_t geometryVersion = 3; std::uint32_t unitsPerVoxel = static_cast(WorldAssemblySubvoxelsPerVoxel); // Import scale is explicit in microns; the reduced ratio maps owner-authored lengths to Core voxels. // L'echelle d'import est explicite en microns; le rapport reduit convertit les longueurs owner. diff --git a/Standalone/Core/src/FarShellsInternal.h b/Standalone/Core/src/FarShellsInternal.h index da08747..53622d9 100644 --- a/Standalone/Core/src/FarShellsInternal.h +++ b/Standalone/Core/src/FarShellsInternal.h @@ -864,6 +864,46 @@ struct FFarMeshColumnIndex return Hits; } + bool HasDownwardHit(double X,double Y,double Z,double Tolerance) const + { + const int32 CX=std::clamp(static_cast(std::floor((X-Origin.X)/Step)),0,Cells-1); + const int32 CY=std::clamp(static_cast(std::floor((Y-Origin.Y)/Step)),0,Cells-1); + const auto& Candidates=ByColumn[static_cast(CY)*Cells+CX]; + for(const int32 Triangle:Candidates) + { + const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1], + IC=Mesh.Triangles[Triangle*3+2]; + if(IA<0||IB<0||IC<0||IA>=Mesh.Normals.Num()||IB>=Mesh.Normals.Num() + ||IC>=Mesh.Normals.Num())continue; + if(Mesh.Normals[IA].Z+Mesh.Normals[IB].Z+Mesh.Normals[IC].Z>=-1.5)continue; + const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2); + const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2); + const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2); + const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX); + if(std::abs(Den)<1.0e-10)continue; + const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den; + const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den; + const double W=1.0-U-V; + if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue; + const double Hit=AZ+U*(BZ-AZ)+V*(CZ-AZ); + if(std::abs(Hit-Z)<=Tolerance)return true; + } + return false; + } + + bool HasDownwardCellCoverage(double X0,double Y0,double X1,double Y1, + double Z,double Tolerance) const + { + constexpr double Samples[3]={0.125,0.5,0.875}; + for(const double V:Samples)for(const double U:Samples) + { + const double X=X0+(X1-X0)*U; + const double Y=Y0+(Y1-Y0)*V; + if(!HasDownwardHit(X,Y,Z,Tolerance))return false; + } + return true; + } + private: double PositionAxis(int32 Index,int32 Axis) const { @@ -974,11 +1014,11 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta std::vector Boundaries; for(const FStrateSlot& Slot:Layout) { - const FStrateGenerationParams Params=World.Manager->GetGenerationParams( - FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2)); - const double HalfWidth=std::max(8.0,static_cast(Params.BoundarySealThickness)+8.0); - Boundaries.push_back({static_cast(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth}); - Boundaries.push_back({static_cast(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth}); + FStandaloneShapeWorld::FResolvedStratumBounds Resolved; + if(!Shape.GetResolvedStratumBounds(Slot,Resolved))continue; + const double HalfWidth=std::max(8.0,Resolved.SealThickness+8.0); + Boundaries.push_back({Resolved.BottomZ,HalfWidth}); + Boundaries.push_back({Resolved.TopZ,HalfWidth}); } std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z Crossing; + }; + std::vector Candidates; + for(const FStrateSlot& Slot:World.Manager->GetLayout()) + { + FCandidate Candidate; + if(!Shape.GetResolvedStratumBounds(Slot,Candidate.Bounds) + ||Candidate.Bounds.SealThickness<=0.0 + ||Candidate.Bounds.TopZOrigin.Z+Extent+1.0e-6)continue; + const double Epsilon=std::min(0.5,Candidate.Bounds.SealThickness*0.25); + if(Epsilon<=0.0)continue; + Candidate.Crossing.resize(static_cast(GridSide)*GridSide,0); + bool AnyCrossing=false; + for(int32 Y=0;Y(WorldX),static_cast(WorldY), + static_cast(Candidate.Bounds.TopZ-Epsilon)); + const float Above=Shape.DensityAt(static_cast(WorldX),static_cast(WorldY), + static_cast(Candidate.Bounds.TopZ+Epsilon)); + const bool bCrosses=std::isfinite(Below)&&std::isfinite(Above)&&Below<0.0f&&Above>0.0f; + Candidate.Crossing[static_cast(Y*GridSide+X)]=bCrosses?1:0; + AnyCrossing=AnyCrossing||bCrosses; + } + if(AnyCrossing)Candidates.push_back(std::move(Candidate)); + } + if(Candidates.empty())return; + + const int32 Cells=std::max(1,World.CoarseTileCells); + FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step); + const int32 OldVertexCount=Mesh.Vertices.Num(); + const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount); + const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount; + const bool bHasColors=Mesh.Colors.Num()==OldVertexCount; + const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255); + const double VoxelSizeCm=static_cast(World.CoreVoxelSizeMicrons)/10000.0; + std::vector NewVertices,NewNormals; + std::vector NewUVs; + std::vector NewColors; + std::vector NewTriangles; + for(const FCandidate& Candidate:Candidates) + { + std::vector VertexMap(static_cast(GridSide)*GridSide,INDEX_NONE); + const double CeilingZ=Candidate.Bounds.TopZ; + const auto AddVertex=[&](int32 X,int32 Y) + { + const std::size_t GridIndex=static_cast(Y*GridSide+X); + int32& Mapped=VertexMap[GridIndex]; + if(Mapped!=INDEX_NONE)return Mapped; + Mapped=static_cast(NewVertices.size()); + const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep; + NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,CeilingZ*VoxelSizeCm); + NewNormals.emplace_back(0.0,0.0,-1.0); + if(bHasUVs)NewUVs.emplace_back(static_cast(WorldX),static_cast(WorldY)); + if(bHasColors)NewColors.push_back(Color); + return Mapped; + }; + for(int32 Y=0;Y+1(Y*GridSide+X); + const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1; + if(!Candidate.Crossing[I00]||!Candidate.Crossing[I10] + ||!Candidate.Crossing[I01]||!Candidate.Crossing[I11])continue; + const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep; + const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep; + // Suppress a coincident plane only when it covers the full cell. A hit at just the + // center of a curved shell triangle is not enough to leave the rest of this cap cell open. + if(MeshIndex.HasDownwardCellCoverage(CellX0,CellY0,CellX1,CellY1,CeilingZ,0.001))continue; + const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y); + const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1); + NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C}); + } + } + if(NewTriangles.empty())return; + + const int32 AddedVertices=static_cast(NewVertices.size()); + auto InsertVertices=[&](auto& Existing,const auto& Added) + { + using TArrayType=std::remove_reference_t; + TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast(Added.size())); + for(int32 I=0;I=OldSurfaceVertices)Index+=AddedVertices; + const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3); + TArray CombinedTriangles;CombinedTriangles.Reserve( + Mesh.Triangles.Num()+static_cast(NewTriangles.size())); + for(int32 I=0;I ExistingFaceMasks=Mesh.TriangleSkirtFaceMasks; + ExistingFaceMasks.SetNum(OldTriangleCount); + TArray CombinedFaceMasks;CombinedFaceMasks.Reserve( + OldTriangleCount+static_cast(NewTriangles.size()/3)); + for(int32 Triangle=0;Triangle(NewTriangles.size()/3); +} diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h index fbb8555..b872f5a 100644 --- a/Standalone/Core/src/FloatingIslandsInternal.h +++ b/Standalone/Core/src/FloatingIslandsInternal.h @@ -266,7 +266,7 @@ namespace floating_island_detail ? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0; const std::uint32_t ExpectedDenominator = ScaleGcd > 0 ? recipe.coreVoxelSizeMicrons / ScaleGcd : 0; - if (recipe.schemaVersion != 1 || (recipe.geometryVersion != 1 && recipe.geometryVersion != 2) + if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 3 || (recipe.geometryVersion == 1 && (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0)) || recipe.unitsPerVoxel != Q diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index f05f7ee..deb2d87 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -5,6 +5,7 @@ #include "VoxelCaveMorphology.h" #include "VoxelNoise.h" #include "VoxelStrateManager.h" +#include "VoxelDensityPrimitives.h" #include "VoxelForgeCore/VoxelForgeCore.h" #include "VoxelForgeFieldShared.h" @@ -32,6 +33,13 @@ class FStandaloneShapeWorld { public: + struct FResolvedStratumBounds + { + double BottomZ=0.0; + double TopZ=0.0; + double SealThickness=0.0; + }; + double GetCoreVoxelSizeCm() const { return Owner == nullptr || Owner->CoreVoxelSizeMicrons == 0 @@ -47,6 +55,41 @@ public: return static_cast(CHUNK_SIZE) * OwnerVoxelSizeCm / GetCoreVoxelSizeCm(); } + static bool MakeResolvedStratumBounds(double BottomZ,double TopZ,double SealThickness, + FResolvedStratumBounds& Out) + { + if(!std::isfinite(BottomZ)||!std::isfinite(TopZ)||!std::isfinite(SealThickness) + ||TopZ<=BottomZ||SealThickness<0.0)return false; + Out={BottomZ,TopZ,SealThickness}; + return true; + } + + bool GetResolvedStratumBounds(const FStrateSlot& Slot,FResolvedStratumBounds& Out) const + { + if(!Owner||!Owner->Manager)return false; + const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2; + if(bUseNativeManifestFields&&WorldAssembly + &&Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,MidChunkZ)) + ==ECaveGeneratorType::FloatingIslands) + { + const auto* Plan=FindWorldAssemblyFloatingIslandStratum(Slot.StrateIndex,MidChunkZ); + if(Plan&&Plan->manifest.recipe.geometryVersion>=3) + { + constexpr double Scale=static_cast( + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + return MakeResolvedStratumBounds( + static_cast(Plan->manifest.recipe.layoutMinimumZSubvoxels)/Scale, + static_cast(Plan->manifest.recipe.layoutMaximumZSubvoxels)/Scale, + static_cast(Plan->manifest.recipe.boundarySealSubvoxels)/Scale,Out); + } + } + const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams( + FIntVector(0,0,MidChunkZ)); + return MakeResolvedStratumBounds(static_cast(Slot.BottomChunkZ)*CHUNK_SIZE, + static_cast(Slot.TopChunkZ+1)*CHUNK_SIZE, + static_cast(Params.BoundarySealThickness),Out); + } + enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel }; struct FBoxClassification { @@ -714,12 +757,12 @@ public: const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast(Cells)*Step; for(const FStrateSlot& Slot:Owner->Manager->GetLayout()) { - const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams( - FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2)); + FResolvedStratumBounds Bounds; + if(!GetResolvedStratumBounds(Slot,Bounds))continue; const float HalfWidth=static_cast(std::max(8.0, - static_cast(Params.BoundarySealThickness)+8.0)); - const float Boundaries[2]={static_cast(Slot.BottomChunkZ)*CHUNK_SIZE, - static_cast(Slot.TopChunkZ+1)*CHUNK_SIZE}; + Bounds.SealThickness+8.0)); + const float Boundaries[2]={static_cast(Bounds.BottomZ), + static_cast(Bounds.TopZ)}; for(const float Z:Boundaries) if(Z+HalfWidth>=TileZ0&&Z-HalfWidth<=TileZ1) Out.stratumBoundaries.push_back({Z,HalfWidth}); @@ -1549,13 +1592,18 @@ public: const std::int64_t Seal=IslandPlan->manifest.recipe.boundarySealSubvoxels; const std::int64_t BoxBottom=static_cast(BMin[2])*Scale; const std::int64_t BoxTop=static_cast(BMax[2])*Scale; - const bool bBottomSealOverlap=BoxBottom=Bottom; - const bool bTopSealOverlap=BoxBottom<=Top&&BoxTop>Top-Seal; + const bool bSharedTopSealEnabled=IslandPlan->manifest.recipe.geometryVersion<3 + ||!VoxelDensityAblation::IsBoundarySealOff(); + const bool bTopSealOverlap=bSharedTopSealEnabled + &&BoxBottom<=Top&&BoxTop>Top-Seal; + const bool bBottomSealOverlap=IslandPlan->manifest.recipe.geometryVersion<3 + &&BoxBottom=Bottom; if(bBottomSealOverlap) return Finish(BoxBottom>=Bottom&&BoxTop<=Bottom+Seal ?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed); if(bTopSealOverlap) - return Finish(BoxBottom>=Top-Seal&&BoxTop<=Top + return Finish((IslandPlan->manifest.recipe.geometryVersion<3 + ?BoxBottom>=Top-Seal:BoxBottom>Top-Seal)&&BoxTop<=Top ?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed); const VoxelForgeCore::FloatingIslandFieldV1 Block( IslandPlan->manifest.recipe,IslandPlan->manifest.plan); @@ -2130,9 +2178,28 @@ private: const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels; const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels; const std::int64_t Zq=ToFixed(Z); - if(Zq>=Bottom&&Zq(1,Zq-Bottom)); - if(Zq<=Top&&Zq>Top-Seal)Density=std::min(Density,-std::max(1,Top-Zq)); - OutDensity=static_cast(static_cast(Density)/Scale); + if(Plan->manifest.recipe.geometryVersion<3) + { + if(Zq>=Bottom&&Zq(1,Zq-Bottom)); + if(Zq<=Top&&Zq>Top-Seal) + Density=std::min(Density,-std::max(1,Top-Zq)); + OutDensity=static_cast(static_cast(Density)/Scale); + } + else + { + OutDensity=static_cast(static_cast(Density)/Scale); + FResolvedStratumBounds Bounds; + if(!MakeResolvedStratumBounds(static_cast(Bottom)/Scale, + static_cast(Top)/Scale,static_cast(Seal)/Scale,Bounds)) + return false; + const FFloatingIslandParams Params=Owner->Manager->GetFloatingIslandParamsForChunk( + FIntVector(0,0,ChunkZ)); + float InternalDensity=-OutDensity; + VF_ApplyUpperBoundarySeal(InternalDensity,Z,static_cast(Bounds.TopZ), + static_cast(Bounds.SealThickness),Params.BaseDensity); + OutDensity=-InternalDensity; + } return std::isfinite(OutDensity); } @@ -2351,19 +2418,23 @@ private: int32 Top=MIN_int32,Bottom=MAX_int32; for(const FStrateSlot& Slot:Layout) { - Top=std::max(Top,Slot.TopChunkZ);Bottom=std::min(Bottom,Slot.BottomChunkZ); + FResolvedStratumBounds Bounds; + if(!GetResolvedStratumBounds(Slot,Bounds))continue; + const int32 ResolvedBottom=FMath::FloorToInt(static_cast(Bounds.BottomZ)); + const int32 ResolvedTop=FMath::CeilToInt(static_cast(Bounds.TopZ)); + Top=std::max(Top,ResolvedTop);Bottom=std::min(Bottom,ResolvedBottom); if(!Slot.Definition)continue; const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2; FLayerField Field; - Field.Lo=Slot.BottomChunkZ*CHUNK_SIZE; - Field.Hi=(Slot.TopChunkZ+1)*CHUNK_SIZE; + Field.Lo=ResolvedBottom; + Field.Hi=ResolvedTop; Field.StrateIndex=Slot.StrateIndex; Field.Params=Owner->Manager->GetGenerationParams(FIntVector(0,0,MidChunkZ)); LayerFields.push_back(std::move(Field)); } if(Top>=Bottom) { - LayerRanges.push_back({Bottom*CHUNK_SIZE,(Top+1)*CHUNK_SIZE}); + LayerRanges.push_back({Bottom,Top}); } } diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 1952e96..0ff684e 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -2204,6 +2204,8 @@ namespace VoxelForgeCore out.diagnostics.horizonGroundTrianglesRemoved=GroundStats.RemovedTriangles; out.diagnostics.horizonCapTriangles=CapStats.Triangles; } + if(impl_->worldAssembly&&!bOverlapsPlayerEdit) + AppendStratumBoundaryCeilings(world,farShape,nativeKey,step,mesh); out.densitySampleCount=static_cast(std::max(0,sampleCount)); if(bHasWorldAssembly) diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/BuildResources.md b/Standalone/Research/WorldAssemblerM3/Linux18/BuildResources.md new file mode 100644 index 0000000..5371e96 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/BuildResources.md @@ -0,0 +1,9 @@ +# L18 build resources + +Limits: CPUs 0–11, `-j12`, normal priority, 6 GiB address-space cap. Governor stayed `powersave`. +These are the final builds after the cell-coverage and owner ceiling corrections. + +- Clang 19, `-ffp-contract=off`: exit 0, 31.689 s, peak process RSS 436,088 KiB, peak VMS 586,240 KiB. +- GCC 14, default FP contraction: exit 0, 48.648 s, peak process RSS 903,168 KiB, peak VMS 1,014,716 KiB. +- Largest measured runtime process was the final Clang gate run at 897,276 KiB RSS. The determinism matrix peaked at 603,172 KiB and the crossing matrix at 548,664 KiB. +- An earlier GCC build was interrupted by owner steering before producing a binary; the final GCC build above completed successfully. diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/CaptureLinux18.py b/Standalone/Research/WorldAssemblerM3/Linux18/CaptureLinux18.py new file mode 100644 index 0000000..53cd38e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/CaptureLinux18.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Render the matched L18 review views as small readable JPEGs.""" +import os +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +CAPTURE = ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-linux18-clang" +EXPORTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux18/exports" +OUT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux18/captures" +LOGS = ROOT / "Saved/Linux" +OUT.mkdir(parents=True, exist_ok=True) +env = os.environ.copy() +env["LP_NUM_THREADS"] = "12" +env["XDG_CACHE_HOME"] = str(ROOT / "Saved/Linux/mesa-cache") + +views = [ + ("floating-islands-flat-square-after", "floating-islands-flat-square.vfmesh", + (-11.612444, -118.221237, -19.8452), 0, 0, True), + ("floating-islands-flat-square-no-edges", "floating-islands-flat-square.vfmesh", + (-11.612444, -118.221237, -19.8452), 0, 0, False), + ("floating-islands-island-view-after", "floating-islands-island-view.vfmesh", + (-22, -19, -22.8), 0, -4, True), + ("floating-islands-ceiling-up", "floating-islands-island-view.vfmesh", + (-22, -19, -22.8), 0, 80, True), + ("surface-world-l1-spawn-after", "surface-world-l1-spawn.vfmesh", + (0, -35, -487.94), 0, -22, False), + ("surface-world-horizon-after", "surface-world-l1-spawn.vfmesh", + (0, -35, -487.94), 0, 0, False), +] + +for name, scene_name, camera, yaw, pitch, tile_edges in views: + scene = EXPORTS / scene_name + png = LOGS / f"l18-{name}.png" + jpg = OUT / f"{name}.jpg" + render_log = LOGS / f"l18-{name}-render.log" + convert_log = LOGS / f"l18-{name}-jpeg.log" + command = [str(CAPTURE), str(scene), str(png), "--size=1600x900", + "--shade=readable", f"--tile-edges={'on' if tile_edges else 'off'}", + "--tile-edge-width-m=0.15", "--camera-m="+",".join(map(str, camera)), + f"--yaw-deg={yaw}", f"--pitch-deg={pitch}"] + with render_log.open("w", encoding="utf-8") as stream: + subprocess.run(command, cwd=ROOT, env=env, stdout=stream, + stderr=subprocess.STDOUT, check=True) + ffmpeg = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y", + "-i", str(png), "-frames:v", "1", "-q:v", "4", str(jpg)] + with convert_log.open("w", encoding="utf-8") as stream: + subprocess.run(ffmpeg, cwd=ROOT, env=env, stdout=stream, + stderr=subprocess.STDOUT, check=True) + if jpg.stat().st_size > 300_000: + raise SystemExit(f"{jpg.name} exceeds the 300 KB review limit") + print(f"[Capture] {jpg.relative_to(ROOT)} bytes={jpg.stat().st_size} status=PASS") diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/CeilingContinuity.py b/Standalone/Research/WorldAssemblerM3/Linux18/CeilingContinuity.py new file mode 100644 index 0000000..bf9ba7f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/CeilingContinuity.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Audit the emitted FloatingIslands roof faces and same-LOD tile-edge continuity.""" +import array +import collections +import math +import struct +import sys + + +def read_exact(stream, size): + data = stream.read(size) + if len(data) != size: + raise EOFError(f"wanted {size} bytes, got {len(data)}") + return data + + +def height_at(triangles, x, y, preferred_z): + best = None + for (x0, y0, z0), (x1, y1, z1), (x2, y2, z2) in triangles: + den = (y1-y2)*(x0-x2)+(x2-x1)*(y0-y2) + if abs(den) < 1.0e-10: + continue + a = ((y1-y2)*(x-x2)+(x2-x1)*(y-y2))/den + b = ((y2-y0)*(x-x2)+(x0-x2)*(y-y2))/den + c = 1.0-a-b + if min(a,b,c) >= -1.0e-5: + z = a*z0+b*z1+c*z2 + if best is None or abs(z-preferred_z) < abs(best-preferred_z): + best = z + return best + + +def quantile(values, fraction): + if not values: + return math.nan + ordered = sorted(values) + return ordered[math.floor(fraction*(len(ordered)-1))] + + +if len(sys.argv) != 3: + raise SystemExit("usage: CeilingContinuity.py scene.vfmesh expected-cap-plane-metres") +scene_path = sys.argv[1] +root_z = float(sys.argv[2]) +tiles = {} +voxel_size_m = None +with open(scene_path, "rb") as stream: + if read_exact(stream, 8) != b"VFMSH001": + raise SystemExit("invalid VFMSH001 scene") + version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12)) + camera = struct.unpack("<3d", read_exact(stream, 24)) + yaw, pitch = struct.unpack("<2d", read_exact(stream, 16)) + voxel_size_m = voxel_um/1_000_000.0 + for _ in range(tile_count): + key = struct.unpack("<4i", read_exact(stream, 16)) + surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack( + "<5I", read_exact(stream, 20)) + positions = array.array("f") + positions.frombytes(read_exact(stream, vertex_count*12)) + normals = array.array("f") + normals.frombytes(read_exact(stream, vertex_count*12)) + indices = array.array("I") + indices.frombytes(read_exact(stream, index_count*4)) + masks = read_exact(stream, mask_count) + if sys.byteorder != "little": + positions.byteswap() + normals.byteswap() + indices.byteswap() + extent = 32*(1 << key[3])*voxel_size_m + z0, z1 = key[2]*extent, (key[2]+1)*extent + ceiling = [] + for triangle in range(index_count//3): + if masks and masks[triangle] != 0: + continue + a,b,c = indices[triangle*3:triangle*3+3] + if max(a,b,c) >= surface_vertices: + continue + nz = (normals[a*3+2]+normals[b*3+2]+normals[c*3+2])/3.0 + if nz > -0.65: + continue + points = tuple((positions[v*3],positions[v*3+1],positions[v*3+2]) + for v in (a,b,c)) + center_z = sum(point[2] for point in points)/3.0 + if abs(center_z-root_z) <= 1.0: + ceiling.append(points) + tiles[key] = { + "extent": extent, "x0": key[0]*extent, "y0": key[1]*extent, + "x1": (key[0]+1)*extent, "y1": (key[1]+1)*extent, + "z0": z0, "z1": z1, "ceiling": ceiling, + } + +by_lod = collections.defaultdict(dict) +for key, tile in tiles.items(): + if tile["z0"] <= root_z+1.0 and tile["z1"] >= root_z-1.0: + by_lod[key[3]][key] = tile + +print(f"scene={scene_path} version={version} voxel_um={voxel_um} tiles={tile_count} " + f"camera_m={camera} yaw={yaw} pitch={pitch} expected_cap_m={root_z:.9f}") +print("lod zband_tiles cap_tiles missing_tiles cap_triangles face_z_m_p05/median/p95 " + "tile_samples hit_samples max_abs_root_delta_cm shared_edges shared_hits edge_gaps " + "edge_delta_cm_median/p95/max") +all_pass = True +for lod in range(9): + target = by_lod[lod] + cap_tiles = {key: tile for key,tile in target.items() if tile["ceiling"]} + face_z = [point[2] for tile in cap_tiles.values() for tri in tile["ceiling"] + for point in tri] + deltas = [] + sample_count = 0 + hit_count = 0 + for key,tile in target.items(): + if not tile["ceiling"]: + continue + for iy in range(16): + for ix in range(16): + x = tile["x0"]+(ix+0.5)*tile["extent"]/16 + y = tile["y0"]+(iy+0.5)*tile["extent"]/16 + sample_count += 1 + height = height_at(tile["ceiling"],x,y,root_z) + if height is not None: + hit_count += 1 + deltas.append(abs(height-root_z)*100.0) + + edge_deltas = [] + edge_count = 0 + edge_gaps = 0 + for key,a in cap_tiles.items(): + x,y,z,level = key + for neighbor_key,axis in (((x+1,y,z,level),"x"),((x,y+1,z,level),"y")): + b = cap_tiles.get(neighbor_key) + if b is None: + continue + edge_count += 1 + epsilon = min(0.02,a["extent"]*1.0e-4) + matched = 0 + for index in range(33): + t = (index+0.5)/33 + if axis == "x": + fixed = a["x1"] + varying = a["y0"]+(a["y1"]-a["y0"])*t + za = height_at(a["ceiling"],fixed-epsilon,varying,root_z) + zb = height_at(b["ceiling"],fixed+epsilon,varying,root_z) + else: + fixed = a["y1"] + varying = a["x0"]+(a["x1"]-a["x0"])*t + za = height_at(a["ceiling"],varying,fixed-epsilon,root_z) + zb = height_at(b["ceiling"],varying,fixed+epsilon,root_z) + if za is not None and zb is not None: + edge_deltas.append(abs(za-zb)*100.0) + matched += 1 + if matched == 0: + edge_gaps += 1 + missing = len(target)-len(cap_tiles) + max_root = max(deltas) if deltas else math.nan + print(f"{lod} {len(target)} {len(cap_tiles)} {missing} " + f"{sum(len(tile['ceiling']) for tile in cap_tiles.values())} " + f"{quantile(face_z,.05):.6f}/{quantile(face_z,.5):.6f}/{quantile(face_z,.95):.6f} " + f"{sample_count} {hit_count} {max_root:.3f} {edge_count} {len(edge_deltas)} " + f"{edge_gaps} {quantile(edge_deltas,.5):.3f}/{quantile(edge_deltas,.95):.3f}/" + f"{max(edge_deltas) if edge_deltas else math.nan:.3f}") + if lod >= 4 and target and (missing or hit_count != sample_count or edge_gaps): + all_pass = False +print(f"[CeilingContinuity] result={'PASS' if all_pass else 'FAIL'} " + "criteria=LOD4-8 requested z-band tiles emit an upper-boundary ceiling; 16x16 ray samples hit; " + "shared same-LOD tile edges have paired hits") +raise SystemExit(0 if all_pass else 1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/CheckCrossingHashes.py b/Standalone/Research/WorldAssemblerM3/Linux18/CheckCrossingHashes.py new file mode 100644 index 0000000..51e13e4 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/CheckCrossingHashes.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Compare all L18 crossing configurations to the L14 24 m crossing hashes.""" +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +RUNS = ROOT / "Saved/Linux/l18-crossing" +PATTERN = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +failed = [] +logs = sorted(RUNS.glob("*-w*-r*.log")) +for path in logs: + parts = path.stem.split("-") + compiler = parts[0] + reference_compiler = "clang" if compiler == "clang" else "gcc" + reference_path = ROOT / f"Saved/Linux/l14-final-crossing-24m-{reference_compiler}-w12.log" + expected = PATTERN.findall(reference_path.read_text(encoding="utf-8")) + actual = PATTERN.findall(path.read_text(encoding="utf-8", errors="replace")) + if actual != expected: + failed.append(path.name) + print(f"[CrossingReferenceMismatch] {path.name} expected_rows={len(expected)} " + f"actual_rows={len(actual)}") +print(f"[CrossingSurfaceHashGate] reference=L14-final-crossing-24m-W12 rows={len(logs)} " + f"result={'FAIL' if failed else 'PASS'}") +raise SystemExit(1 if failed or len(logs) != 14 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/CheckSurfaceHashes.py b/Standalone/Research/WorldAssemblerM3/Linux18/CheckSurfaceHashes.py new file mode 100644 index 0000000..82f5708 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/CheckSurfaceHashes.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Compare every L18 SurfaceWorld matrix hash row with the approved L14 reference.""" +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +RUNS = ROOT / "Saved/Linux/l18-matrix" +REFERENCE = ROOT / "Saved/Linux/l14-final-hash-sw-clang-off-w12-r5.log" +PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +expected = PATTERN.findall(REFERENCE.read_text(encoding="utf-8")) +if not expected or len(expected) != 12: + raise SystemExit(f"invalid L14 reference hash rows: {len(expected)}") +logs = sorted(RUNS.glob("surface-world-*.log")) +if len(logs) != 14: + raise SystemExit(f"expected 14 SurfaceWorld matrix logs, found {len(logs)}") +failed = [] +for path in logs: + actual = PATTERN.findall(path.read_text(encoding="utf-8", errors="replace")) + if actual != expected: + failed.append(path.name) + print(f"[SurfaceHashMismatch] {path.name} expected={len(expected)} actual={len(actual)}") +print(f"[SurfaceHashGate] reference={REFERENCE.name} rows={len(logs)} expected_rows=14 " + f"result={'FAIL' if failed else 'PASS'}") +raise SystemExit(1 if failed else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/FloatingIslandDensityProbe.cpp b/Standalone/Research/WorldAssemblerM3/Linux18/FloatingIslandDensityProbe.cpp new file mode 100644 index 0000000..60b761b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/FloatingIslandDensityProbe.cpp @@ -0,0 +1,149 @@ +#include "Json.h" +#include "OwnerWorldLoader.h" +#include "StandaloneCoreScheduler.h" +#include "WorldAssemblyPrototype.h" +#include "StandaloneSnapshotAdapter.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr double CoreVoxelsPerMeter=4.0; + constexpr std::int64_t SubvoxelsPerCoreVoxel= + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + + void PrintDensity(VoxelForgeCore::World& World,const char* Label,double X,double Y,double Z) + { + const VoxelForgeCore::Vec3d Point{X*CoreVoxelsPerMeter,Y*CoreVoxelsPerMeter, + Z*CoreVoxelsPerMeter}; + const double Density=World.densityAt(Point); + const double Raw=World.unclampedDensityAt(Point); + std::printf("[DensitySample] label=%s xyz_m=%.6f,%.6f,%.6f density_core_vox=%.9f " + "density_cm=%.4f unclamped_core_vox=%.9f sign=%s\n",Label,X,Y,Z,Density, + Density*25.0,Raw,Density<0.0?"solid":"air"); + } + + void PrintCrossing(VoxelForgeCore::World& World,const char* Label,double X,double Y, + double BottomM,double TopM) + { + const auto DensityAt=[&](double Z) + { + return World.densityAt({X*CoreVoxelsPerMeter,Y*CoreVoxelsPerMeter, + Z*CoreVoxelsPerMeter}); + }; + double Lo=BottomM,Hi=TopM; + double LoDensity=DensityAt(Lo),HiDensity=DensityAt(Hi); + if((LoDensity<0.0)==(HiDensity<0.0)) + { + std::printf("[DensityCrossing] label=%s bracket_m=%.6f..%.6f signs=%s/%s result=none\n", + Label,Lo,Hi,LoDensity<0.0?"solid":"air",HiDensity<0.0?"solid":"air"); + return; + } + for(int Iteration=0;Iteration<64;++Iteration) + { + const double Mid=(Lo+Hi)*0.5; + const double MidDensity=DensityAt(Mid); + if((MidDensity<0.0)==(LoDensity<0.0)) + {Lo=Mid;LoDensity=MidDensity;} + else + {Hi=Mid;HiDensity=MidDensity;} + } + std::printf("[DensityCrossing] label=%s z_m=%.9f bracket_m=%.6f..%.6f " + "density_core_vox=%.9f/%.9f\n",Label,(Lo+Hi)*0.5,BottomM,TopM, + LoDensity,HiDensity); + } +} + +int main(int argc,char** argv) +{ + if(argc!=2) + { + std::fprintf(stderr,"usage: %s owner-world.json\n",argv[0]); + return 2; + } + VFJson::Value Root; + std::string Error; + { + std::ifstream Input(argv[1],std::ios::binary); + if(!Input){std::fprintf(stderr,"owner JSON open failed\n");return 2;} + std::string Text{std::istreambuf_iterator(Input),std::istreambuf_iterator{}}; + if(!VFJson::Parser(std::move(Text)).Parse(Root,Error)) + {std::fprintf(stderr,"owner JSON parse failed: %s\n",Error.c_str());return 2;} + } + if(!StandaloneWorldAssemblyPrototype::SelectTopStrateAsFloatingIslands(Root,Error)) + {std::fprintf(stderr,"FI selection failed: %s\n",Error.c_str());return 2;} + auto Owner=std::make_shared(); + if(!StandaloneOwnerLoader::Resolve(Root,*Owner,Error)) + {std::fprintf(stderr,"owner resolve failed: %s\n",Error.c_str());return 2;} + VoxelForgeCore::WorldAssemblyManifest Assembly; + if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype( + *Owner,Root,Error,{},&Assembly) + ||!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly( + *Owner,Root,Assembly,Error)) + {std::fprintf(stderr,"plan construction failed: %s\n",Error.c_str());return 2;} + auto Snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(Root,Owner->Seed,3,8, + false,false,Error); + if(!Snapshot) + {std::fprintf(stderr,"snapshot resolve failed: %s\n",Error.c_str());return 2;} + const auto* ResolvedAssembly=Snapshot.worldAssembly(); + if(!ResolvedAssembly||ResolvedAssembly->floatingIslands.empty()) + {std::fprintf(stderr,"resolved FloatingIslands plan missing\n");return 2;} + + VoxelForgeCore::World World(std::move(Snapshot)); + VoxelForgeCore::ShapeBuildRequest Request; + Request.minVoxel={-96,-512,-288}; + Request.maxVoxel={96,96,64}; + Request.clipMinVoxel=Request.minVoxel; + Request.clipMaxVoxel=Request.maxVoxel; + Request.workerLimit=12; + Request.includeFarWorld=false; + FStandaloneCoreScheduler Scheduler; + if(!World.compile(Request,Scheduler,Error)) + {std::fprintf(stderr,"shape compile failed: %s\n",Error.c_str());return 2;} + + const auto& Plan=ResolvedAssembly->floatingIslands.front(); + const auto& Recipe=Plan.manifest.recipe; + std::printf("[FloatingIslandBounds] geometry_version=%u owner_voxel_um=%u core_voxel_um=%u " + "owner_to_core=%u/%u chunks=%d..%d layout_z_m=%.6f..%.6f ceiling_start_m=%.6f " + "seal_m=%.6f bottom_cap=%s\n",Recipe.geometryVersion,Recipe.ownerVoxelSizeMicrons, + Recipe.coreVoxelSizeMicrons,Recipe.ownerToCoreScaleNumerator, + Recipe.ownerToCoreScaleDenominator,Plan.topChunkZ,Plan.bottomChunkZ, + static_cast(Recipe.layoutMinimumZSubvoxels)/SubvoxelsPerCoreVoxel/4.0, + static_cast(Recipe.layoutMaximumZSubvoxels)/SubvoxelsPerCoreVoxel/4.0, + static_cast(Recipe.layoutMaximumZSubvoxels-Recipe.boundarySealSubvoxels) + /SubvoxelsPerCoreVoxel/4.0, + static_cast(Recipe.boundarySealSubvoxels)/SubvoxelsPerCoreVoxel/4.0, + Recipe.geometryVersion<3?"legacy":"removed"); + std::printf("[DensityProbe] flat_square_xy_m=-11.612444,-118.221237 " + "compiled=%d core_voxel_size_cm=25\n",World.isCompiled()?1:0); + constexpr double X=-11.612443923950195; + constexpr double Y=-118.22123718261719; + for(double Z:{2.75,2.99,3.00,3.01,3.25,3.49,3.50,3.51,3.75,3.99,4.00,4.01, + 4.05,4.125,4.25,4.50,5.00, + 7.75,8.00,8.25,-60.25,-60.00,-59.75,-59.50,-59.25}) + PrintDensity(World,"flat-square",X,Y,Z); + PrintCrossing(World,"shared-upper-ceiling",X,Y,3.50,3.75); + PrintCrossing(World,"upper-boundary-cap",X,Y,4.00,4.25); + PrintCrossing(World,"old-eight-metre-wrapper",X,Y,7.75,8.25); + PrintCrossing(World,"removed-lower-boundary-seal",X,Y,-60.25,-59.75); + for(const auto& Probe:std::array,3>{{ + {{-66.3,-38.7}},{{-62.3,-54.6}},{{-53.34,-22.84}}}}) + { + std::printf("[DensityProbeXY] xy_m=%.3f,%.3f\n",Probe[0],Probe[1]); + for(double Z:{3.49,3.50,3.51,3.75,3.875,3.99,4.00,4.01,4.05,4.125}) + PrintDensity(World,"boundary-ray",Probe[0],Probe[1],Z); + PrintCrossing(World,"boundary-ray",Probe[0],Probe[1],3.49,4.125); + } + for(const auto& Probe:std::array,4>{{ + {{-67.0,-39.0}},{{-66.0,-39.0}},{{-67.0,-38.0}},{{-66.0,-38.0}}}}) + { + PrintDensity(World,"boundary-grid-node",Probe[0],Probe[1],3.875); + PrintDensity(World,"boundary-grid-node",Probe[0],Probe[1],4.125); + } + return 0; +} diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/MeasureBuild.py b/Standalone/Research/WorldAssemblerM3/Linux18/MeasureBuild.py new file mode 100644 index 0000000..a1b28e3 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/MeasureBuild.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Measure peak per-process RSS for a build process tree under the L17 limits.""" +import os +import subprocess +import sys +import time +from pathlib import Path + + +def descendants(root): + pending = [root] + seen = set() + while pending: + pid = pending.pop() + if pid in seen: + continue + seen.add(pid) + try: + children = Path(f"/proc/{pid}/task/{pid}/children").read_text().split() + pending.extend(int(child) for child in children) + except (FileNotFoundError, PermissionError, ProcessLookupError): + pass + return seen + + +def status(pid): + try: + fields = {} + for line in Path(f"/proc/{pid}/status").read_text().splitlines(): + if line.startswith(("Name:", "VmRSS:", "VmSize:")): + key, *value = line.split() + fields[key[:-1]] = value[0] if value else "" + return (int(fields.get("VmRSS", "0")), int(fields.get("VmSize", "0")), + fields.get("Name", "")) + except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError): + return None + + +if len(sys.argv) < 2: + raise SystemExit("usage: MeasureBuild.py command [arguments ...]") +if not os.sched_getaffinity(0).issubset(set(range(12))): + raise SystemExit("run under taskset -c 0-11") + +started = time.monotonic() +process = subprocess.Popen(sys.argv[1:], start_new_session=True) +peak_rss = (0, 0, "") +peak_vms = (0, 0, "") +while process.poll() is None: + for pid in descendants(process.pid): + measured = status(pid) + if measured is None: + continue + rss, vms, name = measured + if rss > peak_rss[0]: + peak_rss = (rss, pid, name) + if vms > peak_vms[0]: + peak_vms = (vms, pid, name) + time.sleep(0.1) + +exit_code = process.wait() +elapsed = time.monotonic() - started +print(f"[BuildTreeResources] exit_code={exit_code} wall_s={elapsed:.3f} " + f"peak_process_rss_kib={peak_rss[0]} peak_rss_pid={peak_rss[1]} " + f"peak_rss_name={peak_rss[2]} peak_process_vms_kib={peak_vms[0]} " + f"peak_vms_pid={peak_vms[1]} peak_vms_name={peak_vms[2]} " + "monitor_interval_s=0.1", flush=True) +raise SystemExit(exit_code) diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/MeshProbe.py b/Standalone/Research/WorldAssemblerM3/Linux18/MeshProbe.py new file mode 100644 index 0000000..414062b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/MeshProbe.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Summarize exported tile geometry and upward-facing ground for L17.""" +import array +import collections +import math +import struct +import sys + + +def read_exact(stream, size): + data = stream.read(size) + if len(data) != size: + raise EOFError(f"wanted {size} bytes, got {len(data)}") + return data + + +path = sys.argv[1] +with open(path, "rb") as stream: + if read_exact(stream, 8) != b"VFMSH001": + raise SystemExit("bad VFMSH001 header") + version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12)) + camera = struct.unpack("<3d", read_exact(stream, 24)) + yaw, pitch = struct.unpack("<2d", read_exact(stream, 16)) + per_lod = collections.defaultdict(lambda: {"tiles": 0, "vertices": 0, + "triangles": 0, "surface_triangles": 0, "up_triangles": 0, + "up_centroids": [], "tile_spans": [], "empty": 0}) + tile_rows = [] + for _ in range(tile_count): + key = struct.unpack("<4i", read_exact(stream, 16)) + surf_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack( + "<5I", read_exact(stream, 20)) + p = array.array("f") + p.frombytes(read_exact(stream, vertex_count * 12)) + n = array.array("f") + n.frombytes(read_exact(stream, vertex_count * 12)) + if sys.byteorder != "little": + p.byteswap(); n.byteswap() + indices = array.array("I") + indices.frombytes(read_exact(stream, index_count * 4)) + masks = read_exact(stream, mask_count) + if sys.byteorder != "little": + indices.byteswap() + lod = key[3] + row = per_lod[lod] + row["tiles"] += 1 + row["vertices"] += vertex_count + row["triangles"] += index_count // 3 + row["surface_triangles"] += index_count // 3 - skirt_count + up = [] + for tri in range(index_count // 3): + if masks and masks[tri] != 0: + continue + a, b, c = indices[tri * 3:tri * 3 + 3] + if max(a, b, c) >= surf_vertices: + continue + if (n[a * 3 + 2] + n[b * 3 + 2] + n[c * 3 + 2]) / 3.0 < 0.65: + continue + up.append((p[a * 3 + 2] + p[b * 3 + 2] + p[c * 3 + 2]) / 3.0) + row["up_triangles"] += len(up) + row["up_centroids"].extend(up) + if up: + up.sort() + span = up[math.floor(0.95 * (len(up) - 1))] - up[math.floor(0.05 * (len(up) - 1))] + row["tile_spans"].append(span) + tile_rows.append((key, vertex_count, index_count // 3, skirt_count, + len(up), up[0], up[len(up) // 2], up[-1], span)) + else: + row["empty"] += 1 + tile_rows.append((key, vertex_count, index_count // 3, skirt_count, + 0, math.nan, math.nan, math.nan, math.nan)) + +print(f"version={version} voxel_um={voxel_um} tiles={tile_count} camera_m={camera} yaw={yaw} pitch={pitch}") +print("lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles " + "upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max") +for lod in sorted(per_lod): + r = per_lod[lod] + z = sorted(r["up_centroids"]) + s = sorted(r["tile_spans"]) + def q(values, fraction): + return values[math.floor(fraction * (len(values) - 1))] if values else math.nan + print(f"{lod} {r['tiles']} {r['tiles']-r['empty']} {r['vertices']} {r['triangles']} " + f"{sum(row[3] for row in tile_rows if row[0][3] == lod)} {r['up_triangles']} " + f"{q(z,.05):.4f}/{q(z,.5):.4f}/{q(z,.95):.4f} " + f"{q(s,.5):.4f}/{q(s,.95):.4f}/{max(s) if s else math.nan:.4f}") + +print("near_camera_up_tiles (sorted by XY tile-centre distance):") +near = [] +for row in tile_rows: + key = row[0] + lod = key[3] + extent = 32 * (1 << lod) * voxel_um / 1_000_000.0 + cx = (key[0] + 0.5) * extent + cy = (key[1] + 0.5) * extent + near.append((math.hypot(cx-camera[0], cy-camera[1]), row)) +for distance, row in sorted(near)[:30]: + key, verts, tris, skirts, ups, lo, med, hi, span = row + if ups: + print(f"key={key} xy_distance_m={distance:.2f} vertices={verts} triangles={tris} " + f"skirt_triangles={skirts} upward_triangles={ups} z_m={lo:.3f}/{med:.3f}/{hi:.3f} " + f"tile_span_p05_p95_m={span:.3f}") diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/ProbeCaptureRays.py b/Standalone/Research/WorldAssemblerM3/Linux18/ProbeCaptureRays.py new file mode 100755 index 0000000..2bfe969 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/ProbeCaptureRays.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Trace review-image pixels through the exported clipmap mesh.""" +import array +import math +import struct +import sys + + +def read_exact(f, count): + data = f.read(count) + if len(data) != count: + raise EOFError((len(data), count)) + return data + + +def floor_div(n, d): + q, r = divmod(n, d) + return q - (r < 0) + + +def visible_mask(key, desired): + x, y, z, lod = key + extent = 32 << lod + origin = (x * extent, y * extent, z * extent) + mask = 0 + for axis in range(3): + for side in range(2): + probe = [origin[i] + extent // 2 for i in range(3)] + probe[axis] = origin[axis] + (extent if side else -1) + neighbor = None + for level in range(9): + neighbor_extent = 32 << level + coord = tuple(floor_div(probe[i], neighbor_extent) for i in range(3)) + (level,) + if coord in desired: + neighbor = level + break + if neighbor is None or neighbor != lod: + mask |= 1 << (axis * 2 + side) + return mask + + +def ray_box(origin, direction, low, high): + lo, hi = 0.1, 20000.0 + for axis in range(3): + if abs(direction[axis]) < 1e-12: + if origin[axis] < low[axis] or origin[axis] > high[axis]: + return None + else: + a = (low[axis] - origin[axis]) / direction[axis] + b = (high[axis] - origin[axis]) / direction[axis] + if a > b: + a, b = b, a + lo, hi = max(lo, a), min(hi, b) + if lo > hi: + return None + return lo, hi + + +def ray_tri(origin, direction, a, b, c): + e1 = tuple(b[i] - a[i] for i in range(3)) + e2 = tuple(c[i] - a[i] for i in range(3)) + p = (direction[1]*e2[2]-direction[2]*e2[1], + direction[2]*e2[0]-direction[0]*e2[2], + direction[0]*e2[1]-direction[1]*e2[0]) + det = sum(e1[i]*p[i] for i in range(3)) + if abs(det) < 1e-10: + return None + inv = 1.0/det + tvec = tuple(origin[i]-a[i] for i in range(3)) + u = sum(tvec[i]*p[i] for i in range(3))*inv + if u < 0 or u > 1: + return None + q = (tvec[1]*e1[2]-tvec[2]*e1[1], + tvec[2]*e1[0]-tvec[0]*e1[2], + tvec[0]*e1[1]-tvec[1]*e1[0]) + v = sum(direction[i]*q[i] for i in range(3))*inv + if v < 0 or u+v > 1: + return None + t = sum(e2[i]*q[i] for i in range(3))*inv + return t if t >= 0.1 else None + + +if len(sys.argv) < 8 or len(sys.argv) % 2 != 1: + raise SystemExit("usage: ProbeCaptureRays.py scene.vfmesh camX camY camZ yaw pitch pixelX pixelY [pixelX pixelY ...]") +scene_path = sys.argv[1] +cam = tuple(map(float, sys.argv[2:5])) +yaw, pitch = map(math.radians, map(float, sys.argv[5:7])) +pixels = [(int(sys.argv[i]), int(sys.argv[i+1])) for i in range(7, len(sys.argv), 2)] +width, height, fov = 1600, 900, math.radians(90) +tan_h = math.tan(fov/2) +tan_v = tan_h / (width/height) +forward = (math.sin(yaw)*math.cos(pitch), math.cos(yaw)*math.cos(pitch), math.sin(pitch)) +right = (math.cos(yaw), -math.sin(yaw), 0.0) +up = (right[1]*forward[2]-right[2]*forward[1], right[2]*forward[0]-right[0]*forward[2], right[0]*forward[1]-right[1]*forward[0]) + +with open(scene_path, "rb") as f: + if read_exact(f, 8) != b"VFMSH001": + raise SystemExit("bad scene") + version, voxel_um, count = struct.unpack("<3I", read_exact(f, 12)) + scene_cam = struct.unpack("<3d", read_exact(f, 24)) + scene_yaw, scene_pitch = struct.unpack("<2d", read_exact(f, 16)) + voxel_m = voxel_um / 1_000_000.0 + tiles = [] + desired = set() + for _ in range(count): + key = struct.unpack("<4i", read_exact(f, 16)) + surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack("<5I", read_exact(f, 20)) + positions = array.array("f"); positions.frombytes(read_exact(f, vertex_count*12)) + normals = array.array("f"); normals.frombytes(read_exact(f, vertex_count*12)) + indices = array.array("I"); indices.frombytes(read_exact(f, index_count*4)) + masks = read_exact(f, mask_count) + if sys.byteorder != "little": + positions.byteswap(); normals.byteswap(); indices.byteswap() + desired.add(key) + tiles.append((key, surface_vertices, skirt_count, positions, normals, indices, masks)) + +print(f"scene={scene_path} tiles={count} camera={cam} yaw={math.degrees(yaw):.3f} pitch={math.degrees(pitch):.3f}") +for px, py in pixels: + nx = 2*px/width - 1 + ny = 1 - 2*py/height + direction0 = tuple(forward[i] + nx*tan_h*right[i] + ny*tan_v*up[i] for i in range(3)) + length = math.sqrt(sum(v*v for v in direction0)) + direction = tuple(v/length for v in direction0) + hits = [] + candidate_tiles = 0 + for key, surface_vertices, skirt_count, positions, normals, indices, masks in tiles: + x,y,z,lod = key + extent = 32*(1<=surface_vertices or ib>=surface_vertices or ic>=surface_vertices)) + hits.sort(key=lambda h:h[0]) + print(f"pixel=({px},{py}) ray={direction} candidate_tiles={candidate_tiles} hits={len(hits)}") + for t,key,tri,m,p,nz,skirt in hits[:12]: + print(f" t={t:.3f}m key={key} tri={tri} mask={m} skirt={skirt} z={p[2]:.3f} normal_z={nz:.3f} xyz=({p[0]:.3f},{p[1]:.3f},{p[2]:.3f})") diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md new file mode 100644 index 0000000..bdd42e6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md @@ -0,0 +1,132 @@ +# Linux18 — FloatingIslands stratum boundaries + +**Result:** FloatingIslands geometry v3 removes its false lower all-XY seal, keeps the ceiling from the shared upper-boundary seal, and projects missing ceiling cells for every generator. SurfaceWorld hashes match the approved L14 baseline at `8704129` in every checked configuration. + +## Scale in the Unreal route + +`AVoxelWorld::CoreVoxelSizeCm` defaults to 25 cm, and `GetActiveVoxelSizeCm()` resolves the runtime value ([VoxelWorld.h:300](../../../../Source/VoxelForge/Public/VoxelWorld.h), [VoxelWorld.cpp:7495](../../../../Source/VoxelForge/Private/VoxelWorld.cpp)). The adapter writes the resolved size to the snapshot ([VoxelForgeCoreAdapter.cpp:1019](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp)); `AVoxelWorld` imports it with `ResolvedWorldSnapshot::FromJson` ([VoxelWorld.cpp:7597](../../../../Source/VoxelForge/Private/VoxelWorld.cpp), [VoxelForgeCore.cpp:916](../../../../Standalone/Core/src/VoxelForgeCore.cpp)). S1 records this single active size source and the 32-Core-voxel tile rule ([Round5 REPORT.md](../Round5/REPORT.md#1-one-core-voxel-size-source)). + +At the current 25 cm setting, **SurfaceWorld strata use 8 m per slot chunk**: the Unreal surface plan multiplies the slot range by 32 Core voxels ([VoxelForgeCoreAdapter.cpp:280](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp); `CHUNK_SIZE=32` in [VoxelTypes.h:211](../../../../Source/VoxelForge/Public/VoxelTypes.h)). **FloatingIslands uses 4 m per owner-authored chunk**: the adapter’s owner size is `VOXEL_SIZE` ([VoxelForgeCoreAdapter.cpp:33](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp); 12.5 cm in [VoxelTypes.h:213](../../../../Source/VoxelForge/Public/VoxelTypes.h)), and its plan multiplies 32 owner voxels by the serialized owner/Core ratio, currently 12.5/25 = 1/2, or 16 Core voxels ([VoxelForgeCoreAdapter.cpp:441](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp)). For chunks 0…−15 that gives z bounds −60…+4 m. This is the two explicit coordinate conventions in the game route, not a global change from 8 m to 4 m. + +Standalone uses the same owner/Core ratio and chunk formula ([OwnerWorldLoader.h:302](../../../../Standalone/Source/OwnerWorldLoader.h), [WorldAssemblyPrototype.h:488](../../../../Standalone/Source/WorldAssemblyPrototype.h)); no current Unreal/standalone scale disagreement was found. L18 also removed the standalone-only two-Core-voxel bounds inset so recipe bounds match the Unreal plan. The owner decides whether either authored scale changes. + +## Fix and ceiling + +Geometry version 3 preserves the island shapes and removes only the lower all-XY seal. The upper seal calls the shared `VF_ApplyUpperBoundarySeal` density primitive; the shared two-sided seal uses the same upper-band math ([VoxelDensityPrimitives.h:44](../../../../Source/VoxelForge/Public/VoxelDensityPrimitives.h)). Resolved bounds come from each strate’s own range. The shared `AppendStratumBoundaryCeilings` mesh path runs for every generator and LOD, adding a plane at the resolved upper boundary where the mesh lacks coverage. It suppresses an existing plane only when nine probes show full cell coverage ([ShapeWorldInternal.h:36](../../../../Standalone/Core/src/ShapeWorldInternal.h), [FarShellsInternal.h:894](../../../../Standalone/Core/src/FarShellsInternal.h), [VoxelForgeCore.cpp:2207](../../../../Standalone/Core/src/VoxelForgeCore.cpp)). The underside normal points into the room, so the existing material lighting renders the interior face. This adds no FloatingIslands-specific height. + +For the captured FloatingIslands strate, the upper bound is **z=+4 m** and its configured 0.5 m seal gives an interior-facing ceiling surface beginning around **z=+3.5 m**. The density probe crosses into the seal at 3.5 m and exits at z=4.019 m; z=7.75, 8.0, and 8.25 m remain air with no crossing. The lower all-XY seal is removed; remaining geometry below −60 m belongs to the lower world and is visible in the ray traces. Thus the apparent 3.5 m “floor” is the retained ceiling when viewed from inside; the 8 m floor is absent. No chunk size changed. + +The owner’s decision and version-3 recipe say an underground strate has a ceiling at its configured upper boundary. “Farther to breathe” comes from a taller configured range. I updated the stale architecture-table wording “open void” to say the islands sit beneath the shared ceiling. + +## Ceiling continuity + +The final audit used 16×16 interior samples per tile and paired samples along same-LOD tile edges. Far LODs pass continuously: + +| LOD | Tiles with cap | Interior hits | Max root delta | Edge gaps | +|---:|---:|---:|---:|---:| +| 4 | 48/48 | 12,288/12,288 | 0 cm | 0 | +| 5 | 48/48 | 12,288/12,288 | 0 cm | 0 | +| 6 | 48/48 | 12,288/12,288 | 0 cm | 0 | +| 7 | 48/48 | 12,288/12,288 | 0 cm | 0 | +| 8 | 153/153 | 39,168/39,168 | 0 cm | 0 | + +LOD0, 2, and 3 also hit all tested samples. LOD1 missed 3 of 12,288 interior samples (0.387 cm maximum root delta) and had no shared-edge gaps. The readable upward view is `captures/floating-islands-ceiling-up.jpg`. + +## Dark tile-aligned patches + +They are lower SurfaceWorld triangles visible below the removed false lower cap, not empty rays and not a viewer dark-square overlay. Traces in the final island export found: + +- Pixel (700,720): first hit at 104.924 m on non-skirt LOD2 tile `(-2,2,-2,2)`, z=−62.925 m, normal-z 0.266; another LOD2 tile hits z=−69.942 m. +- Pixel (1050,650): first hit at 168.051 m on non-skirt LOD3 tile `(0,2,-2,3)`, z=−72.594 m, normal-z 0.895; lower LOD4 geometry is also hit at z=−112.229 m and −120 m. + +The ring probe measures real LOD height steps on those lower surfaces. I left them visible: suppressing those triangles would hide valid lower-stratum geometry, while changing the lower strata or SurfaceWorld would exceed this local fix. The sampled rectangles are not missing tiles or floor holes. + +## Cross-LOD ring steps + +Entries are median / p95 / maximum absolute edge-height difference in centimetres, then gap count. The L18 column is the final matched island camera. The large remaining steps are lower-stratum terrain, not the upper ceiling. + +| LOD edge | L17 before | L17 after | L18 final | +|---|---:|---:|---:| +| 0→1 | 3.451 / 8.920 / 449.158; 14 | —; 0 samples | —; 0 samples | +| 1→2 | 26.642 / 148.262 / 149.862; 0 | 0 / 0 / 0; 0 | 147.141 / 148.403 / 149.862; 0 | +| 2→3 | 0.134 / 198.730 / 773.318; 8 | 0 / 0 / 0; 0 | 128.603 / 200.789 / 773.318; 8 | +| 3→4 | 3.269 / 47.875 / 188.531; 27 | 0 / 0 / 0; 0 | 3.269 / 47.774 / 188.531; 27 | +| 4→5 | 29.975 / 130.496 / 445.444; 6 | 0 / 0 / 30.957; 1 | 29.992 / 124.373 / 443.482; 6 | +| 5→6 | 12.234 / 127.224 / 536.573; 32 | 12.234 / 127.224 / 536.573; 32 | 12.234 / 127.224 / 536.573; 21 | +| 6→7 | 29.013 / 301.752 / 42,624.479; 0 | 28.892 / 291.861 / 945.477; 0 | 29.013 / 301.752 / 42,624.479; 0 | +| 7→8 | 83.843 / 1,225.416 / 3,755.407; 32 | 83.843 / 1,225.416 / 3,755.407; 0 | 83.843 / 1,225.416 / 3,755.407; 0 | + +## SurfaceWorld regression hashes + +The 14-run check compared each final output to `Saved/Linux/l14-final-hash-sw-clang-off-w12-r5.log` from the L14/`8704129` baseline: Clang `-ffp-contract=off` and GCC default, W1/W8/W12, with five W12 repeats. All 14 matched the whole-view, full-detail, LOD0–8, and exact-query rows. The 24 m crossing check likewise passed all 14 compiler/worker configurations against their L14 compiler-specific references. + +Whole view D/M/P/T: `c10697a8f48380c7 / e75c9119a1498cd5 / 49a9a8d1835a6c8c / edcb23288b5e8724`. + +Full detail D/M/P/T: `0ee0579bd15cbbe4 / 32e5d4964d610fee / 1932b96ee4b1d910 / 69743c6bad3447dc`. + +| LOD | Density | Mesh | Payload | Topology | +|---:|---|---|---|---| +| 0 | `769f2a706d585a72` | `33eeeb39fa443816` | `645621a1d968983f` | `c2457ed905036c9d` | +| 1 | `69cc5a5c0a2c60af` | `b48335f5410c57d5` | `d15c5480162ab8ba` | `feca1bd025823604` | +| 2 | `85deed5683a77202` | `53469895987aeea2` | `e55f8ae7e1532033` | `d5feb12a0e85c09b` | +| 3 | `6518caefb3be7e7f` | `ec513c90b853a614` | `f62fa1137ff17f82` | `ccf1dcf681965816` | +| 4 | `62d4be96021992f4` | `f1e4c6efcc2615a9` | `9dc5f9c88be02f33` | `fe6f55125a07c3bf` | +| 5 | `5e4a8c38b8cea6fc` | `08ee56998aaf21e5` | `541eea8558f26cc8` | `6539374a8b3336d8` | +| 6 | `e036e95b9006945b` | `624a33f941a99b02` | `81b73e14f68bdf86` | `4de4c63ce1f80a56` | +| 7 | `8ee358af56a30d8d` | `46ab3c6184048b7c` | `0a6164b7e68e80c5` | `0c26b85bb03bc831` | +| 8 | `c3458e1cbdb9c471` | `46c03d0994d75192` | `8f89fc5bde12d230` | `d16dc0cf62031898` | + +Exact query: 9 probes; density/unclamped `e4d4a8eb95b35933`, detail `efcba20c0c70c415`, collision `e604823a249029bf`. + +The 24 m crossing W12×5 hashes match those same aggregate whole-view, full-detail, and exact-query values on both compiler references; the full check compared all 12 rows per run. + +FloatingIslands’ representative W12×5 whole-view D/M/P/T is `ca7f375cded78f1d / da003331c554f8f3 / 0d2e74862b4ee500 / b7a7177af8915a21`; full-detail is `fc71abf85f5b9373 / e139b32d42ecfa29 / 28a2414e41de59bd / d61ca68ca64f74ca`. Exact query is `4ef614485815ee30` density/unclamped, `efcba20c0c70c415` detail, `e604823a249029bf` collision. Clang and GCC representative hashes agree, and all nine LOD rows stayed stable across the 28-run matrix. + +## Other invariants and Unreal edits + +- Final matrix: 28/28 rows passed; repeated runs were stable within each compiler. Route parity had 0 mismatches across 782,026 FI and 910,651 SurfaceWorld exact samples. Proof audits had 0 violations: FI 9,048 blocks / 12,042,888 samples; SurfaceWorld 27,479 / 36,574,549. Readiness negative controls were caught; walkability passed 4,000/4,000 FI and 5,642/5,642 SurfaceWorld samples. +- L3 FP-contract preamble/self-test, L4 `FLazy`, L7 shared cache, L9/L12 proofs, L11 readiness/crash handling, L13 route binding, ARCHITECTURE §8.10, and async/MPSC/Epoch paths remain intact. Both final compiler runs started without the Core FP-contract self-test refusing the build. No async route was added. +- Unreal-side edits are limited to [VoxelForgeCoreAdapter.cpp](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:453), bumping FI geometry version 2→3 so serialized recipes pick up the lower-seal removal, and the shared [VoxelDensityPrimitives.h](../../../../Source/VoxelForge/Public/VoxelDensityPrimitives.h:44), extracting the common upper-seal math used by v3. The Unreal target could not be compiled on this Linux host. + +## Captures + +All JPEGs are below 300 KB. Before/after pairs use matched cameras: FI flat square `(-11.6124,-118.2212,-19.8452), yaw/pitch 0/0`; FI island `(-22,-19,-22.8), 0/−4`; SurfaceWorld spawn `(0,-35,-487.94), 0/−22`; horizon uses the same spawn camera with pitch 0. The ceiling view uses the island camera with pitch 80. The SurfaceWorld pairs look the same; the exact hash checks certify its output bit-for-bit. + +- FI flat square: `captures/floating-islands-flat-square-before.jpg`, `captures/floating-islands-flat-square-after.jpg`. +- FI low island: `captures/floating-islands-island-view-before.jpg`, `captures/floating-islands-island-view-after.jpg`. +- FI upward ceiling: `captures/floating-islands-ceiling-up.jpg`. +- SurfaceWorld L1 spawn: `captures/surface-world-l1-spawn-before.jpg`, `captures/surface-world-l1-spawn-after.jpg`. +- SurfaceWorld horizon: `captures/surface-world-horizon-before.jpg`, `captures/surface-world-horizon-after.jpg`. +- Flat-square diagnostic without tile edges: `captures/floating-islands-flat-square-no-edges.jpg`. + +## Failed or retried runs + +- The first density-probe link omitted legacy compatibility objects and failed with undefined references. Relinking with the legacy implementation objects succeeded; both boundary probes then ran. +- The initial 4×4 continuity audit failed at far LODs 4–6 and found edge gaps. The shared all-LOD boundary cap and final full-cell coverage check replaced the center-only deduplication; final 16×16 LOD4–8 continuity passed with zero edge gaps. +- An early matrix checker compared Clang contraction-off hashes directly with GCC default hashes and reported FI mismatches. The checker was corrected to test repeated runs within each compiler and the L14 compiler-specific references; the final 28-row matrix passed. +- An early offscreen capture drew all skirts and showed an artificial slab. The capture now applies the viewer’s hidden-skirt mask; final matched captures were regenerated. +- A GCC build interrupted by owner steering produced no binary and was rerun. A pre-correction Clang build was superseded by the final build. No final build or matrix run failed or exceeded the runtime limits. + +## Resources and owner note + +Final builds: Clang 19 exit 0 in 31.689 s, peak RSS 436,088 KiB; GCC 14 exit 0 in 48.648 s, peak RSS 903,168 KiB. The largest measured runtime process was 897,276 KiB RSS. All processes stayed below the 6 GiB address-space cap; CPU affinity was 0–11, worker maximum 12, normal priority, and governor `powersave`. No run exceeded 30 minutes or stalled for 15 minutes. See [BuildResources.md](BuildResources.md). + +**Owner note:** SurfaceWorld stayed bit-identical to L14. FloatingIslands now retains a ceiling at its configured upper strate boundary while removing the false lower all-XY floor. At the current plan that is a z=+4 m boundary with a 0.5 m seal; its distance above the islands comes from the configured range. The small Unreal edits are listed above and need an owner-machine UBT compile. + +## Orchestrator review (Claude) + +- **Merged.** SurfaceWorld is bit-identical (14 configs plus the 24 m crossing); the FI false all-XY lower seal + is removed; the ceiling comes from the shared, strate-parameter upper-boundary seal and ceiling path + (owner rule: every strate has a ceiling, height from its own parameters). The Unreal edits are a pure + extraction of the existing upper-seal math plus the FI `geometryVersion` 2→3; they need a UBT compile on + the owner PC. +- **The scale answer:** both conventions are deliberate and agree between Unreal and the standalone + (SurfaceWorld 32 Core voxels per chunk; FloatingIslands 32 owner voxels = 16 Core voxels). Nothing + rescaled. +- **Still open (L19):** + 1. Small black holes in the FI ceiling, visible near the top of `floating-islands-flat-square-after.jpg`; + compare LOD1 missing 3/12,288 continuity samples. + 2. The owner's ledges are most likely the LOWER stratum's terrain seen through the floorless FI void: + ring steps of 147 cm median at LOD1→2 and 129 cm at 2→3 remain in that terrain. "Valid lower-stratum + geometry" with metre-scale cross-LOD steps is still a bug the owner can see. +- Exports (229 MB `.vfmesh`) are not committed. diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/RingProbe.py b/Standalone/Research/WorldAssemblerM3/Linux18/RingProbe.py new file mode 100644 index 0000000..b722a3a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/RingProbe.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Compare upward mesh heights on shared XY edges between adjacent LOD rings.""" +import array +import collections +import math +import struct +import sys + + +def read_scene(path): + tiles = [] + with open(path, "rb") as f: + if f.read(8) != b"VFMSH001": + raise SystemExit(f"{path}: invalid mesh header") + version, voxel_um, count = struct.unpack("<3I", f.read(12)) + camera = struct.unpack("<3d", f.read(24)) + yaw, pitch = struct.unpack("<2d", f.read(16)) + voxel_m = voxel_um / 1_000_000.0 + for _ in range(count): + key = struct.unpack("<4i", f.read(16)) + surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack( + "<5I", f.read(20)) + positions = array.array("f") + positions.frombytes(f.read(vertex_count * 12)) + normals = array.array("f") + normals.frombytes(f.read(vertex_count * 12)) + indices = array.array("I") + indices.frombytes(f.read(index_count * 4)) + masks = f.read(mask_count) + if sys.byteorder != "little": + positions.byteswap(); normals.byteswap(); indices.byteswap() + triangles = [] + for i in range(0, index_count, 3): + if masks and masks[i // 3] != 0: + continue + a, b, c = indices[i:i + 3] + if max(a, b, c) >= surface_vertices: + continue + nz = (normals[a * 3 + 2] + normals[b * 3 + 2] + normals[c * 3 + 2]) / 3.0 + if nz < 0.65: + continue + tri = tuple((positions[j * 3], positions[j * 3 + 1], positions[j * 3 + 2]) + for j in (a, b, c)) + triangles.append(tri) + if triangles: + lod = key[3] + extent = 32 * (1 << lod) * voxel_m + tiles.append({"key": key, "lod": lod, "extent": extent, + "x0": key[0] * extent, "y0": key[1] * extent, + "x1": (key[0] + 1) * extent, "y1": (key[1] + 1) * extent, + "z0": key[2] * extent, "z1": (key[2] + 1) * extent, + "triangles": triangles}) + return camera, yaw, pitch, tiles + + +def height_at(tile, x, y): + best = None + for tri in tile["triangles"]: + (x0, y0, z0), (x1, y1, z1), (x2, y2, z2) = tri + den = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2) + if abs(den) < 1.0e-10: + continue + a = ((y1 - y2) * (x - x2) + (x2 - x1) * (y - y2)) / den + b = ((y2 - y0) * (x - x2) + (x0 - x2) * (y - y2)) / den + c = 1.0 - a - b + if min(a, b, c) >= -1.0e-5: + z = a * z0 + b * z1 + c * z2 + if best is None or z > best: + best = z + return best + + +path = sys.argv[1] +camera, yaw, pitch, tiles = read_scene(path) +by_lod = collections.defaultdict(list) +for tile in tiles: + by_lod[tile["lod"]].append(tile) +print(f"scene={path} camera_m={camera} yaw={yaw} pitch={pitch} nonempty_mesh_tiles={len(tiles)}") +print("lod_pair shared_edges samples_both median_abs_cm p95_abs_cm max_abs_cm median_delta_cm gaps") +for lod in range(8): + fine = by_lod[lod] + coarse = by_lod[lod + 1] + deltas = [] + edge_rows = [] + edge_count = 0 + gap_count = 0 + # Shared XY edges occur only where tile extents touch and overlap on the other axis. + for a in fine: + for b in coarse: + zlo, zhi = max(a["z0"], b["z0"]), min(a["z1"], b["z1"]) + if zhi - zlo <= 1.0e-4: + continue + edge = None + if abs(a["x1"] - b["x0"]) < 1.0e-4 or abs(b["x1"] - a["x0"]) < 1.0e-4: + lo, hi = max(a["y0"], b["y0"]), min(a["y1"], b["y1"]) + if hi - lo > 1.0e-4: + edge = ("x", a["x1"] if abs(a["x1"] - b["x0"]) < 1.0e-4 else a["x0"], lo, hi) + if edge is None and (abs(a["y1"] - b["y0"]) < 1.0e-4 or abs(b["y1"] - a["y0"]) < 1.0e-4): + lo, hi = max(a["x0"], b["x0"]), min(a["x1"], b["x1"]) + if hi - lo > 1.0e-4: + edge = ("y", a["y1"] if abs(a["y1"] - b["y0"]) < 1.0e-4 else a["y0"], lo, hi) + if edge is None: + continue + edge_count += 1 + axis, fixed, lo, hi = edge + epsilon = min(0.02, a["extent"] * 0.001, b["extent"] * 0.001) + samples = 9 + matched = 0 + edge_deltas = [] + for i in range(samples): + t = (i + 0.5) / samples + varying = lo + (hi - lo) * t + if axis == "x": + side_a = 1.0 if abs(a["x1"] - fixed) < 1.0e-4 else -1.0 + side_b = 1.0 if abs(b["x1"] - fixed) < 1.0e-4 else -1.0 + za = height_at(a, fixed - side_a * epsilon, varying) + zb = height_at(b, fixed - side_b * epsilon, varying) + else: + side_a = 1.0 if abs(a["y1"] - fixed) < 1.0e-4 else -1.0 + side_b = 1.0 if abs(b["y1"] - fixed) < 1.0e-4 else -1.0 + za = height_at(a, varying, fixed - side_a * epsilon) + zb = height_at(b, varying, fixed - side_b * epsilon) + if za is not None and not zlo - 1.0e-3 <= za <= zhi + 1.0e-3: + za = None + if zb is not None and not zlo - 1.0e-3 <= zb <= zhi + 1.0e-3: + zb = None + if za is not None and zb is not None: + delta = (za - zb) * 100.0 + deltas.append(delta) + edge_deltas.append((delta, za, zb, varying)) + matched += 1 + if matched == 0: + gap_count += 1 + elif edge_deltas: + edge_rows.append((max(abs(row[0]) for row in edge_deltas), + a["key"], b["key"], edge, edge_deltas)) + if deltas: + absolute = sorted(abs(x) for x in deltas) + signed = sorted(deltas) + p95 = absolute[math.floor(0.95 * (len(absolute) - 1))] + median_abs = absolute[len(absolute) // 2] + median_delta = signed[len(signed) // 2] + print(f"{lod}->{lod+1} {edge_count} {len(deltas)} {median_abs:.3f} {p95:.3f} {max(absolute):.3f} {median_delta:.3f} {gap_count}") + for worst, fine_key, coarse_key, edge, rows in sorted(edge_rows, reverse=True)[:2]: + row = max(rows, key=lambda item: abs(item[0])) + print(f" worst_edge max_abs_cm={worst:.3f} fine={fine_key} coarse={coarse_key} " + f"axis={edge[0]} fixed_m={edge[1]:.3f} span_m={edge[2]:.3f}..{edge[3]:.3f} " + f"sample_v_m={row[3]:.3f} fine_z_m={row[1]:.3f} " + f"coarse_z_m={row[2]:.3f} delta_cm={row[0]:.3f}") + else: + print(f"{lod}->{lod+1} {edge_count} 0 n/a n/a n/a n/a {gap_count}") diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/RunCrossing.py b/Standalone/Research/WorldAssemblerM3/Linux18/RunCrossing.py new file mode 100644 index 0000000..e5f0aef --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/RunCrossing.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Run the L18 24 m SurfaceWorld crossing hash matrix.""" +import os +import re +import signal +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l18-crossing" +OUT.mkdir(parents=True, exist_ok=True) +if not os.sched_getaffinity(0).issubset(set(range(12))): + raise SystemExit("run under taskset -c 0-11") +BINARIES = { + "clang-off": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux18-clang", + "gcc-default": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux18-gcc", +} +PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +rows = [] +passed = True +for compiler, binary in BINARIES.items(): + for workers, repeat in [(1, 1), (8, 1), (12, 1), (12, 2), (12, 3), (12, 4), (12, 5)]: + name = f"{compiler}-w{workers}-r{repeat}" + logfile = OUT / f"{name}.log" + command = ["prlimit", "--as=6442450944", "--", str(binary), + str(ROOT / "Standalone/data/owner_world.json"), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8", + f"--workers={workers}", "--camera=0,-35,-487.94,0,-22", + "--camera-step=24,0,0", "--region-crossing", "--recipe=surface-world"] + started = time.monotonic() + peak_rss = 0 + with logfile.open("w", encoding="utf-8") as stream: + process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT, + start_new_session=True) + while process.poll() is None: + pending = [process.pid] + seen = set() + while pending: + pid = pending.pop() + if pid in seen: + continue + seen.add(pid) + try: + pending.extend(int(value) for value in + Path(f"/proc/{pid}/task/{pid}/children").read_text().split()) + for line in Path(f"/proc/{pid}/status").read_text().splitlines(): + if line.startswith("VmRSS:"): + peak_rss = max(peak_rss, int(line.split()[1])) + break + except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError): + pass + if time.monotonic() - started > 900: + os.killpg(process.pid, signal.SIGTERM) + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL) + process.wait() + passed = False + break + time.sleep(0.1) + code = process.wait() + elapsed = time.monotonic() - started + actual = PATTERN.findall(logfile.read_text(encoding="utf-8", errors="replace")) + ok = code == 0 and len(actual) == 12 + passed &= ok + rows.append((compiler, workers, repeat, code, elapsed, peak_rss, ok, actual)) + print(f"[CrossingRun] {name} exit={code} seconds={elapsed:.3f} " + f"peak_rss_kib={peak_rss} hash_rows={len(actual)} status={'PASS' if ok else 'FAIL'}", + flush=True) +reference = None +for compiler, workers, repeat, code, elapsed, peak_rss, ok, hashes in rows: + if reference is None: + reference = hashes + elif hashes != reference: + passed = False + print(f"[CrossingHashMismatch] {compiler} workers={workers} repeat={repeat}", flush=True) +print(f"[CrossingSummary] rows={len(rows)} expected=14 result={'PASS' if passed else 'FAIL'} " + f"path={OUT}", flush=True) +raise SystemExit(0 if passed and len(rows) == 14 else 1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/RunMatrix.py b/Standalone/Research/WorldAssemblerM3/Linux18/RunMatrix.py new file mode 100644 index 0000000..24efb7d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux18/RunMatrix.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Run the L18 deterministic W1/W8/W12 x5 matrix on both supported Linux builds.""" +import os +import re +import signal +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l18-matrix" +OUT.mkdir(parents=True, exist_ok=True) +if not os.sched_getaffinity(0).issubset(set(range(12))): + raise SystemExit("run under taskset -c 0-11") + +BINARIES = { + "clang-off": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux18-clang", + "gcc-default": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux18-gcc", +} +RECIPES = { + "floating-islands": "-10,-31,-22,0,-8", + "surface-world": "0,-35,-487.94,0,-22", +} +HASH_PATTERNS = { + "full": re.compile(r"^\[PipelineFullDetailHash\].*$", re.M), + "whole": re.compile(r"^\[PipelineWholeViewHash\].*$", re.M), + "lod": re.compile(r"^\[PipelineLODHash\].*$", re.M), + "exact": re.compile(r"^\[PipelineExactQueryHash\].*$", re.M), +} +rows = [] +all_pass = True + +for recipe, camera in RECIPES.items(): + for compiler, binary in BINARIES.items(): + if not binary.exists(): + raise SystemExit(f"missing binary: {binary}") + for workers, repeat in [(1, 1), (8, 1), (12, 1), (12, 2), (12, 3), (12, 4), (12, 5)]: + name = f"{recipe}-{compiler}-w{workers}-r{repeat}" + logfile = OUT / f"{name}.log" + command = ["prlimit", "--as=6442450944", "--", str(binary), + str(ROOT / "Standalone/data/owner_world.json"), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--stream-hash", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", f"--workers={workers}", f"--camera={camera}", + f"--recipe={recipe}"] + started = time.monotonic() + peak_rss_kib = 0 + with logfile.open("w", encoding="utf-8") as stream: + process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT, + start_new_session=True) + while process.poll() is None: + pending = [process.pid] + seen = set() + while pending: + pid = pending.pop() + if pid in seen: + continue + seen.add(pid) + try: + children = Path(f"/proc/{pid}/task/{pid}/children").read_text().split() + pending.extend(int(value) for value in children) + for line in Path(f"/proc/{pid}/status").read_text().splitlines(): + if line.startswith("VmRSS:"): + peak_rss_kib = max(peak_rss_kib, int(line.split()[1])) + break + except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError): + pass + if time.monotonic() - started > 900: + os.killpg(process.pid, signal.SIGTERM) + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL) + process.wait() + all_pass = False + break + time.sleep(0.1) + code = process.wait() + elapsed = time.monotonic() - started + output = logfile.read_text(encoding="utf-8", errors="replace") + matched = {key: pattern.findall(output) for key, pattern in HASH_PATTERNS.items()} + ok = code == 0 and all(matched.values()) and len(matched["lod"]) == 9 + if not ok: + all_pass = False + row = {"recipe": recipe, "compiler": compiler, "workers": workers, + "repeat": repeat, "exit": code, "seconds": elapsed, + "peak_rss_kib": peak_rss_kib, "ok": ok, + "full": matched["full"][-1] if matched["full"] else "", + "whole": matched["whole"][-1] if matched["whole"] else "", + "lod": " || ".join(matched["lod"]), + "exact": matched["exact"][-1] if matched["exact"] else ""} + rows.append(row) + print(f"[MatrixRun] {name} exit={code} seconds={elapsed:.3f} " + f"peak_rss_kib={peak_rss_kib} hash_rows={sum(map(len,matched.values()))} " + f"status={'PASS' if ok else 'FAIL'}", flush=True) + +summary = OUT / "summary.tsv" +columns = ["recipe", "compiler", "workers", "repeat", "exit", "seconds", "peak_rss_kib", + "ok", "full", "whole", "lod", "exact"] +with summary.open("w", encoding="utf-8") as f: + f.write("\t".join(columns) + "\n") + for row in rows: + f.write("\t".join(str(row[key]) for key in columns) + "\n") + +for recipe in RECIPES: + for compiler in BINARIES: + reference = None + for row in rows: + if row["recipe"] != recipe or row["compiler"] != compiler: + continue + signature = (row["full"], row["whole"], row["lod"], row["exact"]) + if reference is None: + reference = signature + elif signature != reference: + print(f"[MatrixMismatch] recipe={recipe} workers={row['workers']} " + f"compiler={compiler} repeat={row['repeat']}", flush=True) + all_pass = False + # Compiler defaults are intentionally recorded separately: Clang uses contraction off, + # while GCC default is the owner-requested control. Report rather than hide any cross-build + # difference; worker/repeat determinism is asserted within each compiler above. + compiler_signatures = {} + for row in rows: + if row["recipe"] == recipe: + compiler_signatures.setdefault(row["compiler"], + (row["full"], row["whole"], row["lod"], row["exact"])) + if len(set(compiler_signatures.values())) > 1: + print(f"[MatrixCompilerDifference] recipe={recipe} compilers=" + f"{','.join(compiler_signatures)}", flush=True) +print(f"[MatrixSummary] rows={len(rows)} expected=28 result={'PASS' if all_pass else 'FAIL'} " + f"path={summary}", flush=True) +raise SystemExit(0 if all_pass and len(rows) == 28 else 1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-ceiling-up.jpg b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-ceiling-up.jpg new file mode 100644 index 0000000..38b7028 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-ceiling-up.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-flat-square-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-flat-square-after.jpg new file mode 100644 index 0000000..a88fef2 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-flat-square-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-flat-square-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-flat-square-before.jpg new file mode 100644 index 0000000..8c55566 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-flat-square-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-flat-square-no-edges.jpg b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-flat-square-no-edges.jpg new file mode 100644 index 0000000..706d72d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-flat-square-no-edges.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-island-view-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-island-view-after.jpg new file mode 100644 index 0000000..c42dcd3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-island-view-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-island-view-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-island-view-before.jpg new file mode 100644 index 0000000..f6b6097 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux18/captures/floating-islands-island-view-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-horizon-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-horizon-after.jpg new file mode 100644 index 0000000..896ba4e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-horizon-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-horizon-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-horizon-before.jpg new file mode 100644 index 0000000..4f639c8 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-horizon-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-l1-spawn-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-l1-spawn-after.jpg new file mode 100644 index 0000000..bffdac3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-l1-spawn-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-l1-spawn-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-l1-spawn-before.jpg new file mode 100644 index 0000000..07710f4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux18/captures/surface-world-l1-spawn-before.jpg differ diff --git a/Standalone/Source/LinuxOffscreenCapture.cpp b/Standalone/Source/LinuxOffscreenCapture.cpp index 1dfe7f3..ef2effd 100644 --- a/Standalone/Source/LinuxOffscreenCapture.cpp +++ b/Standalone/Source/LinuxOffscreenCapture.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include namespace @@ -38,10 +39,69 @@ namespace float MaximumHeight=-std::numeric_limits::infinity(); std::vector Vertices; std::vector Indices; + struct FTileRange + { + std::array Key{}; + std::size_t IndexBegin=0; + std::uint32_t IndexCount=0; + std::vector FaceMasks; + }; + std::vector Tiles; std::uint64_t Triangles=0,SkirtTriangles=0; + std::uint64_t VisibleSkirtTriangles=0,HiddenSameLodSkirtTriangles=0; + std::uint64_t DrawnTriangles=0; std::uint32_t TileCount=0; }; + struct FTileKeyHash + { + std::size_t operator()(const std::array& Key) const noexcept + { + std::size_t Hash=0xcbf29ce484222325ull; + for(const std::int32_t Value:Key) + { + Hash^=static_cast(Value); + Hash*=0x100000001b3ull; + } + return Hash; + } + }; + + std::int32_t FloorDiv(std::int64_t Value,std::int64_t Divisor) + { + const std::int64_t Quotient=Value/Divisor, Remainder=Value%Divisor; + return static_cast(Quotient-((Remainder<0)?1:0)); + } + + std::uint8_t ComputeVisibleSkirtFaceMask( + const std::array& Tile, + const std::unordered_set,FTileKeyHash>& Desired) + { + const std::int64_t Extent=static_cast(32)<(Tile[0])*Extent, + static_cast(Tile[1])*Extent,static_cast(Tile[2])*Extent}; + std::uint8_t Mask=0; + for(std::int32_t Axis=0;Axis<3;++Axis)for(std::int32_t Side=0;Side<2;++Side) + { + std::int64_t Probe[3]={Origin[0]+Extent/2,Origin[1]+Extent/2, + Origin[2]+Extent/2}; + Probe[Axis]=Side?Origin[Axis]+Extent:Origin[Axis]-1; + std::int32_t NeighborLevel=-1; + for(std::int32_t Level=0;Level<=8;++Level) + { + const std::int64_t NeighborExtent=static_cast(32)< Neighbor={ + FloorDiv(Probe[0],NeighborExtent),FloorDiv(Probe[1],NeighborExtent), + FloorDiv(Probe[2],NeighborExtent),Level}; + if(Desired.contains(Neighbor)) + {NeighborLevel=Level;break;} + } + if(NeighborLevel<0||NeighborLevel!=Tile[3]) + Mask|=static_cast(1u<<(Axis*2+Side)); + } + return Mask; + } + template bool Read(std::ifstream& File,T& Value) { File.read(reinterpret_cast(&Value),sizeof(Value)); @@ -126,11 +186,36 @@ namespace } Scene.Triangles+=IndexCount/3; Scene.SkirtTriangles+=SkirtCount; + Scene.Tiles.push_back({{Key[0],Key[1],Key[2],Key[3]},IndexOffset, + IndexCount,std::move(Masks)}); TotalVertices+=VertexCount;TotalIndices+=IndexCount; } if(File.peek()!=std::char_traits::eof()) {Error="mesh scene has trailing bytes";return false;} if(Scene.Indices.empty()){Error="mesh scene contains no triangles";return false;} + std::unordered_set,FTileKeyHash> Desired; + Desired.reserve(Scene.Tiles.size()); + for(const auto& Tile:Scene.Tiles)Desired.insert(Tile.Key); + std::vector VisibleIndices; + VisibleIndices.reserve(Scene.Indices.size()); + for(const auto& Tile:Scene.Tiles) + { + const std::uint8_t VisibleMask=ComputeVisibleSkirtFaceMask(Tile.Key,Desired); + for(std::uint32_t Triangle=0;Triangle(Triangle)*3; + VisibleIndices.insert(VisibleIndices.end(),Scene.Indices.begin()+Index, + Scene.Indices.begin()+Index+3); + } + } + Scene.Indices=std::move(VisibleIndices); + Scene.DrawnTriangles=Scene.Indices.size()/3; return true; } @@ -399,9 +484,14 @@ void main() { std::error_code SizeError; const auto Bytes=std::filesystem::file_size(Output,SizeError); - std::printf("[OffscreenCapture] triangles=%llu tiles=%u skirts=%llu shading=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n", - static_cast(Scene.Triangles),Scene.TileCount, + std::printf("[OffscreenCapture] source_triangles=%llu drawn_triangles=%llu tiles=%u " + "skirts=%llu visible_skirt_triangles=%llu hidden_same_lod_skirt_triangles=%llu " + "shading=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n", + static_cast(Scene.Triangles), + static_cast(Scene.DrawnTriangles),Scene.TileCount, static_cast(Scene.SkirtTriangles), + static_cast(Scene.VisibleSkirtTriangles), + static_cast(Scene.HiddenSameLodSkirtTriangles), ReadableMode?"readable":"normal",TileEdges?"on":"off",TileEdgeWidthMeters, static_cast(SizeError?0:Bytes),Output.string().c_str()); } diff --git a/Standalone/Source/VoxelForge/Public/VoxelDensityPrimitives.h b/Standalone/Source/VoxelForge/Public/VoxelDensityPrimitives.h index 7fb17f4..8e2c4c3 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelDensityPrimitives.h +++ b/Standalone/Source/VoxelForge/Public/VoxelDensityPrimitives.h @@ -41,21 +41,27 @@ // SealFactor > 0 et BaseDensity > 0, le résultat est solide GARANTI quelle qu'ait été l'entrée. // C'est exactement ce qu'encode `IVoxelDensityOp::ClassifyBox` (voir VoxelDensityOp.h), et la // raison pour laquelle cette méthode existe. -FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ, - float StrateTopZ, float StrateBottomZ, - float Thickness, float BaseDensity) +FORCEINLINE void VF_ApplyUpperBoundarySealValue(float& Density, float WorldZ, + float StrateTopZ, float Thickness, float BaseDensity) { - if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return; - - const float DistTop = StrateTopZ - WorldZ; // + si on est sous le plafond - const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol - + const float DistTop = StrateTopZ - WorldZ; if (DistTop >= 0.0f && DistTop < Thickness) { float SealFactor = 1.0f - (DistTop / Thickness); SealFactor = SmoothStep01(SealFactor); Density = FMath::Max(Density, SealFactor * BaseDensity); } +} + +FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ, + float StrateTopZ, float StrateBottomZ, + float Thickness, float BaseDensity) +{ + if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return; + + const float DistBot = WorldZ - StrateBottomZ; // + si on est au-dessus du sol + + VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity); if (DistBot >= 0.0f && DistBot < Thickness) { float SealFactor = 1.0f - (DistBot / Thickness); @@ -64,6 +70,16 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ, } } +// The same structural seal, limited to a strate's upper boundary. Native finite fields use this +// when their lower boundary intentionally has no seal; keeping the math here makes that ceiling +// use the same shared density primitive as every two-sided strate seal. +FORCEINLINE void VF_ApplyUpperBoundarySeal(float& Density, float WorldZ, + float StrateTopZ, float Thickness, float BaseDensity) +{ + if (VoxelDensityAblation::IsBoundarySealOff() || Thickness <= 0.0f) return; + VF_ApplyUpperBoundarySealValue(Density, WorldZ, StrateTopZ, Thickness, BaseDensity); +} + //============================================================================= // CARVE DE PASSAGE / PASSAGE CARVING //============================================================================= diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index 90c6cdd..e581ccf 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -494,11 +494,9 @@ namespace StandaloneWorldAssemblyPrototype const std::int64_t coreVoxelsPerOwnerChunk=static_cast(CHUNK_SIZE) *recipe.ownerToCoreScaleNumerator/recipe.ownerToCoreScaleDenominator; const std::int64_t minimumZ=static_cast(islandSlot->BottomChunkZ) - *coreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel - +2*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + *coreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; const std::int64_t maximumZ=static_cast(islandSlot->TopChunkZ+1) - *coreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel - -2*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + *coreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; if(maximumZ<=minimumZ) {error="FloatingIslands strate is too short for a bounded Core recipe";return false;} recipe.layoutMinimumZSubvoxels=minimumZ;