Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34ae1d7187 | ||
|
|
9a7c87e47b | ||
|
|
a6cd7cf339 | ||
|
|
a176d0743a | ||
|
|
976d012a86 | ||
|
|
32b8670833 | ||
|
|
b1cfaf53b8 | ||
|
|
b370908174 | ||
|
|
7650abb755 | ||
|
|
82813fb13b | ||
|
|
4b1108bf86 | ||
|
|
9898e057fd |
No files matched your search
@@ -732,6 +732,15 @@ driven by `EditorBrush*` props.
|
||||
its default member initialization was never observable; removing that generated TLS leaf avoids
|
||||
constructor work without changing a height or density. Integer queries still read the cached
|
||||
payload, and fractional queries still use the same direct locals.
|
||||
- **Core shared column cache (L7)**: `Heightfield2D` and FloatingIslands keep their `FLazy`
|
||||
worker-local caches as the first tier. A world-owned, fixed-size four-way table is the second
|
||||
tier, keyed by recipe and plan identity plus content hash, exact XY, and an optional exact
|
||||
sub-domain identity (FloatingIslands uses the island stable ID). Hits read versioned atomic
|
||||
entries without taking a lock; misses take only their set's spin lock, recheck, compute once, and
|
||||
publish. The tables are bounded and die with the world after its tile work has drained. This
|
||||
deliberately adds shared reads on column-cache hits while preserving the no-lock hot-path rule;
|
||||
S14's purely worker-local cache remains the first lookup. Keep large worker-local storage behind
|
||||
`FLazy`, and do not make cache values or keys depend on engine objects.
|
||||
- **Sampler unknown-source audit**: the earlier plugin-side `FSurfaceColumn` constructor PCs no longer
|
||||
occur after the trivial-payload change. The largest remaining LOD0 unknown cluster in the final
|
||||
sampler raw CSV is `UnrealEditor-Core.dll + RVA 0xC080` (runtime PCs around
|
||||
@@ -1797,6 +1806,62 @@ page was added: cross-worker repeats alone do not establish a net page-cache win
|
||||
immutable-page lifetime rules would add ownership and memory cost. Round 18 changed no output or
|
||||
recipe version. See [`Standalone/Research/WorldAssemblerM3/Round18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round18/REPORT.md).
|
||||
|
||||
### 8.35 Shared Core column cache — World Assembler L7
|
||||
|
||||
L7 measured 1.34 million repeated formula evaluations on the W12 L1 view before sharing: 83.6%
|
||||
were another worker's XY, 4.0% were same-worker repeats after eviction, and 12.0% were halo/normal
|
||||
repeats. The world-owned shared second tier cut measured formula executions by 60.9% and improved
|
||||
median whole-view-ready time by 19.0%; moving 8 m and generating 144 entering tiles improved median
|
||||
wall time by 36.1%. Peak RSS rose by about 66 MiB. The cache is a bounded exact-value table rather
|
||||
than published pages; no scheduling affinity or page prefetch was needed to clear the measured
|
||||
whole-view threshold.
|
||||
|
||||
The thread-local first tier remains because it avoids shared atomics for local hits. The shared
|
||||
table has no lock on hits; only a miss takes a set-local spin lock. FloatingIslands' cache identity
|
||||
includes its island stable ID because distinct anchors can request the same XY with different
|
||||
column metrics. All W1/W12 L1 hashes, exact queries, and the seed-0 FloatingIslands fixture match
|
||||
the pre-L7 binary with Clang FP contraction off and GCC defaults. See
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux7/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux7/REPORT.md).
|
||||
|
||||
### 8.36 Route-invariant Core density — World Assembler M3 L13
|
||||
|
||||
For an unedited Core world, tile density, mesh, payload, and topology are a function of the world
|
||||
manifest/recipe, seed, and tile key. The reviewed manifest field is authoritative: SurfaceWorld uses
|
||||
its `Heightfield2D`, while FloatingIslands uses its `FloatingIslandFieldV1`. `World::densityAt` and
|
||||
tile generation read the same `FStandaloneShapeWorld` instance. A compiled analytic region page
|
||||
retains bounded spatial bins for page ownership and routing, but `SetSharedDensitySource` binds it to
|
||||
that base shape before publication; density, lattice, detail, classification, and solid queries
|
||||
delegate to the shared evaluator. This keeps exact queries, collision occupancy signs, page tiles,
|
||||
and base tiles on one field. Player edits remain explicit versioned overlays on that world state.
|
||||
|
||||
The route-binding change increments `WorldAssemblyEvaluatorVersion` from 5 to 6. Do not restore a
|
||||
page-local legacy evaluator: it disagrees with the reviewed field. Uniform-block proof audits must
|
||||
fill and check page tiles against the evaluator they actually sample. This is the shared Core path;
|
||||
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 Owner/Core stratum coordinates and FloatingIslands boundaries — World Assembler M3 L17
|
||||
|
||||
Manager chunk coordinates are in owner chunks, while Core density positions are Core voxels. Convert
|
||||
between them with `32 * OwnerVoxelSizeMicrons / CoreVoxelSizeMicrons`; do not divide Core positions
|
||||
by `CHUNK_SIZE` directly. The L17 owner snapshot uses 0.125 m owner voxels and 0.25 m Core voxels,
|
||||
so one owner chunk is 16 Core voxels (4 m), not 32 (8 m). The old hard-coded divisor doubled the
|
||||
manifest's vertical stratum bounds and misplaced native-only readiness queries.
|
||||
|
||||
FloatingIslands is the sparse `FloatingIslandFieldV1` manifest route. Its 0.5 m `boundarySeal`
|
||||
limits feature influence; it is not a signed solid cap across the whole XY plane. Applying it as a
|
||||
solid cap produced a false top at +3.5 m, while the doubled legacy layer bound also produced a cap at
|
||||
+8 m. SurfaceWorld remains the planar `Heightfield2D` field with its independent ground and cap
|
||||
surfaces. Adjacent native FI/SW strata now clip their signed distances to the same physical owner
|
||||
boundary, preserving exact surface signs and height through the mesh sample lattice.
|
||||
|
||||
The output change increments `WorldAssemblyEvaluatorVersion` from 6 to 7; the plan schema and
|
||||
recipe geometry version stay unchanged because their inputs did not change. The L17 probes found
|
||||
zero cave/floor influence at the false square and at the sampled island ground. The shared Core
|
||||
coordinate conversion also changes SurfaceWorld native readiness and hashes where the previous
|
||||
mapping selected the wrong route. See
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux17/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux17/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
|
||||
|
||||
@@ -158,7 +158,8 @@ remains `0`, and lateral regions remain gated off.
|
||||
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**, `VoxelForgeCore`. `AVoxelWorld::bUseVoxelForgeCore` opts into the Core runtime route; it defaults off. |
|
||||
| `../../VoxelForgeCore/VoxelForgeCore.Build.cs` | C++20 runtime module. Reads `Standalone/Core/core_sources.txt`, validates one include shim per canonical source, and fails UBT on missing, extra, or mismatched shims. No unity/PCH; private copied implementation stays under `vfcore::detail`. |
|
||||
| `../../VoxelForgeCore/Private/Manifest/VFCore_*.cpp` | Eighteen one-source UBT shims that include canonical files under `Standalone/`; they do not copy Core implementation. The API-boundary row includes `Core/src/VoxelForgeCore.cpp` directly. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/ThreadLocalStorage.h` | `VoxelForgeCoreThreadLocal::FLazy<T>`: thread-local handle that heap-allocates a worker-private cache on first use. All large Core and legacy caches use it (HeightfieldColumnCache, IslandColumnCache, GFloorReliefCache, GRoomGraphCache, GSurfColCache, tunnel/diff caches). See ARCHITECTURE §8.10. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/ThreadLocalStorage.h` | `VoxelForgeCoreThreadLocal::FLazy<T>`: thread-local handle that heap-allocates a worker-private cache on first use. Large worker-local caches use it (HeightfieldColumnCache, IslandColumnCache, GFloorReliefCache, GRoomGraphCache, GSurfColCache, tunnel/diff caches). See ARCHITECTURE §8.10. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h` | Bounded four-way second-tier cache for exact per-column values. Readers use versioned atomic slots without a lock; misses use a set-local spin lock. World-owned lifetime, recipe/plan identity and content hashes, XY and optional sub-domain identity. SurfaceWorld and FloatingIslands share this mechanism; see ARCHITECTURE §8.35. |
|
||||
| `../../Standalone/Core/src/CoreNamespacePreamble.h` | First include of every Core TU. Forces FP contraction off per compiler (Clang/MSVC/GCC pragmas; unknown compiler = `#error`) and defines `vfcore::fp_contract::selfTest`, which `World::compile` runs before building: a fused multiply-add (e.g. Clang `-ffp-contract=fast`) refuses world generation. The world depends on contraction (Linux2/Linux3 reports), so never remove either. |
|
||||
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
|
||||
@@ -1117,6 +1118,33 @@ restores the S13 path for the negative control. Runtime CVar capture lives in
|
||||
cold-path decision are in
|
||||
[`Standalone/Research/WorldAssemblerM3/Round18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round18/REPORT.md).
|
||||
|
||||
### 3.21 LOD1 uniform-block proof — World Assembler M3 Linux9
|
||||
|
||||
`FStandaloneShapeWorld::IsNativeSurfaceBox` gates the step-2 proof to a single native SurfaceWorld
|
||||
stratum. `ClassifyBoxDetailed(..., bUseNativeLatticeProof)` delegates to
|
||||
`Heightfield2D::classifyLatticeBox`, whose ground/cap extrema cover the MC lattice and normal halo.
|
||||
`UVoxelMarchingCubesMesher::GenerateMesh` sparsely fills only Mixed blocks and their sample halo;
|
||||
Mixed/Unknown remains on the fill path. The headless `--audit-uniform-block-proofs` forces sample
|
||||
verification, while `--profile-lod-work` reports per-LOD filled samples; both are off by default.
|
||||
Implementation and evidence: [`Standalone/Research/WorldAssemblerM3/Linux9/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux9/REPORT.md).
|
||||
|
||||
### 3.22 Route-invariant Core density — World Assembler M3 Linux13
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `FStandaloneShapeWorld::SetSharedDensitySource`, `SharedDensitySource` | `Standalone/Core/src/ShapeWorldInternal.h`, bound before publication in `Standalone/Core/src/VoxelForgeCore.cpp` | Makes compiled analytic pages delegate field, lattice, detail, classification, and solid queries to the Core world's base shape; `WorldAssemblyEvaluatorVersion` is 7. |
|
||||
|
||||
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 Owner/Core stratum coordinates — World Assembler M3 L17
|
||||
|
||||
`FStandaloneShapeWorld::GetCoreVoxelsPerOwnerChunk` is the shared conversion for manager chunk
|
||||
lookups, manifest layer bounds, readiness proofs, and adjacent native strata. FloatingIslands uses
|
||||
`FloatingIslandFieldV1`; its boundary seal bounds feature influence and does not fill an XY-wide
|
||||
solid cap. SurfaceWorld remains the `Heightfield2D` path. See ARCHITECTURE §8.37 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux17/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux17/REPORT.md).
|
||||
|
||||
## 4. The density pipeline (most-edited hot path)
|
||||
|
||||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||
|
||||
@@ -146,6 +146,10 @@ public:
|
||||
FVoxelForgeCoreTaskScheduler Scheduler;
|
||||
std::unordered_set<VoxelForgeCore::TileKey, VoxelForgeCore::TileKeyHash> RetainedTiles;
|
||||
FCriticalSection RetainedTilesLock;
|
||||
FCriticalSection ShapeReadinessLock;
|
||||
std::unordered_set<VoxelForgeCore::TileKey, VoxelForgeCore::TileKeyHash> ShapeFreeTiles;
|
||||
std::unordered_set<VoxelForgeCore::TileKey, VoxelForgeCore::TileKeyHash> ShapeDependentTiles;
|
||||
std::atomic<bool> bSnapshotReady{false};
|
||||
std::atomic<bool> bReady{false};
|
||||
std::atomic<bool> bInitialFailed{false};
|
||||
std::atomic<bool> bRegionCompileInFlight{false};
|
||||
@@ -7689,6 +7693,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
Request.includeFarWorld = true;
|
||||
|
||||
VoxelForgeCoreState = State;
|
||||
State->bSnapshotReady.store(true, std::memory_order_release);
|
||||
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
|
||||
UE::Tasks::Launch(TEXT("VoxelForgeCoreCompile"),
|
||||
[this, State, Request, InitialDesired, ActiveCoreClipRadius, ActiveCoreMaxClipLevel,
|
||||
@@ -7708,10 +7713,17 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
|
||||
std::string Error;
|
||||
bool bCompiled = false;
|
||||
{
|
||||
FWriteScopeLock Lock(State->WorldLock);
|
||||
bCompiled = State->World->compile(Request, State->Scheduler, Error);
|
||||
if (bCompiled)
|
||||
{
|
||||
// The initial compile builds into local state and atomically publishes an immutable
|
||||
// shape. Proven native-only tiles may hold shared WorldLock readers during this work;
|
||||
// take the writer only for region retention after publication.
|
||||
// La compilation initiale construit un etat local puis publie une forme immuable.
|
||||
// Les tuiles natives validees gardent le verrou lecteur partage; le verrou ecrivain
|
||||
// ne sert qu'a retenir les regions apres publication.
|
||||
FWriteScopeLock Lock(State->WorldLock);
|
||||
if (State->World)
|
||||
{
|
||||
State->World->retainTileRegions(InitialDesired);
|
||||
FScopeLock RetainedLock(&State->RetainedTilesLock);
|
||||
@@ -13019,9 +13031,33 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (PendingTiles.Contains(Tile) || !IsVoxelForgeCoreReady()) return;
|
||||
if (PendingTiles.Contains(Tile)) return;
|
||||
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
|
||||
if (!State.IsValid() || State->bRegionCompileFailed.load(std::memory_order_acquire)) return;
|
||||
if (!State.IsValid()||State->bInitialFailed.load(std::memory_order_acquire)
|
||||
||State->bRegionCompileFailed.load(std::memory_order_acquire)
|
||||
||State->bShuttingDown.load(std::memory_order_acquire))return;
|
||||
const bool bShapeReady=State->bReady.load(std::memory_order_acquire);
|
||||
if(!bShapeReady)
|
||||
{
|
||||
if(!State->bSnapshotReady.load(std::memory_order_acquire))return;
|
||||
const VoxelForgeCore::TileKey CoreKey{
|
||||
{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level};
|
||||
bool bShapeFree=false;
|
||||
{
|
||||
FScopeLock ReadinessLock(&State->ShapeReadinessLock);
|
||||
if(State->ShapeFreeTiles.contains(CoreKey))bShapeFree=true;
|
||||
else if(State->ShapeDependentTiles.contains(CoreKey))return;
|
||||
else
|
||||
{
|
||||
FReadScopeLock WorldReadLock(State->WorldLock);
|
||||
if(!State->World)return;
|
||||
bShapeFree=State->World->isTileShapeFree(CoreKey);
|
||||
if(bShapeFree)State->ShapeFreeTiles.insert(CoreKey);
|
||||
else State->ShapeDependentTiles.insert(CoreKey);
|
||||
}
|
||||
}
|
||||
if(!bShapeFree)return;
|
||||
}
|
||||
if (IsStreamingResultBacklogCapped())
|
||||
{
|
||||
StreamingResultBacklogCapStops.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
@@ -33,6 +33,8 @@ if [[ "$FP_CONTRACT" != "default" && "$FP_CONTRACT" != "off" && "$FP_CONTRACT" !
|
||||
fi
|
||||
command -v "$COMPILER" >/dev/null || { printf 'Compiler not found: %s\n' "$COMPILER" >&2; exit 2; }
|
||||
command -v ar >/dev/null || { printf 'ar not found\n' >&2; exit 2; }
|
||||
command -v pkg-config >/dev/null || { printf 'pkg-config not found\n' >&2; exit 2; }
|
||||
pkg-config --exists egl glesv2 libpng || { printf 'EGL, GLESv2, and libpng development files are required\n' >&2; exit 2; }
|
||||
|
||||
BUILD_DIR="$ROOT/Build/linux-$VARIANT"
|
||||
BIN_DIR="$ROOT/bin/linux"
|
||||
@@ -117,5 +119,10 @@ CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
|
||||
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
|
||||
OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT"
|
||||
"$COMPILER" -pthread "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
|
||||
CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT"
|
||||
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
|
||||
"$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \
|
||||
$(pkg-config --cflags --libs egl glesv2 libpng)
|
||||
printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT"
|
||||
printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_OUTPUT"
|
||||
printf 'Compiler: %s | variant: %s | fp-contract: %s | jobs: %s\n' "$COMPILER" "$VARIANT" "$FP_CONTRACT" "$JOBS"
|
||||
@@ -0,0 +1,183 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace VoxelForgeCoreShared
|
||||
{
|
||||
// Exact input identity for a pure XY column value. LOD is deliberately not part of this key:
|
||||
// the same recipe/plan column has the same bits at every sampling step.
|
||||
struct FColumnCacheKey
|
||||
{
|
||||
std::uint64_t recipeIdentity = 0;
|
||||
std::uint64_t recipeContentHash = 0;
|
||||
std::uint64_t planIdentity = 0;
|
||||
std::uint64_t planContentHash = 0;
|
||||
std::int64_t xSubvoxels = 0;
|
||||
std::int64_t ySubvoxels = 0;
|
||||
// Optional exact sub-domain identity for recipes with several independent columns
|
||||
// at the same XY (for example, a floating-island anchor).
|
||||
std::uint64_t contextIdentity = 0;
|
||||
|
||||
std::array<std::uint64_t, 7> words() const noexcept
|
||||
{
|
||||
return {recipeIdentity, recipeContentHash, planIdentity, planContentHash,
|
||||
static_cast<std::uint64_t>(xSubvoxels), static_cast<std::uint64_t>(ySubvoxels),
|
||||
contextIdentity};
|
||||
}
|
||||
};
|
||||
|
||||
// A bounded, world-owned second-level cache. Existing thread-local caches stay first, so
|
||||
// their ordinary hit path remains private and lock-free. Shared hits read atomic words; only
|
||||
// a shared miss takes the set spin lock. Entries are four-way set associative, evict on
|
||||
// collision, and carry pointer identities plus content hashes to reject stale reuse.
|
||||
// Les caches TLS restent le premier niveau; seul un defaut partage prend le verrou du groupe.
|
||||
template <typename TValue, std::size_t SetCount, std::size_t Ways = 4>
|
||||
class TSharedColumnValueCache
|
||||
{
|
||||
static_assert(SetCount > 0 && (SetCount & (SetCount - 1)) == 0,
|
||||
"column cache set count must be a power of two");
|
||||
static_assert(Ways > 0 && Ways <= 16, "unsupported column cache associativity");
|
||||
static_assert(std::is_trivially_copyable_v<TValue>,
|
||||
"shared column values must be trivially copyable");
|
||||
|
||||
static constexpr std::size_t ValueWordCount = (sizeof(TValue) + 7) / 8;
|
||||
using FKeyWords = std::array<std::uint64_t, 7>;
|
||||
using FValueWords = std::array<std::uint64_t, ValueWordCount>;
|
||||
|
||||
struct FEntry
|
||||
{
|
||||
std::atomic<std::uint64_t> Version{0};
|
||||
std::atomic<std::uint64_t> Tag{0};
|
||||
std::array<std::atomic<std::uint64_t>, 7> Key{};
|
||||
std::array<std::atomic<std::uint64_t>, ValueWordCount> Value{};
|
||||
};
|
||||
|
||||
struct FSet
|
||||
{
|
||||
std::atomic_flag Writer = ATOMIC_FLAG_INIT;
|
||||
std::uint8_t NextWay = 0;
|
||||
std::array<FEntry, Ways> Entries{};
|
||||
};
|
||||
|
||||
struct FSpinGuard
|
||||
{
|
||||
explicit FSpinGuard(std::atomic_flag& value) : Flag(value)
|
||||
{
|
||||
while (Flag.test_and_set(std::memory_order_acquire))
|
||||
std::this_thread::yield();
|
||||
}
|
||||
~FSpinGuard() { Flag.clear(std::memory_order_release); }
|
||||
std::atomic_flag& Flag;
|
||||
};
|
||||
|
||||
public:
|
||||
TSharedColumnValueCache() : Sets(std::make_unique<FSet[]>(SetCount)) {}
|
||||
TSharedColumnValueCache(const TSharedColumnValueCache&) = delete;
|
||||
TSharedColumnValueCache& operator=(const TSharedColumnValueCache&) = delete;
|
||||
|
||||
static TSharedColumnValueCache* setThreadActive(TSharedColumnValueCache* value) noexcept
|
||||
{
|
||||
TSharedColumnValueCache* previous = Active;
|
||||
Active = value;
|
||||
return previous;
|
||||
}
|
||||
|
||||
static TSharedColumnValueCache* threadActive() noexcept { return Active; }
|
||||
|
||||
template <typename TFactory>
|
||||
TValue getOrCompute(const FColumnCacheKey& key, TFactory&& factory,
|
||||
bool& computed)
|
||||
{
|
||||
computed = false;
|
||||
const FKeyWords keyWords = key.words();
|
||||
const std::uint64_t tag = Hash(keyWords);
|
||||
const std::size_t setIndex = static_cast<std::size_t>(tag) & (SetCount - 1);
|
||||
FSet& set = Sets[setIndex];
|
||||
TValue value{};
|
||||
if (TryReadSet(set, keyWords, tag, value)) return value;
|
||||
|
||||
FSpinGuard lock(set.Writer);
|
||||
if (TryReadSet(set, keyWords, tag, value)) return value;
|
||||
|
||||
value = std::forward<TFactory>(factory)();
|
||||
computed = true;
|
||||
FValueWords valueWords{};
|
||||
std::memcpy(valueWords.data(), &value, sizeof(TValue));
|
||||
|
||||
std::size_t replacement = Ways;
|
||||
for (std::size_t way = 0; way < Ways; ++way)
|
||||
if (set.Entries[way].Version.load(std::memory_order_seq_cst) == 0)
|
||||
{ replacement = way; break; }
|
||||
if (replacement == Ways)
|
||||
{
|
||||
replacement = set.NextWay;
|
||||
set.NextWay = static_cast<std::uint8_t>((set.NextWay + 1) % Ways);
|
||||
}
|
||||
FEntry& entry = set.Entries[replacement];
|
||||
std::uint64_t version = entry.Version.load(std::memory_order_seq_cst);
|
||||
if (version & 1u) ++version;
|
||||
entry.Version.store(version + 1, std::memory_order_seq_cst);
|
||||
for (std::size_t word = 0; word < keyWords.size(); ++word)
|
||||
entry.Key[word].store(keyWords[word], std::memory_order_seq_cst);
|
||||
for (std::size_t word = 0; word < valueWords.size(); ++word)
|
||||
entry.Value[word].store(valueWords[word], std::memory_order_seq_cst);
|
||||
entry.Tag.store(tag, std::memory_order_seq_cst);
|
||||
entry.Version.store(version + 2, std::memory_order_seq_cst);
|
||||
return value;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::uint64_t Hash(const FKeyWords& words) noexcept
|
||||
{
|
||||
std::uint64_t hash = 0x9E3779B185EBCA87ull;
|
||||
for (const std::uint64_t word : words)
|
||||
{
|
||||
hash ^= word + 0x9E3779B97F4A7C15ull + (hash << 6) + (hash >> 2);
|
||||
hash ^= hash >> 30;
|
||||
hash *= 0xBF58476D1CE4E5B9ull;
|
||||
hash ^= hash >> 27;
|
||||
hash *= 0x94D049BB133111EBull;
|
||||
hash ^= hash >> 31;
|
||||
}
|
||||
return hash == 0 ? 1 : hash;
|
||||
}
|
||||
|
||||
static bool TryRead(const FEntry& entry, const FKeyWords& keyWords,
|
||||
std::uint64_t tag, TValue& out)
|
||||
{
|
||||
const std::uint64_t before = entry.Version.load(std::memory_order_seq_cst);
|
||||
if (before == 0 || (before & 1u)
|
||||
|| entry.Tag.load(std::memory_order_seq_cst) != tag) return false;
|
||||
FKeyWords foundKey{};
|
||||
for (std::size_t word = 0; word < foundKey.size(); ++word)
|
||||
foundKey[word] = entry.Key[word].load(std::memory_order_seq_cst);
|
||||
if (foundKey != keyWords) return false;
|
||||
FValueWords foundValue{};
|
||||
for (std::size_t word = 0; word < foundValue.size(); ++word)
|
||||
foundValue[word] = entry.Value[word].load(std::memory_order_seq_cst);
|
||||
const std::uint64_t after = entry.Version.load(std::memory_order_seq_cst);
|
||||
if (before != after || (after & 1u)) return false;
|
||||
std::memcpy(&out, foundValue.data(), sizeof(TValue));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryReadSet(const FSet& set, const FKeyWords& keyWords,
|
||||
std::uint64_t tag, TValue& out)
|
||||
{
|
||||
for (const FEntry& entry : set.Entries)
|
||||
if (TryRead(entry, keyWords, tag, out)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<FSet[]> Sets;
|
||||
inline static thread_local TSharedColumnValueCache* Active = nullptr;
|
||||
};
|
||||
}
|
||||
@@ -110,9 +110,19 @@ namespace VoxelForgeCore
|
||||
|
||||
enum class TileClass : std::uint8_t { allSolid, allAir, mixed, unknown };
|
||||
|
||||
struct HeightfieldColumnEvaluation
|
||||
{
|
||||
std::int64_t x = 0, y = 0;
|
||||
std::uint64_t sequence = 0;
|
||||
bool outsideTileFootprint = false;
|
||||
};
|
||||
|
||||
struct TileGenerationStats
|
||||
{
|
||||
TileClass tileClass = TileClass::unknown;
|
||||
bool nativeOnlyRoute = false;
|
||||
bool analyticRegionPageRoute = false;
|
||||
bool nativeSurfaceTile = false;
|
||||
std::uint64_t solidBoxes = 0;
|
||||
std::uint64_t airBoxes = 0;
|
||||
std::uint64_t mixedBoxes = 0;
|
||||
@@ -124,6 +134,13 @@ namespace VoxelForgeCore
|
||||
std::uint64_t columnCacheHits = 0;
|
||||
std::uint64_t columnCacheMisses = 0;
|
||||
std::uint64_t columnCacheConflictMisses = 0;
|
||||
std::uint64_t sharedColumnCacheHits = 0;
|
||||
std::uint64_t columnEvaluations = 0;
|
||||
std::uint64_t uniformBlockProofsChecked = 0;
|
||||
std::uint64_t uniformBlockProofSamplesChecked = 0;
|
||||
std::uint64_t uniformBlockProofFallbackBlocks = 0;
|
||||
std::uint64_t uniformBlockProofUnknownBlocks = 0;
|
||||
std::uint64_t uniformBlockProofViolations = 0;
|
||||
double classificationMilliseconds = 0.0;
|
||||
double columnComputeMilliseconds = 0.0;
|
||||
double columnEvaluationExclusiveMilliseconds = 0.0;
|
||||
@@ -137,6 +154,7 @@ namespace VoxelForgeCore
|
||||
double marchingCubesGradientNormalsMilliseconds = 0.0;
|
||||
double marchingCubesEdgeInterpolationMilliseconds = 0.0;
|
||||
double marchingCubesVertexDedupStreamMilliseconds = 0.0;
|
||||
double marchingCubesSkirtMilliseconds = 0.0;
|
||||
double marchingCubesOtherMilliseconds = 0.0;
|
||||
std::uint64_t marchingCubesGradientFallbackDensityCalls = 0;
|
||||
std::uint64_t marchingCubesEdgeMidpointDensityCalls = 0;
|
||||
@@ -152,6 +170,20 @@ namespace VoxelForgeCore
|
||||
double farHorizonMilliseconds = 0.0;
|
||||
std::uint64_t farStratumFieldCalls = 0;
|
||||
std::uint64_t farConnectorFieldCalls = 0;
|
||||
std::uint64_t farStratumColumnPointCalls = 0;
|
||||
std::uint64_t farStratumColumnCacheHits = 0;
|
||||
std::uint64_t farStratumColumnCacheMisses = 0;
|
||||
std::uint64_t farStratumColumnConflictMisses = 0;
|
||||
std::uint64_t farStratumSharedColumnCacheHits = 0;
|
||||
std::uint64_t farStratumColumnEvaluations = 0;
|
||||
std::uint64_t farConnectorColumnPointCalls = 0;
|
||||
std::uint64_t farConnectorColumnCacheHits = 0;
|
||||
std::uint64_t farConnectorColumnCacheMisses = 0;
|
||||
std::uint64_t farConnectorColumnConflictMisses = 0;
|
||||
std::uint64_t farConnectorSharedColumnCacheHits = 0;
|
||||
std::uint64_t farConnectorColumnEvaluations = 0;
|
||||
double farStratumColumnComputeMilliseconds = 0.0;
|
||||
double farConnectorColumnComputeMilliseconds = 0.0;
|
||||
std::uint64_t farShellTrianglesAdded = 0;
|
||||
std::uint64_t horizonGroundTriangles = 0;
|
||||
std::uint64_t horizonGroundTrianglesRemoved = 0;
|
||||
@@ -167,7 +199,7 @@ namespace VoxelForgeCore
|
||||
bool capRefined = false;
|
||||
std::uint64_t columnEvaluationWorkerHash = 0;
|
||||
bool columnCoordinateCaptureComplete = true;
|
||||
std::vector<std::array<std::int64_t, 2>> evaluatedColumnCoordinates;
|
||||
std::vector<HeightfieldColumnEvaluation> evaluatedColumnCoordinates;
|
||||
};
|
||||
|
||||
struct TileMesh
|
||||
@@ -217,7 +249,8 @@ namespace VoxelForgeCore
|
||||
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
||||
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 1;
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 5;
|
||||
// Version 7 fixes owner/Core chunk mapping and native adjacent-stratum boundaries.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 7;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
@@ -650,10 +683,12 @@ namespace VoxelForgeCore
|
||||
std::uint64_t columnHits = 0;
|
||||
std::uint64_t columnMisses = 0;
|
||||
std::uint64_t columnConflictMisses = 0;
|
||||
std::uint64_t sharedColumnHits = 0;
|
||||
std::uint64_t columnEvaluations = 0;
|
||||
double columnComputeMilliseconds = 0.0;
|
||||
bool captureCoordinates = false;
|
||||
bool coordinateCaptureComplete = true;
|
||||
std::vector<std::array<std::int64_t, 2>> evaluatedCoordinates;
|
||||
std::vector<HeightfieldColumnEvaluation> evaluatedCoordinates;
|
||||
};
|
||||
|
||||
struct FieldInfluenceBounds
|
||||
@@ -704,6 +739,13 @@ namespace VoxelForgeCore
|
||||
std::int64_t maximumZSubvoxels, std::int32_t stepVoxels) const noexcept;
|
||||
static void resetThreadColumnCacheStats(bool captureCoordinates = false,
|
||||
bool clearCache = false, std::uint32_t octaveDrop = 0) noexcept;
|
||||
static void setThreadColumnCaptureBounds(std::int64_t minimumXSubvoxels,
|
||||
std::int64_t minimumYSubvoxels, std::int64_t maximumXSubvoxels,
|
||||
std::int64_t maximumYSubvoxels) noexcept;
|
||||
static void setThreadColumnOctaveDrop(std::uint32_t octaveDrop) noexcept;
|
||||
static std::uint32_t threadColumnOctaveDrop() noexcept;
|
||||
static void setThreadColumnInterpolationStrideSubvoxels(std::int64_t stride) noexcept;
|
||||
static std::int64_t threadColumnInterpolationStrideSubvoxels() noexcept;
|
||||
static void setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept;
|
||||
static const HeightfieldColumnCacheStats& threadColumnCacheStats() noexcept;
|
||||
FieldInterval interval(std::int64_t minimumXSubvoxels,
|
||||
@@ -1023,6 +1065,9 @@ namespace VoxelForgeCore
|
||||
std::int32_t bandMaxZVoxel = 2147483647;
|
||||
bool hashDensity = false;
|
||||
bool captureDensitySamples = false;
|
||||
// Diagnostic only; materialize the full density lattice even when block proofs
|
||||
// establish that the tile has no surface.
|
||||
bool forceFullDensityCapture = false;
|
||||
bool skipUvAndColors = false;
|
||||
bool skipUv = false;
|
||||
bool skipColors = false;
|
||||
@@ -1033,9 +1078,13 @@ namespace VoxelForgeCore
|
||||
// Opt-in diagnostic only; records XY columns evaluated by this LOD8 tile.
|
||||
// Diagnostic seulement; enregistre les colonnes XY evaluees par cette tuile LOD8.
|
||||
bool captureColumnCoordinates = false;
|
||||
bool useSharedColumnCache = true;
|
||||
// Separate cold pass only; samples the MC inner stages without changing output.
|
||||
// Passe froide separee seulement; mesure les etapes internes MC sans changer la sortie.
|
||||
bool profileMesherStages = false;
|
||||
// Diagnostic only; fills and checks every lattice sample in blocks certified uniform.
|
||||
// Diagnostic seulement; evalue et verifie tous les echantillons des blocs uniformes.
|
||||
bool auditUniformBlockProofs = false;
|
||||
// Negative control for the shared XY-column cache; 1 restores the old direct-map indexing.
|
||||
// Controle negatif du cache XY partage; 1 restaure l'ancien indexage direct.
|
||||
bool legacyDirectMappedColumnCache = false;
|
||||
@@ -1050,6 +1099,15 @@ namespace VoxelForgeCore
|
||||
// Incremente quand le plafond lointain change. Les hashes LOD4-8 incluent cette version;
|
||||
// densite et hashes LOD0-3 restent inchanges.
|
||||
std::uint32_t farHorizonVersion = 3;
|
||||
// Linux research controls for reproducing L5 negative controls. Defaults preserve output.
|
||||
std::uint32_t diagnosticGlobalOctaveDrop = 0;
|
||||
std::uint32_t diagnosticCellStride = 1;
|
||||
std::uint32_t diagnosticColumnInterpolationStride = 1;
|
||||
// Diagnostic negative control only: deliberately bypass the native-only readiness
|
||||
// proof and require the sampling guard to reject any shape-dependent tile.
|
||||
// Controle negatif diagnostique: contourne volontairement la preuve native seule
|
||||
// et exige que la garde rejette toute tuile dependante de la forme.
|
||||
bool diagnosticForceNativeOnly = false;
|
||||
};
|
||||
|
||||
class VFCORE_API World
|
||||
@@ -1067,16 +1125,26 @@ namespace VoxelForgeCore
|
||||
// True only when a fine tile needs a separately compiled region before generation.
|
||||
// Vrai seulement si une tuile fine doit attendre la compilation d'une region dediee.
|
||||
bool needsTileRegion(const TileKey& tile) const;
|
||||
// Conservative precompile proof: true only for a tile that can sample the native
|
||||
// SurfaceWorld heightfield throughout its padded mesh domain without compiled shape data.
|
||||
// Preuve prudente avant compilation: vrai seulement si toute la zone maillee avec marge
|
||||
// utilise le champ SurfaceWorld natif, sans lire la forme compilee.
|
||||
bool isTileShapeFree(const TileKey& tile) const;
|
||||
// Compile missing detail regions for entering fine tiles. Generated tile keys map to
|
||||
// shared world-coordinate regions. The default analyticOnly path coalesces each entering
|
||||
// LOD slab, builds its primitives/bins, and evaluates density on demand without
|
||||
// rasterizing unused lattice volumes. Set false to use fixed absolute packed pages. The
|
||||
// optional count is newly covered tile keys; regions stay resident until retainTileRegions.
|
||||
// bounded, world-aligned pages. The default analyticOnly path builds each page's
|
||||
// primitives/bins and evaluates density on demand without rasterizing unused lattice
|
||||
// volumes. Set false to use fixed absolute packed pages. The optional count is newly
|
||||
// covered tile keys; regions stay resident until retainTileRegions.
|
||||
// Compile les regions de detail manquantes. Les tuiles entrantes sont regroupees en pages
|
||||
// bornees alignees au monde; analyticOnly construit les primitives et bins sans rasteriser
|
||||
// de volume inutilise. Sinon, utilise les pages compactes fixes; le compteur indique les
|
||||
// nouvelles tuiles couvertes et retainTileRegions libere les anciennes regions.
|
||||
bool compileTileRegion(std::span<const TileKey> tiles, std::uint32_t workerLimit,
|
||||
const JobScheduler& scheduler, std::string& error,
|
||||
std::size_t* outCompiledTiles = nullptr,
|
||||
TileRegionCompileStats* outStats = nullptr,
|
||||
bool analyticOnly = true);
|
||||
bool analyticOnly = true,
|
||||
bool diagnosticIncludeBaseCoveredTiles = false);
|
||||
void retainTileRegions(std::span<const TileKey> desiredTiles);
|
||||
void buildFarWorld();
|
||||
bool isCompiled() const noexcept;
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "Json.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
||||
#include "VoxelForgeCore/SharedColumnCache.h"
|
||||
|
||||
// Keep Core's expression-level FMA behavior fixed regardless of the toolchain setting.
|
||||
// Clang's "fast" mode ignores the pragma, so Core's runtime self-test rejects that build.
|
||||
|
||||
@@ -425,6 +425,9 @@ namespace floating_island_detail
|
||||
struct FIslandColumnCacheEntry
|
||||
{
|
||||
bool bValid=false;
|
||||
const FloatingIslandRecipeV1* RecipeIdentity=nullptr;
|
||||
const FloatingIslandPlanV1* PlanIdentity=nullptr;
|
||||
std::uint64_t RecipeContentHash=0,PlanContentHash=0;
|
||||
std::uint32_t Seed=0;
|
||||
std::uint64_t StableId=0;
|
||||
std::int64_t SampleX=0,SampleY=0,CenterX=0,CenterY=0,TopZ=0,Radius=0,TopHalf=0;
|
||||
@@ -432,9 +435,12 @@ namespace floating_island_detail
|
||||
double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0;
|
||||
};
|
||||
|
||||
using FSharedIslandColumnCache =
|
||||
VoxelForgeCoreShared::TSharedColumnValueCache<FIslandColumnCacheEntry,16384,4>;
|
||||
|
||||
inline const FIslandColumnCacheEntry& IslandColumnCache(
|
||||
const FloatingIslandRecipeV1& recipe,const FloatingIslandAnchorV1& island,
|
||||
double x,double y)
|
||||
const FloatingIslandRecipeV1& recipe,const FloatingIslandPlanV1& plan,
|
||||
const FloatingIslandAnchorV1& island,double x,double y)
|
||||
{
|
||||
// Density samples arrive on integer Core lattice columns; reuse the exact column metrics
|
||||
// across Z samples without changing the field value or touching shared world state.
|
||||
@@ -450,7 +456,10 @@ namespace floating_island_detail
|
||||
std::uint64_t IndexKey=island.stableId
|
||||
^(static_cast<std::uint64_t>(SampleX)*0x9e3779b97f4a7c15ull)
|
||||
^(static_cast<std::uint64_t>(SampleY)*0xc2b2ae3d27d4eb4full)
|
||||
^(static_cast<std::uint64_t>(recipe.seed)<<23);
|
||||
^(static_cast<std::uint64_t>(recipe.seed)<<23)
|
||||
^recipe.contentHash^std::rotl(plan.contentHash,17)
|
||||
^static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe))
|
||||
^std::rotl(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),31);
|
||||
IndexKey^=IndexKey>>33;
|
||||
IndexKey*=0xff51afd7ed558ccdull;
|
||||
IndexKey^=IndexKey>>33;
|
||||
@@ -459,7 +468,9 @@ namespace floating_island_detail
|
||||
for(std::size_t Way=0;Way<Ways;++Way)
|
||||
{
|
||||
FIslandColumnCacheEntry& Entry=cache[Base+Way];
|
||||
const bool bKeyMatches=Entry.bValid&&Entry.Seed==recipe.seed
|
||||
const bool bKeyMatches=Entry.bValid&&Entry.RecipeIdentity==&recipe
|
||||
&&Entry.PlanIdentity==&plan&&Entry.RecipeContentHash==recipe.contentHash
|
||||
&&Entry.PlanContentHash==plan.contentHash&&Entry.Seed==recipe.seed
|
||||
&&Entry.StableId==island.stableId
|
||||
&&Entry.SampleX==SampleX&&Entry.SampleY==SampleY
|
||||
&&Entry.CenterX==island.centerSubvoxels[0]
|
||||
@@ -473,8 +484,14 @@ namespace floating_island_detail
|
||||
if(!Entry.bValid)Replacement=&Entry;
|
||||
}
|
||||
|
||||
FIslandColumnCacheEntry& Entry=*Replacement;
|
||||
const auto build=[&]()
|
||||
{
|
||||
FIslandColumnCacheEntry Entry;
|
||||
Entry.bValid=true;
|
||||
Entry.RecipeIdentity=&recipe;
|
||||
Entry.PlanIdentity=&plan;
|
||||
Entry.RecipeContentHash=recipe.contentHash;
|
||||
Entry.PlanContentHash=plan.contentHash;
|
||||
Entry.Seed=recipe.seed;
|
||||
Entry.StableId=island.stableId;
|
||||
Entry.SampleX=SampleX;
|
||||
@@ -499,6 +516,21 @@ namespace floating_island_detail
|
||||
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
|
||||
Entry.TopSurface=static_cast<double>(island.topZSubvoxels)-Drop;
|
||||
return Entry;
|
||||
};
|
||||
FIslandColumnCacheEntry Entry{};
|
||||
if(auto* shared=FSharedIslandColumnCache::threadActive())
|
||||
{
|
||||
const VoxelForgeCoreShared::FColumnCacheKey key{
|
||||
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe)),
|
||||
recipe.contentHash,
|
||||
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
|
||||
plan.contentHash,SampleX,SampleY,island.stableId};
|
||||
bool computed=false;
|
||||
Entry=shared->getOrCompute(key,build,computed);
|
||||
}
|
||||
else Entry=build();
|
||||
*Replacement=Entry;
|
||||
return *Replacement;
|
||||
}
|
||||
|
||||
inline std::int64_t ScaledQ16(std::int64_t value, std::int64_t scaleQ16)
|
||||
@@ -641,6 +673,7 @@ namespace floating_island_detail
|
||||
}
|
||||
|
||||
inline double IslandDensity(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandPlanV1& plan,
|
||||
const FloatingIslandAnchorV1& island,
|
||||
double x, double y, double z)
|
||||
{
|
||||
@@ -665,7 +698,7 @@ namespace floating_island_detail
|
||||
std::max({z - top, radial - envelope, bottomZ - z}), x, y, z);
|
||||
}
|
||||
|
||||
const FIslandColumnCacheEntry& Column=IslandColumnCache(recipe,island,x,y);
|
||||
const FIslandColumnCacheEntry& Column=IslandColumnCache(recipe,plan,island,x,y);
|
||||
const double dx=Column.DeltaX;
|
||||
const double dy=Column.DeltaY;
|
||||
const double radial=Column.Radial;
|
||||
@@ -733,7 +766,7 @@ namespace floating_island_detail
|
||||
double result = static_cast<double>(MaxDensity);
|
||||
for (const auto& island : plan.islands)
|
||||
{
|
||||
double group = IslandDensity(recipe, island,
|
||||
double group = IslandDensity(recipe, plan, island,
|
||||
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
|
||||
for (const auto& edge : plan.clusterEdges)
|
||||
{
|
||||
@@ -769,7 +802,7 @@ namespace floating_island_detail
|
||||
const auto evaluateLocalGroup = [&](std::size_t islandIndex)
|
||||
{
|
||||
const auto& island = plan.islands[islandIndex];
|
||||
double group = IslandDensity(recipe, island,
|
||||
double group = IslandDensity(recipe, plan, island,
|
||||
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
|
||||
for (const std::uint32_t edgeIndex : featureIndices)
|
||||
{
|
||||
@@ -795,7 +828,7 @@ namespace floating_island_detail
|
||||
const auto evaluateDistantGroup = [&](std::size_t islandIndex)
|
||||
{
|
||||
const auto& island = plan.islands[islandIndex];
|
||||
double group = IslandDensity(recipe, island,
|
||||
double group = IslandDensity(recipe, plan, island,
|
||||
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
|
||||
for (const auto& edge : plan.clusterEdges)
|
||||
{
|
||||
|
||||
@@ -47,6 +47,18 @@ public:
|
||||
return static_cast<double>(CHUNK_SIZE) * OwnerVoxelSizeCm / GetCoreVoxelSizeCm();
|
||||
}
|
||||
|
||||
int32 OwnerChunkLowerBoundaryCoreVoxel(int32 ChunkZ) const
|
||||
{
|
||||
return static_cast<int32>(std::floor(
|
||||
static_cast<double>(ChunkZ) * GetCoreVoxelsPerOwnerChunk()));
|
||||
}
|
||||
|
||||
int32 OwnerChunkUpperBoundaryCoreVoxel(int32 ChunkZ) const
|
||||
{
|
||||
return static_cast<int32>(std::ceil(
|
||||
static_cast<double>(ChunkZ) * GetCoreVoxelsPerOwnerChunk()));
|
||||
}
|
||||
|
||||
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
|
||||
struct FBoxClassification
|
||||
{
|
||||
@@ -319,6 +331,7 @@ public:
|
||||
Owner=&World;WorldAssembly=InWorldAssembly;
|
||||
WorkerCount=std::clamp(Workers,1,16);DetailVariant=Variant;
|
||||
Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels;
|
||||
bUseNativeManifestFields=true;
|
||||
bAnalyticOnly=false;
|
||||
bProfileDetailBand=bInProfileDetailBand;
|
||||
for(int32 A=0;A<3;++A)
|
||||
@@ -470,10 +483,16 @@ public:
|
||||
bool BuildAnalyticRegion(FStandaloneOwnerWorld& World,const FIntVector& MinVoxels,
|
||||
const FIntVector& MaxVoxels,const FIntVector& ClipMinVoxels,
|
||||
const FIntVector& ClipMaxVoxels,EDetailVariant Variant,
|
||||
bool bInRoundTunnels,std::string& Error)
|
||||
bool bInRoundTunnels,
|
||||
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly,
|
||||
std::string& Error)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
Owner=&World;WorkerCount=1;DetailVariant=Variant;Scheduler=nullptr;
|
||||
Owner=&World;WorldAssembly=InWorldAssembly;
|
||||
WorkerCount=1;DetailVariant=Variant;Scheduler=nullptr;
|
||||
// Analytic pages retain their bounded owner bins for route bookkeeping. Once published,
|
||||
// the Core world binds each page to its base shape as the single density evaluator.
|
||||
bUseNativeManifestFields=true;
|
||||
RoundTunnelsEnabled=bInRoundTunnels;bAnalyticOnly=true;bProfileDetailBand=false;
|
||||
for(int32 A=0;A<3;++A)
|
||||
{
|
||||
@@ -487,7 +506,13 @@ public:
|
||||
const auto Stage=Clock::now();
|
||||
BuildLayerBounds();BuildOwnerPrimitives();
|
||||
if(RoundTunnelsEnabled)BuildRoundTunnelPrimitives();
|
||||
BuildSpatialBins(LineBinSize);
|
||||
// Analytic regions never build packed interval lines. Their exact-density fallback only
|
||||
// reads the sparse 3D primitive bins; allocating the dense per-axis line-bin grids here
|
||||
// scales with the region's XY bounding rectangle and is both unnecessary and unbounded.
|
||||
// Les regions analytiques n'utilisent pas les intervalles compactes; leur repli exact lit
|
||||
// les bins 3D clairsemes. Les grilles denses par axe croitraient avec la surface XY sans
|
||||
// servir ce chemin.
|
||||
BuildSpatialBins(LineBinSize,false);
|
||||
Stats.ShapeSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
|
||||
Stats.PrimitiveCount=Primitives.size();
|
||||
if(Primitives.empty())
|
||||
@@ -497,6 +522,59 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
void InitializeNativeSurfaceOnly(FStandaloneOwnerWorld& World,
|
||||
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly,
|
||||
const FIntVector& InMinimum,const FIntVector& InMaximum)
|
||||
{
|
||||
Owner=&World;WorldAssembly=InWorldAssembly;
|
||||
NativeOnlyMinimum=InMinimum;NativeOnlyMaximum=InMaximum;
|
||||
bUseNativeManifestFields=true;bNativeOnly=true;bAnalyticOnly=true;Built=true;
|
||||
NativeOnlyViolations.store(0,std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool BuildShapeDependencyProof(FStandaloneOwnerWorld& World,
|
||||
const FIntVector& MinVoxels,const FIntVector& MaxVoxels,bool bInRoundTunnels)
|
||||
{
|
||||
Owner=&World;WorkerCount=1;RoundTunnelsEnabled=bInRoundTunnels;
|
||||
bAnalyticOnly=true;bNativeOnly=false;Stats={};
|
||||
for(int32 Axis=0;Axis<3;++Axis)
|
||||
{
|
||||
Min[Axis]=(&MinVoxels.X)[Axis]-2;Max[Axis]=(&MaxVoxels.X)[Axis]+2;
|
||||
ClipMin[Axis]=(&MinVoxels.X)[Axis];ClipMax[Axis]=(&MaxVoxels.X)[Axis];
|
||||
Size[Axis]=Max[Axis]-Min[Axis]+1;
|
||||
if(Size[Axis]<=0||Size[Axis]>4096||ClipMax[Axis]<ClipMin[Axis])return false;
|
||||
}
|
||||
BuildLayerBounds();BuildOwnerPrimitives();
|
||||
if(RoundTunnelsEnabled)BuildRoundTunnelPrimitives();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HasShapeDependencyInBox(const FIntVector& Origin,int32 Step,int32 Cells) const
|
||||
{
|
||||
const int32 Pad=std::max(Step,1);
|
||||
const int32 BMin[3]={Origin.X-Pad,Origin.Y-Pad,Origin.Z-Pad};
|
||||
const int32 BMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,
|
||||
Origin.Z+(Cells+1)*Step};
|
||||
for(const FPrimitive& Primitive:Primitives)
|
||||
{
|
||||
bool bOverlaps=true;
|
||||
for(int32 Axis=0;Axis<3;++Axis)
|
||||
bOverlaps=bOverlaps&&Primitive.Max[Axis]+DetailBand>=BMin[Axis]
|
||||
&&Primitive.Min[Axis]-DetailBand<=BMax[Axis];
|
||||
if(bOverlaps)return true;
|
||||
}
|
||||
for(const FInterval& Range:LayerRanges)
|
||||
if(Range.Lo<=BMax[2]&&Range.Hi>=BMin[2]
|
||||
&&(Range.Lo>=BMin[2]||Range.Hi<=BMax[2]))return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsNativeOnly() const { return bNativeOnly; }
|
||||
uint32 GetNativeOnlyViolationCount() const
|
||||
{ return NativeOnlyViolations.load(std::memory_order_relaxed); }
|
||||
void RecordNativeOnlyViolation() const
|
||||
{ NativeOnlyViolations.fetch_add(1,std::memory_order_relaxed); }
|
||||
|
||||
const FBuildStats& GetStats() const { return Stats; }
|
||||
bool IsBuilt() const { return Built; }
|
||||
bool HasFarWorld() const { return FarBuilt; }
|
||||
@@ -646,14 +724,21 @@ public:
|
||||
if(Owner&&Owner->Manager)
|
||||
{
|
||||
const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast<double>(Cells)*Step;
|
||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
||||
for(const FStrateSlot& Slot:Owner->Manager->GetLayout())
|
||||
{
|
||||
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
|
||||
if(bUseNativeManifestFields
|
||||
&&Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,MidChunkZ))
|
||||
==ECaveGeneratorType::FloatingIslands)
|
||||
continue;
|
||||
const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(
|
||||
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
|
||||
FIntVector(0,0,MidChunkZ));
|
||||
const float HalfWidth=static_cast<float>(std::max(8.0,
|
||||
static_cast<double>(Params.BoundarySealThickness)+8.0));
|
||||
const float Boundaries[2]={static_cast<float>(Slot.BottomChunkZ)*CHUNK_SIZE,
|
||||
static_cast<float>(Slot.TopChunkZ+1)*CHUNK_SIZE};
|
||||
const float Boundaries[2]={
|
||||
static_cast<float>(static_cast<double>(Slot.BottomChunkZ)*CoreVoxelsPerOwnerChunk),
|
||||
static_cast<float>(static_cast<double>(Slot.TopChunkZ+1)*CoreVoxelsPerOwnerChunk)};
|
||||
for(const float Z:Boundaries)
|
||||
if(Z+HalfWidth>=TileZ0&&Z-HalfWidth<=TileZ1)
|
||||
Out.stratumBoundaries.push_back({Z,HalfWidth});
|
||||
@@ -925,6 +1010,9 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void SetSharedDensitySource(const FStandaloneShapeWorld* Source)
|
||||
{ SharedDensitySource=Source==this?nullptr:Source; }
|
||||
|
||||
bool HasEditTransaction() const { return !EditBrushes.empty(); }
|
||||
FIntVector GetEditBoundsMin() const { return EditBoundsMin; }
|
||||
FIntVector GetEditBoundsMax() const { return EditBoundsMax; }
|
||||
@@ -951,17 +1039,29 @@ public:
|
||||
// edge interpolation and normals to the existing mesher.
|
||||
float DensityAt(float X,float Y,float Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->DensityAt(X,Y,Z);
|
||||
if(bNativeOnly)
|
||||
{
|
||||
if(!NativeOnlyContains(X,Y,Z))
|
||||
{RecordNativeOnlyViolation();return 0.0f;}
|
||||
float NativeDensity=0.0f;
|
||||
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||
{RecordNativeOnlyViolation();return 0.0f;}
|
||||
return ApplyAdjacentStratumBoundary(X,Y,Z,NativeDensity,false);
|
||||
}
|
||||
float IslandDensity=0.0f;
|
||||
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
||||
{
|
||||
++ThreadClassificationStats().NativeFloatFieldCalls;
|
||||
return ApplyEditBrushes(IslandDensity,X,Y,Z);
|
||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
||||
X,Y,Z,IslandDensity,true),X,Y,Z);
|
||||
}
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||
{
|
||||
++ThreadClassificationStats().NativeFloatFieldCalls;
|
||||
return ApplyEditBrushes(NativeDensity,X,Y,Z);
|
||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
||||
X,Y,Z,NativeDensity,false),X,Y,Z);
|
||||
}
|
||||
if(UsesLegacySurfaceField(X,Y,Z))
|
||||
return LegacySurfaceDensityAt(X,Y,Z);
|
||||
@@ -975,19 +1075,37 @@ public:
|
||||
|
||||
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->DensityAtLattice(X,Y,Z);
|
||||
if(bNativeOnly)
|
||||
{
|
||||
if(!NativeOnlyContains(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z)))
|
||||
{RecordNativeOnlyViolation();return 0.0f;}
|
||||
float NativeDensity=0.0f;
|
||||
if(!NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),NativeDensity))
|
||||
{RecordNativeOnlyViolation();return 0.0f;}
|
||||
return ApplyAdjacentStratumBoundary(
|
||||
static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z),
|
||||
NativeDensity,false);
|
||||
}
|
||||
float IslandDensity=0.0f;
|
||||
if(NativeFloatingIslandDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),IslandDensity))
|
||||
{
|
||||
++ThreadClassificationStats().NativeLatticeFieldCalls;
|
||||
return ApplyEditBrushes(IslandDensity,X,Y,Z);
|
||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
||||
static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z),
|
||||
IslandDensity,true),X,Y,Z);
|
||||
}
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),NativeDensity))
|
||||
{
|
||||
++ThreadClassificationStats().NativeLatticeFieldCalls;
|
||||
return ApplyEditBrushes(NativeDensity,X,Y,Z);
|
||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
||||
static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z),
|
||||
NativeDensity,false),X,Y,Z);
|
||||
}
|
||||
if(UsesLegacySurfaceField(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z)))
|
||||
@@ -1003,17 +1121,29 @@ public:
|
||||
|
||||
float DensityAtUnclamped(float X,float Y,float Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->DensityAtUnclamped(X,Y,Z);
|
||||
if(bNativeOnly)
|
||||
{
|
||||
if(!NativeOnlyContains(X,Y,Z))
|
||||
{RecordNativeOnlyViolation();return 0.0f;}
|
||||
float NativeDensity=0.0f;
|
||||
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||
{RecordNativeOnlyViolation();return 0.0f;}
|
||||
return ApplyAdjacentStratumBoundary(X,Y,Z,NativeDensity,false);
|
||||
}
|
||||
float IslandDensity=0.0f;
|
||||
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
||||
{
|
||||
++ThreadClassificationStats().NativeUnclampedFieldCalls;
|
||||
return ApplyEditBrushes(IslandDensity,X,Y,Z);
|
||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
||||
X,Y,Z,IslandDensity,true),X,Y,Z);
|
||||
}
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||
{
|
||||
++ThreadClassificationStats().NativeUnclampedFieldCalls;
|
||||
return ApplyEditBrushes(NativeDensity,X,Y,Z);
|
||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
||||
X,Y,Z,NativeDensity,false),X,Y,Z);
|
||||
}
|
||||
if(UsesLegacySurfaceField(X,Y,Z))
|
||||
return LegacySurfaceDensityAt(X,Y,Z);
|
||||
@@ -1022,7 +1152,10 @@ public:
|
||||
|
||||
VoxelForgeCore::DetailFieldSample DetailSampleAt(float X,float Y,float Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->DetailSampleAt(X,Y,Z);
|
||||
VoxelForgeCore::DetailFieldSample Sample;
|
||||
if(bNativeOnly)
|
||||
{RecordNativeOnlyViolation();return Sample;}
|
||||
AnalyticDensityAt(X,Y,Z,false,&Sample.displacement,
|
||||
&Sample.caveInfluence,&Sample.floorInfluence);
|
||||
return Sample;
|
||||
@@ -1038,8 +1171,19 @@ public:
|
||||
void DensityAtLatticeX8(int32 XStart,int32 XStep,int32 Y,int32 Z,
|
||||
int32 Count,float OutDensity[8]) const
|
||||
{
|
||||
if(SharedDensitySource)
|
||||
{
|
||||
SharedDensitySource->DensityAtLatticeX8(XStart,XStep,Y,Z,Count,OutDensity);
|
||||
return;
|
||||
}
|
||||
Count=std::clamp(Count,0,8);
|
||||
if(Count==0)return;
|
||||
if(bNativeOnly)
|
||||
{
|
||||
for(int32 Lane=0;Lane<Count;++Lane)
|
||||
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
|
||||
return;
|
||||
}
|
||||
bool bHasLegacySurfaceSample=false;
|
||||
bool bHasNativeIslandSample=false;
|
||||
for(int32 Lane=0;Lane<Count;++Lane)
|
||||
@@ -1138,17 +1282,25 @@ private:
|
||||
float* OutFloorInfluence=nullptr,
|
||||
bool bApplyEdits=true) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->AnalyticDensityAt(X,Y,Z,
|
||||
bClampNearZero,OutDetailDisplacement,OutCaveInfluence,OutFloorInfluence,bApplyEdits);
|
||||
if(OutDetailDisplacement)*OutDetailDisplacement=0.0f;
|
||||
if(OutCaveInfluence)*OutCaveInfluence=0.0f;
|
||||
if(OutFloorInfluence)*OutFloorInfluence=0.0f;
|
||||
float IslandDensity=0.0f;
|
||||
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
||||
{
|
||||
IslandDensity=ApplyAdjacentStratumBoundary(X,Y,Z,IslandDensity,true);
|
||||
return bApplyEdits?ApplyEditBrushes(IslandDensity,X,Y,Z):IslandDensity;
|
||||
}
|
||||
if(UsesLegacySurfaceField(X,Y,Z))
|
||||
{
|
||||
float NativeDensity=0.0f;
|
||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||
{
|
||||
NativeDensity=ApplyAdjacentStratumBoundary(X,Y,Z,NativeDensity,false);
|
||||
return bApplyEdits?ApplyEditBrushes(NativeDensity,X,Y,Z):NativeDensity;
|
||||
}
|
||||
const float Density=Owner->Generator->GetDensityAt(X,Y,Z);
|
||||
return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density;
|
||||
}
|
||||
@@ -1200,12 +1352,24 @@ public:
|
||||
bool GetNativeSurfaceHeightsAt(float X,float Y,int32 ChunkZ,
|
||||
float& OutGroundZ,float& OutCeilingZ) const
|
||||
{
|
||||
if(SharedDensitySource)
|
||||
return SharedDensitySource->GetNativeSurfaceHeightsAt(X,Y,ChunkZ,OutGroundZ,OutCeilingZ);
|
||||
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
||||
||!std::isfinite(X)||!std::isfinite(Y))return false;
|
||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
||||
const std::int64_t chunkMinimum=static_cast<std::int64_t>(std::floor(
|
||||
static_cast<double>(ChunkZ)*CoreVoxelsPerOwnerChunk));
|
||||
const std::int64_t chunkMaximum=static_cast<std::int64_t>(std::ceil(
|
||||
static_cast<double>(ChunkZ+1)*CoreVoxelsPerOwnerChunk));
|
||||
if(bNativeOnly&&(!NativeOnlyContainsColumn(X,Y)
|
||||
||chunkMaximum<NativeOnlyMinimum.Z||chunkMinimum>NativeOnlyMaximum.Z
|
||||
||Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||
!=ECaveGeneratorType::SurfaceWorld))
|
||||
{RecordNativeOnlyViolation();return false;}
|
||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||
!=ECaveGeneratorType::SurfaceWorld)return false;
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
||||
static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
|
||||
const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
|
||||
if(!Plan)return false;
|
||||
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
@@ -1218,7 +1382,8 @@ public:
|
||||
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*Plan);
|
||||
if(!Block.isValid())return false;
|
||||
const auto Column=Block.point(ToFixed(X),ToFixed(Y),
|
||||
static_cast<int64>(ChunkZ)*CHUNK_SIZE*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
static_cast<int64>(std::llround(static_cast<double>(ChunkZ)
|
||||
*CoreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)));
|
||||
OutGroundZ=static_cast<float>(Column.groundHeightSubvoxels/Scale);
|
||||
OutCeilingZ=static_cast<float>(Column.capHeightSubvoxels/Scale);
|
||||
return std::isfinite(OutGroundZ)&&std::isfinite(OutCeilingZ);
|
||||
@@ -1227,8 +1392,50 @@ public:
|
||||
EVoxelTileClass ClassifyBox(const FIntVector& Origin,int32 Step,int32 Cells) const
|
||||
{ return ClassifyBoxDetailed(Origin,Step,Cells).Verdict; }
|
||||
|
||||
FBoxClassification ClassifyBoxDetailed(const FIntVector& Origin,int32 Step,int32 Cells) const
|
||||
bool IsNativeSurfaceBox(const FIntVector& Origin,int32 Step,int32 Cells) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->IsNativeSurfaceBox(Origin,Step,Cells);
|
||||
if(!WorldAssembly||!Owner||!Owner->Manager||Step<=0||Cells<=0)return false;
|
||||
const int32 Pad=std::max(Step,1);
|
||||
const int32 BMin=Origin.Z-Pad;
|
||||
const int32 BMax=Origin.Z+(Cells+1)*Step;
|
||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
||||
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin)
|
||||
/static_cast<float>(CoreVoxelsPerOwnerChunk));
|
||||
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax)
|
||||
/static_cast<float>(CoreVoxelsPerOwnerChunk));
|
||||
const VoxelForgeCore::WorldAssemblyStratumPlanV1* Plan=nullptr;
|
||||
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
|
||||
{
|
||||
const auto It=std::find_if(WorldAssembly->plan.strates.begin(),
|
||||
WorldAssembly->plan.strates.end(),[ChunkZ](const auto& Entry)
|
||||
{return ChunkZ<=Entry.topChunkZ&&ChunkZ>=Entry.bottomChunkZ;});
|
||||
if(It==WorldAssembly->plan.strates.end())return false;
|
||||
const auto* Candidate=&*It;
|
||||
if(Plan&&Plan!=Candidate)return false;
|
||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||
!=ECaveGeneratorType::SurfaceWorld)return false;
|
||||
Plan=Candidate;
|
||||
}
|
||||
if(bNativeOnly)
|
||||
{
|
||||
const int32 BMinX=Origin.X-Pad,BMinY=Origin.Y-Pad;
|
||||
const int32 BMaxX=Origin.X+(Cells+1)*Step;
|
||||
const int32 BMaxY=Origin.Y+(Cells+1)*Step;
|
||||
if(BMinX<NativeOnlyMinimum.X||BMinY<NativeOnlyMinimum.Y
|
||||
||BMin<NativeOnlyMinimum.Z||BMaxX>NativeOnlyMaximum.X
|
||||
||BMaxY>NativeOnlyMaximum.Y||BMax>NativeOnlyMaximum.Z)
|
||||
{RecordNativeOnlyViolation();return false;}
|
||||
}
|
||||
if(bNativeOnly&&!Plan)RecordNativeOnlyViolation();
|
||||
return Plan!=nullptr;
|
||||
}
|
||||
|
||||
FBoxClassification ClassifyBoxDetailed(const FIntVector& Origin,int32 Step,int32 Cells,
|
||||
bool bUseNativeLatticeProof=false) const
|
||||
{
|
||||
if(SharedDensitySource)
|
||||
return SharedDensitySource->ClassifyBoxDetailed(Origin,Step,Cells,bUseNativeLatticeProof);
|
||||
using Clock=std::chrono::steady_clock;
|
||||
const auto Start=Clock::now();
|
||||
const int32 Pad=std::max(Step,1);
|
||||
@@ -1240,12 +1447,13 @@ public:
|
||||
bool bIslandOtherStrateOverlap=false,bBadPlan=false;
|
||||
if(Owner&&Owner->Manager)
|
||||
{
|
||||
// Keep the delivered SurfaceWorld chunk mapping unchanged.
|
||||
// Conserver le mapping de chunks SurfaceWorld deja livre.
|
||||
// All manifest strata use the same resolved owner-chunk to Core-voxel scale.
|
||||
// Toutes les strates du manifeste partagent l'echelle owner/Core resolue.
|
||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
||||
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin[2])
|
||||
/static_cast<float>(CHUNK_SIZE));
|
||||
/static_cast<float>(CoreVoxelsPerOwnerChunk));
|
||||
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax[2])
|
||||
/static_cast<float>(CHUNK_SIZE));
|
||||
/static_cast<float>(CoreVoxelsPerOwnerChunk));
|
||||
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
|
||||
{
|
||||
const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk(
|
||||
@@ -1254,7 +1462,7 @@ public:
|
||||
{
|
||||
bSurfaceOverlap=true;
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
||||
static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
|
||||
const auto* Candidate=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
|
||||
if(!Candidate||(SurfacePlan&&SurfacePlan!=Candidate))
|
||||
bBadPlan=true;
|
||||
@@ -1279,7 +1487,7 @@ public:
|
||||
{
|
||||
bIslandOverlap=true;
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
||||
static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
|
||||
const auto* Candidate=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ);
|
||||
if(!Candidate||(IslandPlan&&IslandPlan!=Candidate))bBadPlan=true;
|
||||
IslandPlan=Candidate;
|
||||
@@ -1300,6 +1508,19 @@ public:
|
||||
else ++Counts.MixedBoxes;
|
||||
return Result;
|
||||
};
|
||||
if(bNativeOnly)
|
||||
{
|
||||
const bool bInsideProofBox=BMin[0]>=NativeOnlyMinimum.X
|
||||
&&BMin[1]>=NativeOnlyMinimum.Y&&BMin[2]>=NativeOnlyMinimum.Z
|
||||
&&BMax[0]<=NativeOnlyMaximum.X&&BMax[1]<=NativeOnlyMaximum.Y
|
||||
&&BMax[2]<=NativeOnlyMaximum.Z;
|
||||
if(!bInsideProofBox||!bSurfaceOverlap||bIslandOverlap||bOtherStrateOverlap
|
||||
||bBadPlan||!EditBrushes.empty())
|
||||
{
|
||||
RecordNativeOnlyViolation();
|
||||
return Finish(EVoxelTileClass::Mixed,true);
|
||||
}
|
||||
}
|
||||
// A box crossing recipes cannot use one field interval; unknown composition stays Mixed.
|
||||
// Une boite qui traverse plusieurs recettes reste Mixed, car l'intervalle d'un seul champ ne suffit pas.
|
||||
if(bBadPlan||(bSurfaceOverlap&&(bIslandOverlap||bOtherStrateOverlap))
|
||||
@@ -1310,6 +1531,12 @@ public:
|
||||
&&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1]
|
||||
&&EditBoundsMin.Z<=BMax[2]&&EditBoundsMax.Z>=BMin[2])
|
||||
return Finish(EVoxelTileClass::Mixed,true);
|
||||
// The native interval path below is the exact field selected first by DensityAtLattice
|
||||
// for SurfaceWorld and FloatingIslands. Legacy analytic primitives/layer bounds cannot
|
||||
// modify that field; only the explicit edit transaction above can. Keep their guards on
|
||||
// the legacy classifier path, where those overlays do participate in density.
|
||||
if(!bNativeOnly&&(!bUseNativeLatticeProof||(!bSurfaceOverlap&&!bIslandOverlap)))
|
||||
{
|
||||
for(int32 BZ=FloorDiv(BMin[2],ActiveSpatialBinSize);BZ<=FloorDiv(BMax[2],ActiveSpatialBinSize);++BZ)
|
||||
for(int32 BY=FloorDiv(BMin[1],ActiveSpatialBinSize);BY<=FloorDiv(BMax[1],ActiveSpatialBinSize);++BY)
|
||||
for(int32 BX=FloorDiv(BMin[0],ActiveSpatialBinSize);BX<=FloorDiv(BMax[0],ActiveSpatialBinSize);++BX)
|
||||
@@ -1327,10 +1554,14 @@ public:
|
||||
if(R.Lo<=BMax[2]&&R.Hi>=BMin[2]
|
||||
&&(R.Lo>=BMin[2]||R.Hi<=BMax[2]))
|
||||
return Finish(EVoxelTileClass::Mixed);
|
||||
}
|
||||
if(bSurfaceOverlap)
|
||||
{
|
||||
if(!WorldAssembly||!SurfacePlan)return Finish(EVoxelTileClass::Mixed,true);
|
||||
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*SurfacePlan);
|
||||
// classifyLatticeBox takes the exact extrema of ground/cap columns at every
|
||||
// MC lattice XY in this box. Since density is min(z-ground, cap-z), those extrema
|
||||
// bound every lattice sample in Z without a noise amplitude guess.
|
||||
const auto Verdict=Block.classifyLatticeBox(
|
||||
static_cast<int64>(BMin[0])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
||||
static_cast<int64>(BMin[1])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
||||
@@ -1354,19 +1585,6 @@ public:
|
||||
{
|
||||
if(!IslandPlan)return Finish(EVoxelTileClass::Mixed,true);
|
||||
constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t Bottom=IslandPlan->manifest.recipe.layoutMinimumZSubvoxels;
|
||||
const std::int64_t Top=IslandPlan->manifest.recipe.layoutMaximumZSubvoxels;
|
||||
const std::int64_t Seal=IslandPlan->manifest.recipe.boundarySealSubvoxels;
|
||||
const std::int64_t BoxBottom=static_cast<std::int64_t>(BMin[2])*Scale;
|
||||
const std::int64_t BoxTop=static_cast<std::int64_t>(BMax[2])*Scale;
|
||||
const bool bBottomSealOverlap=BoxBottom<Bottom+Seal&&BoxTop>=Bottom;
|
||||
const bool bTopSealOverlap=BoxBottom<=Top&&BoxTop>Top-Seal;
|
||||
if(bBottomSealOverlap)
|
||||
return Finish(BoxBottom>=Bottom&&BoxTop<=Bottom+Seal
|
||||
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
||||
if(bTopSealOverlap)
|
||||
return Finish(BoxBottom>=Top-Seal&&BoxTop<=Top
|
||||
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
||||
const VoxelForgeCore::FloatingIslandFieldV1 Block(
|
||||
IslandPlan->manifest.recipe,IslandPlan->manifest.plan);
|
||||
if(!Block.isValid())return Finish(EVoxelTileClass::Mixed,true);
|
||||
@@ -1386,6 +1604,7 @@ public:
|
||||
|
||||
bool IsRock(int32 X,int32 Y,int32 Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->IsRock(X,Y,Z);
|
||||
if(!Contains(X,Y,Z))return EvaluateFast(X,Y,Z);
|
||||
const size_t Line=static_cast<size_t>(Y-Min[1])*Size[0]
|
||||
+static_cast<size_t>(X-Min[0]);
|
||||
@@ -1843,6 +2062,21 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
bool NativeOnlyContainsColumn(float X,float Y) const
|
||||
{
|
||||
return std::isfinite(X)&&std::isfinite(Y)
|
||||
&&X>=NativeOnlyMinimum.X&&X<=NativeOnlyMaximum.X
|
||||
&&Y>=NativeOnlyMinimum.Y&&Y<=NativeOnlyMaximum.Y;
|
||||
}
|
||||
|
||||
bool NativeOnlyContains(float X,float Y,float Z) const
|
||||
{
|
||||
return std::isfinite(X)&&std::isfinite(Y)&&std::isfinite(Z)
|
||||
&&X>=NativeOnlyMinimum.X&&X<=NativeOnlyMaximum.X
|
||||
&&Y>=NativeOnlyMinimum.Y&&Y<=NativeOnlyMaximum.Y
|
||||
&&Z>=NativeOnlyMinimum.Z&&Z<=NativeOnlyMaximum.Z;
|
||||
}
|
||||
|
||||
static FClassificationThreadStats& ThreadClassificationStats()
|
||||
{
|
||||
static thread_local FClassificationThreadStats Stats;
|
||||
@@ -1871,13 +2105,14 @@ private:
|
||||
|
||||
bool NativeSurfaceDensityAt(float X,float Y,float Z,float& OutDensity) const
|
||||
{
|
||||
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
||||
if(!bUseNativeManifestFields||!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
||||
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return false;
|
||||
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CHUNK_SIZE));
|
||||
const int32 ChunkZ=FMath::FloorToInt(Z
|
||||
/static_cast<float>(GetCoreVoxelsPerOwnerChunk()));
|
||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||
!=ECaveGeneratorType::SurfaceWorld)return false;
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
||||
static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
|
||||
const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
|
||||
if(!Plan)return false;
|
||||
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
@@ -1896,8 +2131,9 @@ private:
|
||||
|
||||
bool NativeFloatingIslandDensityAt(float X,float Y,float Z,float& OutDensity) const
|
||||
{
|
||||
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
||||
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return false;
|
||||
if(!bUseNativeManifestFields||!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
||||
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))
|
||||
return false;
|
||||
// Convert Core query Z back to the owner's legacy chunk coordinate.
|
||||
// Convertir Z du Core vers la coordonnee de chunk legacy du proprietaire.
|
||||
const double CoreVoxelsPerLegacyChunk=GetCoreVoxelsPerOwnerChunk();
|
||||
@@ -1905,7 +2141,7 @@ private:
|
||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||
!=ECaveGeneratorType::FloatingIslands)return false;
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
||||
static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
|
||||
const auto* Plan=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ);
|
||||
if(!Plan)return false;
|
||||
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
@@ -1918,23 +2154,79 @@ private:
|
||||
const VoxelForgeCore::FloatingIslandFieldV1 Field(
|
||||
Plan->manifest.recipe,Plan->manifest.plan);
|
||||
if(!Field.isValid())return false;
|
||||
std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
||||
const std::int64_t Bottom=Plan->manifest.recipe.layoutMinimumZSubvoxels;
|
||||
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<Bottom+Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Zq-Bottom));
|
||||
if(Zq<=Top&&Zq>Top-Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Top-Zq));
|
||||
// The recipe's boundary seal is a feature margin, not a horizontal solid cap.
|
||||
// La marge de scellement du plan borne ses formes, sans remplir tout le plan XY.
|
||||
const std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
||||
OutDensity=static_cast<float>(static_cast<double>(Density)/Scale);
|
||||
return std::isfinite(OutDensity);
|
||||
}
|
||||
|
||||
float ApplyAdjacentStratumBoundary(float X,float Y,float Z,float Density,
|
||||
bool bFloatingIsland) const
|
||||
{
|
||||
if(!bUseNativeManifestFields||!WorldAssembly||!Owner||!Owner->Manager
|
||||
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return Density;
|
||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
||||
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CoreVoxelsPerOwnerChunk));
|
||||
const ECaveGeneratorType CurrentType=Owner->Manager->GetGeneratorTypeForChunk(
|
||||
FIntVector(0,0,ChunkZ));
|
||||
if(bFloatingIsland&&CurrentType==ECaveGeneratorType::FloatingIslands)
|
||||
{
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
|
||||
const auto* Plan=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ);
|
||||
if(!Plan)return Density;
|
||||
const int32 LowerChunk=Plan->bottomChunkZ-1;
|
||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,LowerChunk))
|
||||
!=ECaveGeneratorType::SurfaceWorld)return Density;
|
||||
const double Boundary=static_cast<double>(Plan->bottomChunkZ)
|
||||
*CoreVoxelsPerOwnerChunk;
|
||||
float LowerDensity=0.0f,UpperDensity=0.0f;
|
||||
if(!NativeSurfaceDensityAt(X,Y,static_cast<float>(Boundary-0.5),LowerDensity)
|
||||
||!NativeFloatingIslandDensityAt(X,Y,static_cast<float>(Boundary+0.5),UpperDensity)
|
||||
||(LowerDensity<0.0f)==(UpperDensity<0.0f))return Density;
|
||||
|
||||
// Clip the two signed-distance fields to their shared owner-stratum plane.
|
||||
// This keeps a solid-below/air-above seam continuous at every mesh sample step.
|
||||
// Les deux champs sont limites au plan commun, quel que soit le pas de maillage.
|
||||
const float Plane=static_cast<float>(static_cast<double>(Z)-Boundary);
|
||||
return LowerDensity<0.0f?std::min(Density,Plane)
|
||||
:std::max(Density,-Plane);
|
||||
}
|
||||
if(!bFloatingIsland&&CurrentType==ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
|
||||
const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
|
||||
if(!Plan)return Density;
|
||||
const int32 UpperChunk=Plan->topChunkZ+1;
|
||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,UpperChunk))
|
||||
!=ECaveGeneratorType::FloatingIslands)return Density;
|
||||
const auto* UpperPlan=FindWorldAssemblyFloatingIslandStratum(
|
||||
Owner->Manager->GetStrateIndex(static_cast<float>((
|
||||
static_cast<double>(UpperChunk)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE)),UpperChunk);
|
||||
if(!UpperPlan)return Density;
|
||||
const double Boundary=static_cast<double>(UpperPlan->bottomChunkZ)
|
||||
*CoreVoxelsPerOwnerChunk;
|
||||
float LowerDensity=0.0f,UpperDensity=0.0f;
|
||||
if(!NativeSurfaceDensityAt(X,Y,static_cast<float>(Boundary-0.5),LowerDensity)
|
||||
||!NativeFloatingIslandDensityAt(X,Y,static_cast<float>(Boundary+0.5),UpperDensity)
|
||||
||(LowerDensity<0.0f)==(UpperDensity<0.0f))return Density;
|
||||
|
||||
const float Plane=static_cast<float>(static_cast<double>(Z)-Boundary);
|
||||
return LowerDensity<0.0f?std::max(Density,Plane)
|
||||
:std::min(Density,-Plane);
|
||||
}
|
||||
return Density;
|
||||
}
|
||||
|
||||
bool UsesLegacySurfaceField(float X,float Y,float Z) const
|
||||
{
|
||||
if(!Owner||!Owner->Manager||!Owner->Generator)return false;
|
||||
const FIntVector ChunkCoord(FMath::FloorToInt(X/static_cast<float>(CHUNK_SIZE)),
|
||||
FMath::FloorToInt(Y/static_cast<float>(CHUNK_SIZE)),
|
||||
FMath::FloorToInt(Z/static_cast<float>(CHUNK_SIZE)));
|
||||
const float CoreVoxelsPerOwnerChunk=static_cast<float>(GetCoreVoxelsPerOwnerChunk());
|
||||
const FIntVector ChunkCoord(FMath::FloorToInt(X/CoreVoxelsPerOwnerChunk),
|
||||
FMath::FloorToInt(Y/CoreVoxelsPerOwnerChunk),
|
||||
FMath::FloorToInt(Z/CoreVoxelsPerOwnerChunk));
|
||||
return Owner->Manager->GetGeneratorTypeForChunk(ChunkCoord)
|
||||
==ECaveGeneratorType::SurfaceWorld;
|
||||
}
|
||||
@@ -2144,19 +2436,26 @@ private:
|
||||
int32 Top=MIN_int32,Bottom=MAX_int32;
|
||||
for(const FStrateSlot& Slot:Layout)
|
||||
{
|
||||
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
|
||||
// A manifest-backed FloatingIslands stratum supplies its own sparse island
|
||||
// field; it must not extend the legacy solid envelope across the open sky.
|
||||
if(bUseNativeManifestFields
|
||||
&&Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,MidChunkZ))
|
||||
==ECaveGeneratorType::FloatingIslands)
|
||||
continue;
|
||||
Top=std::max(Top,Slot.TopChunkZ);Bottom=std::min(Bottom,Slot.BottomChunkZ);
|
||||
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=OwnerChunkLowerBoundaryCoreVoxel(Slot.BottomChunkZ);
|
||||
Field.Hi=OwnerChunkUpperBoundaryCoreVoxel(Slot.TopChunkZ+1);
|
||||
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({OwnerChunkLowerBoundaryCoreVoxel(Bottom),
|
||||
OwnerChunkUpperBoundaryCoreVoxel(Top+1)});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3267,8 +3566,9 @@ private:
|
||||
auto Unit=[&](uint32 H){return static_cast<double>(Hash(H)>>8)*(1.0/16777216.0);};
|
||||
for(const FStrateSlot& Slot:Layout)
|
||||
{
|
||||
const double ZBottom=static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE;
|
||||
const double ZTop=static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE;
|
||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
||||
const double ZBottom=static_cast<double>(Slot.BottomChunkZ)*CoreVoxelsPerOwnerChunk;
|
||||
const double ZTop=static_cast<double>(Slot.TopChunkZ+1)*CoreVoxelsPerOwnerChunk;
|
||||
for(int32 GY=GY0;GY<=GY1;++GY)for(int32 GX=GX0;GX<=GX1;++GX)
|
||||
{
|
||||
uint32 H=Hash(Seed^static_cast<uint32>(GX)*0x9E3779B9u
|
||||
@@ -4637,7 +4937,18 @@ public:
|
||||
void DensityAtLatticeZRun(int32 X,int32 Y,int32 ZStart,int32 ZStep,
|
||||
int32 Count,float* OutDensity) const
|
||||
{
|
||||
if(SharedDensitySource)
|
||||
{
|
||||
SharedDensitySource->DensityAtLatticeZRun(X,Y,ZStart,ZStep,Count,OutDensity);
|
||||
return;
|
||||
}
|
||||
if(Count<=0||OutDensity==nullptr)return;
|
||||
if(bNativeOnly)
|
||||
{
|
||||
for(int32 Lane=0;Lane<Count;++Lane)
|
||||
OutDensity[Lane]=DensityAtLattice(X,Y,ZStart+Lane*ZStep);
|
||||
return;
|
||||
}
|
||||
bool bHasLegacySurfaceSample=false;
|
||||
for(int32 Lane=0;Lane<Count;++Lane)
|
||||
bHasLegacySurfaceSample|=UsesLegacySurfaceField(static_cast<float>(X),
|
||||
@@ -5161,6 +5472,7 @@ private:
|
||||
|
||||
FStandaloneOwnerWorld* Owner=nullptr;
|
||||
const VoxelForgeCore::WorldAssemblyManifest* WorldAssembly=nullptr;
|
||||
const FStandaloneShapeWorld* SharedDensitySource=nullptr;
|
||||
int32 WorkerCount=6;
|
||||
const VoxelForgeCore::JobScheduler* Scheduler=nullptr;
|
||||
bool RoundTunnelsEnabled=false;
|
||||
@@ -5174,6 +5486,11 @@ private:
|
||||
size_t WordsPerZ=0;
|
||||
EDetailVariant DetailVariant=EDetailVariant::Coherent;
|
||||
bool Built=false;
|
||||
bool bNativeOnly=false;
|
||||
bool bUseNativeManifestFields=false;
|
||||
FIntVector NativeOnlyMinimum=FIntVector::ZeroValue;
|
||||
FIntVector NativeOnlyMaximum=FIntVector::ZeroValue;
|
||||
mutable std::atomic<uint32> NativeOnlyViolations{0};
|
||||
bool FarBuilt=false;
|
||||
std::vector<FInterval> LayerRanges;
|
||||
std::vector<FLayerField> LayerFields;
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <set>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace vfcore::detail
|
||||
{
|
||||
@@ -943,6 +945,11 @@ namespace VoxelForgeCore
|
||||
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
|
||||
WorldConfiguration configuration;
|
||||
std::unique_ptr<FStandaloneShapeWorld> shape;
|
||||
std::atomic<bool> shapeReady{false};
|
||||
std::unique_ptr<world_assembly_detail::FSharedHeightfieldColumnCache>
|
||||
sharedHeightfieldColumns;
|
||||
std::unique_ptr<floating_island_detail::FSharedIslandColumnCache>
|
||||
sharedIslandColumns;
|
||||
ShapeBuildRequest buildRequest;
|
||||
mutable std::shared_mutex tileRegionMutex;
|
||||
std::unordered_map<TileKey,std::shared_ptr<FStandaloneShapeWorld>,TileKeyHash> tileRegions;
|
||||
@@ -953,6 +960,23 @@ namespace VoxelForgeCore
|
||||
std::uint64_t lastSequence=0;
|
||||
};
|
||||
|
||||
struct FScopedSharedColumnCaches
|
||||
{
|
||||
using FHeightfieldCache=world_assembly_detail::FSharedHeightfieldColumnCache;
|
||||
using FIslandCache=floating_island_detail::FSharedIslandColumnCache;
|
||||
FHeightfieldCache* PreviousHeightfield=nullptr;
|
||||
FIslandCache* PreviousIslands=nullptr;
|
||||
|
||||
FScopedSharedColumnCaches(FHeightfieldCache* Heightfield,FIslandCache* Islands)
|
||||
: PreviousHeightfield(FHeightfieldCache::setThreadActive(Heightfield)),
|
||||
PreviousIslands(FIslandCache::setThreadActive(Islands)) {}
|
||||
~FScopedSharedColumnCaches()
|
||||
{
|
||||
FHeightfieldCache::setThreadActive(PreviousHeightfield);
|
||||
FIslandCache::setThreadActive(PreviousIslands);
|
||||
}
|
||||
};
|
||||
|
||||
World::World(ResolvedWorldSnapshot snapshot):impl_(std::make_unique<Impl>())
|
||||
{
|
||||
if(snapshot.data_)
|
||||
@@ -960,15 +984,151 @@ namespace VoxelForgeCore
|
||||
impl_->owner=snapshot.data_->owner;
|
||||
impl_->worldAssembly=snapshot.data_->worldAssembly;
|
||||
impl_->configuration=snapshot.data_->configuration;
|
||||
if(impl_->worldAssembly)
|
||||
{
|
||||
if(!impl_->worldAssembly->plan.strates.empty())
|
||||
impl_->sharedHeightfieldColumns=
|
||||
std::make_unique<world_assembly_detail::FSharedHeightfieldColumnCache>();
|
||||
if(!impl_->worldAssembly->floatingIslands.empty())
|
||||
impl_->sharedIslandColumns=
|
||||
std::make_unique<floating_island_detail::FSharedIslandColumnCache>();
|
||||
}
|
||||
}
|
||||
}
|
||||
World::~World()=default;
|
||||
World::World(World&&) noexcept=default;
|
||||
World& World::operator=(World&&) noexcept=default;
|
||||
|
||||
namespace
|
||||
{
|
||||
template<class TImpl>
|
||||
bool NativeOnlyBoundsForTile(const TImpl& impl,const TileKey& key,
|
||||
FIntVector& minimum,FIntVector& maximum)
|
||||
{
|
||||
if(!impl.owner||key.lod!=0||key.lod>=impl.configuration.fullResolutionLodCount)
|
||||
return false;
|
||||
constexpr std::int64_t TileExtent=CHUNK_SIZE;
|
||||
const std::int64_t skirt=impl.owner->CoreVoxelSizeMicrons>0
|
||||
?static_cast<std::int64_t>(std::ceil(std::abs(
|
||||
static_cast<double>(impl.owner->SkirtCells)
|
||||
*static_cast<double>(impl.owner->OwnerVoxelSizeMicrons)
|
||||
/static_cast<double>(impl.owner->CoreVoxelSizeMicrons)))):0;
|
||||
const std::int64_t pad=std::max<std::int64_t>(8,skirt+4);
|
||||
const std::int64_t coordinates[3]={key.coordinate.x,key.coordinate.y,key.coordinate.z};
|
||||
std::int64_t low[3]{},high[3]{};
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
const std::int64_t origin=coordinates[axis]*TileExtent;
|
||||
low[axis]=origin-pad;
|
||||
high[axis]=origin+TileExtent+pad;
|
||||
if(low[axis]<std::numeric_limits<std::int32_t>::min()+4ll
|
||||
||high[axis]>std::numeric_limits<std::int32_t>::max()-4ll)return false;
|
||||
}
|
||||
minimum=FIntVector(static_cast<int32>(low[0]),static_cast<int32>(low[1]),
|
||||
static_cast<int32>(low[2]));
|
||||
maximum=FIntVector(static_cast<int32>(high[0]),static_cast<int32>(high[1]),
|
||||
static_cast<int32>(high[2]));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IntersectsBox(const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum,
|
||||
const std::int64_t tileMinimum[3],const std::int64_t tileMaximum[3])
|
||||
{
|
||||
for(int axis=0;axis<3;++axis)
|
||||
if(maximum[axis]<tileMinimum[axis]||minimum[axis]>tileMaximum[axis])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RouteTouchesBox(const WorldAssemblyStratumPlanV1& plan,
|
||||
const std::int64_t tileMinimum[3],const std::int64_t tileMaximum[3])
|
||||
{
|
||||
if(plan.route.empty())return false;
|
||||
const std::int64_t constraintRadius=std::max<std::int64_t>(0,
|
||||
plan.routeConstraints.capsuleRadiusSubvoxels)
|
||||
+std::max<std::int64_t>(0,plan.routeConstraints.capsuleHeightSubvoxels)/2;
|
||||
for(std::size_t index=0;index<plan.route.size();++index)
|
||||
{
|
||||
const auto& point=plan.route[index];
|
||||
const std::int64_t radius=std::max(constraintRadius,
|
||||
std::max<std::int64_t>(0,point.radiusSubvoxels));
|
||||
std::array<std::int64_t,3> low{},high{};
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
low[axis]=point.positionSubvoxels[axis]-radius;
|
||||
high[axis]=point.positionSubvoxels[axis]+radius;
|
||||
}
|
||||
if(IntersectsBox(low,high,tileMinimum,tileMaximum))return true;
|
||||
if(index+1>=plan.route.size())continue;
|
||||
const auto& next=plan.route[index+1];
|
||||
const std::int64_t segmentRadius=std::max(radius,
|
||||
std::max(constraintRadius,std::max<std::int64_t>(0,next.radiusSubvoxels)));
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
low[axis]=std::min(point.positionSubvoxels[axis],next.positionSubvoxels[axis])
|
||||
-segmentRadius;
|
||||
high[axis]=std::max(point.positionSubvoxels[axis],next.positionSubvoxels[axis])
|
||||
+segmentRadius;
|
||||
}
|
||||
if(IntersectsBox(low,high,tileMinimum,tileMaximum))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OwnerPassageTouchesBox(const FStandaloneOwnerWorld& owner,
|
||||
const FIntVector& minimum,const FIntVector& maximum)
|
||||
{
|
||||
const double ownerToCore=owner.OwnerVoxelSizeMicrons>0
|
||||
&&owner.CoreVoxelSizeMicrons>0
|
||||
?static_cast<double>(owner.OwnerVoxelSizeMicrons)
|
||||
/static_cast<double>(owner.CoreVoxelSizeMicrons):1.0;
|
||||
const double padding=(std::max(8.0,static_cast<double>(owner.SkirtCells)+4.0)
|
||||
/ownerToCore)+64.0;
|
||||
const double tileMinimum[3]={static_cast<double>(minimum.X),
|
||||
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
|
||||
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
||||
const auto SegmentTouches=[&](const FVector& a,const FVector& b,double radius)
|
||||
{
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
const double pointA=(&a.X)[axis]*ownerToCore;
|
||||
const double pointB=(&b.X)[axis]*ownerToCore;
|
||||
const double expansion=(radius+padding)*ownerToCore;
|
||||
if(std::max(pointA,pointB)+expansion<tileMinimum[axis]
|
||||
||std::min(pointA,pointB)-expansion>tileMaximum[axis])return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
for(const FVoxelPassage& passage:owner.Manager->GetPassages())
|
||||
{
|
||||
const auto& points=passage.ControlPoints;
|
||||
if(points.Num()<2)
|
||||
{
|
||||
if(SegmentTouches(passage.UpperPoint,passage.LowerPoint,passage.Radius))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
for(int32 index=0;index+1<points.Num();++index)
|
||||
{
|
||||
const double radius=std::max({static_cast<double>(passage.Radius),
|
||||
passage.ControlRadii.IsValidIndex(index)
|
||||
?static_cast<double>(passage.ControlRadii[index]):0.0,
|
||||
passage.ControlRadii.IsValidIndex(index+1)
|
||||
?static_cast<double>(passage.ControlRadii[index+1]):0.0});
|
||||
if(SegmentTouches(points[index],points[index+1],radius))return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool World::compile(const ShapeBuildRequest& request,const JobScheduler& scheduler,std::string& error)
|
||||
{
|
||||
if(!impl_->owner){error="resolved world snapshot is empty";return false;}
|
||||
if(impl_->shapeReady.load(std::memory_order_acquire))
|
||||
{error="world shape is already compiled";return false;}
|
||||
if(!::vfcore::fp_contract::selfTest())
|
||||
{
|
||||
error="Core startup refused: floating-point contraction self-test detected a fused multiply-add; this compiler/flag combination is unsupported for world generation";
|
||||
@@ -988,13 +1148,76 @@ namespace VoxelForgeCore
|
||||
std::unique_lock lock(impl_->tileRegionMutex);
|
||||
impl_->tileRegions.clear();
|
||||
impl_->tileRegionCount.store(0,std::memory_order_release);
|
||||
impl_->shapeReady.store(true,std::memory_order_release);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool World::isTileShapeFree(const TileKey& key) const
|
||||
{
|
||||
if(!impl_||!impl_->owner||!impl_->worldAssembly||!impl_->owner->Manager
|
||||
||!impl_->owner->Generator||!impl_->editLog.empty())return false;
|
||||
FIntVector boundsMin,boundsMax;
|
||||
if(!NativeOnlyBoundsForTile(*impl_,key,boundsMin,boundsMax))return false;
|
||||
|
||||
const std::int64_t tileMinimum[3]={boundsMin.X,boundsMin.Y,boundsMin.Z};
|
||||
const std::int64_t tileMaximum[3]={boundsMax.X,boundsMax.Y,boundsMax.Z};
|
||||
const double coreVoxelsPerOwnerChunk=static_cast<double>(CHUNK_SIZE)
|
||||
*static_cast<double>(impl_->owner->OwnerVoxelSizeMicrons)
|
||||
/static_cast<double>(std::max(1u,impl_->owner->CoreVoxelSizeMicrons));
|
||||
const int32 firstSurfaceChunk=FMath::FloorToInt(
|
||||
static_cast<double>(boundsMin.Z)/coreVoxelsPerOwnerChunk);
|
||||
const int32 lastSurfaceChunk=FMath::FloorToInt(
|
||||
static_cast<double>(boundsMax.Z)/coreVoxelsPerOwnerChunk);
|
||||
const WorldAssemblyStratumPlanV1* selectedPlan=nullptr;
|
||||
for(int32 chunkZ=firstSurfaceChunk;chunkZ<=lastSurfaceChunk;++chunkZ)
|
||||
{
|
||||
if(impl_->owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ))
|
||||
!=ECaveGeneratorType::SurfaceWorld)return false;
|
||||
const int32 strateIndex=impl_->owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(chunkZ)*CHUNK_SIZE
|
||||
+CHUNK_SIZE*0.5)*VOXEL_SIZE));
|
||||
const auto found=std::find_if(impl_->worldAssembly->plan.strates.begin(),
|
||||
impl_->worldAssembly->plan.strates.end(),[strateIndex,chunkZ](const auto& plan)
|
||||
{return plan.strateIndex==strateIndex&&chunkZ<=plan.topChunkZ
|
||||
&&chunkZ>=plan.bottomChunkZ;});
|
||||
if(found==impl_->worldAssembly->plan.strates.end()
|
||||
||(selectedPlan&&selectedPlan!=&*found))return false;
|
||||
selectedPlan=&*found;
|
||||
}
|
||||
if(!selectedPlan||!Heightfield2D(impl_->worldAssembly->recipe,*selectedPlan).isValid())
|
||||
return false;
|
||||
|
||||
const int32 firstIslandChunk=FMath::FloorToInt(
|
||||
static_cast<double>(boundsMin.Z)/coreVoxelsPerOwnerChunk);
|
||||
const int32 lastIslandChunk=FMath::FloorToInt(
|
||||
static_cast<double>(boundsMax.Z)/coreVoxelsPerOwnerChunk);
|
||||
for(int32 chunkZ=firstIslandChunk;chunkZ<=lastIslandChunk;++chunkZ)
|
||||
if(impl_->owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ))
|
||||
==ECaveGeneratorType::FloatingIslands)return false;
|
||||
|
||||
constexpr std::int64_t scale=WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t subMinimum[3]={tileMinimum[0]*scale,tileMinimum[1]*scale,
|
||||
tileMinimum[2]*scale};
|
||||
const std::int64_t subMaximum[3]={tileMaximum[0]*scale,tileMaximum[1]*scale,
|
||||
tileMaximum[2]*scale};
|
||||
for(const WorldAssemblyFeatureV1& feature:selectedPlan->features)
|
||||
if(IntersectsBox(feature.minimumSubvoxels,feature.maximumSubvoxels,
|
||||
subMinimum,subMaximum))return false;
|
||||
if(RouteTouchesBox(*selectedPlan,subMinimum,subMaximum))return false;
|
||||
if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false;
|
||||
FStandaloneShapeWorld proofGeometry;
|
||||
if(!proofGeometry.BuildShapeDependencyProof(*impl_->owner,boundsMin,boundsMax,
|
||||
impl_->owner->Generator->bStandaloneResearchRoundTunnels))return false;
|
||||
const FIntVector tileOrigin=ToNative(key).OriginVoxels();
|
||||
if(proofGeometry.HasShapeDependencyInBox(tileOrigin,1,CHUNK_SIZE))return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool World::needsTileRegion(const TileKey& key) const
|
||||
{
|
||||
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt()
|
||||
if(!impl_||!impl_->owner||!impl_->shapeReady.load(std::memory_order_acquire)
|
||||
||!impl_->shape||!impl_->shape->IsBuilt()
|
||||
||key.lod<0||key.lod>=impl_->configuration.fullResolutionLodCount)
|
||||
return false;
|
||||
if(CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))return false;
|
||||
@@ -1005,14 +1228,20 @@ namespace VoxelForgeCore
|
||||
bool World::compileTileRegion(std::span<const TileKey> tiles,std::uint32_t workerLimit,
|
||||
const JobScheduler& scheduler,std::string& error,
|
||||
std::size_t* outCompiledTiles,
|
||||
TileRegionCompileStats* outStats,bool analyticOnly)
|
||||
TileRegionCompileStats* outStats,bool analyticOnly,
|
||||
bool diagnosticIncludeBaseCoveredTiles)
|
||||
{
|
||||
if(outCompiledTiles)*outCompiledTiles=0;
|
||||
if(outStats)*outStats={};
|
||||
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt())
|
||||
try
|
||||
{
|
||||
if(!impl_||!impl_->owner||!impl_->shapeReady.load(std::memory_order_acquire)
|
||||
||!impl_->shape||!impl_->shape->IsBuilt())
|
||||
{error="the base world must be compiled before adding tile regions";return false;}
|
||||
if(tiles.empty())return true;
|
||||
|
||||
constexpr std::int64_t RegionPadding=10;
|
||||
|
||||
struct FPendingRegion
|
||||
{
|
||||
TileKey pageKey;
|
||||
@@ -1031,12 +1260,28 @@ namespace VoxelForgeCore
|
||||
const TileKey& key=tiles[inputIndex];
|
||||
if(key.lod<0||key.lod>8)
|
||||
{error="tile region contains an invalid LOD";return false;}
|
||||
if(key.lod>=impl_->configuration.fullResolutionLodCount
|
||||
||CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))continue;
|
||||
if(key.lod>=impl_->configuration.fullResolutionLodCount)continue;
|
||||
const std::int64_t extent=static_cast<std::int64_t>(CHUNK_SIZE)
|
||||
*(std::int64_t{1}<<key.lod);
|
||||
const std::int32_t coordinates[3]={key.coordinate.x,key.coordinate.y,
|
||||
key.coordinate.z};
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
const std::int64_t origin=static_cast<std::int64_t>(coordinates[axis])*extent;
|
||||
if(origin<std::numeric_limits<std::int32_t>::min()+RegionPadding
|
||||
||origin+extent>std::numeric_limits<std::int32_t>::max()-RegionPadding)
|
||||
{error="tile region coordinate is out of range";return false;}
|
||||
}
|
||||
if(!diagnosticIncludeBaseCoveredTiles
|
||||
&&CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))continue;
|
||||
if(impl_->tileRegions.contains(key)||!seenTiles.insert(key).second)continue;
|
||||
// Analytic regions evaluate only requested fine tiles and can safely coalesce a
|
||||
// whole entering LOD slab. Packed pages retain their fixed absolute parent cells.
|
||||
const TileKey pageKey=analyticOnly?TileKey{{0,0,0},key.lod}:ShapePageKey(key);
|
||||
// Keep every region's bounds spatially finite. A large camera step can bring in
|
||||
// disjoint tiles across the whole view; coalescing an entire LOD slab makes the
|
||||
// dense axis-bin grid grow with the bounding rectangle of all of them at once.
|
||||
// Garder chaque region spatialement bornee: un grand deplacement peut amener des
|
||||
// tuiles disjointes; fusionner tout un niveau ferait croitre les bins avec son
|
||||
// grand rectangle englobant.
|
||||
const TileKey pageKey=ShapePageKey(key);
|
||||
auto [found,inserted]=regionByPage.try_emplace(pageKey,pendingRegions.size());
|
||||
if(inserted)pendingRegions.push_back({pageKey,{}});
|
||||
pendingRegions[found->second].tiles.push_back(key);
|
||||
@@ -1076,7 +1321,8 @@ namespace VoxelForgeCore
|
||||
const FIntVector fineMax(regionMax.X+PagePad,regionMax.Y+PagePad,regionMax.Z+PagePad);
|
||||
auto region=std::make_shared<FStandaloneShapeWorld>();
|
||||
if(!region->BuildAnalyticRegion(*impl_->owner,fineMin,fineMax,regionMin,regionMax,
|
||||
ToNative(base.detail),base.roundTunnels,regionErrors[index]))return;
|
||||
ToNative(base.detail),base.roundTunnels,impl_->worldAssembly.get(),
|
||||
regionErrors[index]))return;
|
||||
compiledRegions[index]=std::move(region);
|
||||
};
|
||||
struct FAnalyticBuildContext
|
||||
@@ -1142,13 +1388,19 @@ namespace VoxelForgeCore
|
||||
compiledRegions[i]=std::move(region);
|
||||
}
|
||||
}
|
||||
for(const auto& region:compiledRegions)
|
||||
if(region)region->SetSharedDensitySource(impl_->shape.get());
|
||||
{
|
||||
std::unique_lock lock(impl_->tileRegionMutex);
|
||||
std::unordered_map<TileKey,std::shared_ptr<FStandaloneShapeWorld>,TileKeyHash> publish;
|
||||
publish.reserve(stats.tileKeysCovered);
|
||||
for(std::size_t i=0;i<pendingRegions.size();++i)
|
||||
{
|
||||
for(const TileKey& key:pendingRegions[i].tiles)
|
||||
if(impl_->tileRegions.try_emplace(key,compiledRegions[i]).second)++compiledTileCount;
|
||||
}
|
||||
publish.try_emplace(key,compiledRegions[i]);
|
||||
const std::size_t candidates=publish.size();
|
||||
std::unique_lock lock(impl_->tileRegionMutex);
|
||||
impl_->tileRegions.reserve(impl_->tileRegions.size()+candidates);
|
||||
impl_->tileRegions.merge(publish);
|
||||
compiledTileCount=candidates-publish.size();
|
||||
impl_->tileRegionCount.store(impl_->tileRegions.size(),std::memory_order_release);
|
||||
}
|
||||
stats.wallSeconds=std::chrono::duration<double>(Clock::now()-compileStart).count();
|
||||
@@ -1156,6 +1408,21 @@ namespace VoxelForgeCore
|
||||
if(outStats)*outStats=stats;
|
||||
return true;
|
||||
}
|
||||
catch(const std::bad_alloc&)
|
||||
{
|
||||
if(outCompiledTiles)*outCompiledTiles=0;
|
||||
if(outStats)*outStats={};
|
||||
error="region OOM";
|
||||
return false;
|
||||
}
|
||||
catch(const std::length_error&)
|
||||
{
|
||||
if(outCompiledTiles)*outCompiledTiles=0;
|
||||
if(outStats)*outStats={};
|
||||
error="region size";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void World::retainTileRegions(std::span<const TileKey> desiredTiles)
|
||||
{
|
||||
@@ -1171,14 +1438,17 @@ namespace VoxelForgeCore
|
||||
}
|
||||
|
||||
void World::buildFarWorld()
|
||||
{ if(impl_->shape)impl_->shape->BuildFarWorld(); }
|
||||
bool World::isCompiled() const noexcept { return impl_&&impl_->shape&&impl_->shape->IsBuilt(); }
|
||||
bool World::hasFarWorld() const noexcept { return impl_&&impl_->shape&&impl_->shape->HasFarWorld(); }
|
||||
{ if(impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape)impl_->shape->BuildFarWorld(); }
|
||||
bool World::isCompiled() const noexcept
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire); }
|
||||
bool World::hasFarWorld() const noexcept
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)
|
||||
&&impl_->shape&&impl_->shape->HasFarWorld(); }
|
||||
|
||||
ShapeBuildStats World::buildStats() const
|
||||
{
|
||||
ShapeBuildStats out;
|
||||
if(!impl_||!impl_->shape)return out;
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)return out;
|
||||
const auto& stats=impl_->shape->GetStats();
|
||||
out.shapeSeconds=stats.ShapeSeconds;out.intervalSeconds=stats.IntervalSeconds;
|
||||
out.surfaceSeconds=stats.SurfaceSeconds;out.farShapeSeconds=stats.FarShapeSeconds;
|
||||
@@ -1188,18 +1458,19 @@ namespace VoxelForgeCore
|
||||
}
|
||||
|
||||
float World::densityAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shape?impl_->shape->DensityAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
|
||||
float World::unclampedDensityAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shape?impl_->shape->DensityAtUnclamped(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAtUnclamped(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
|
||||
DetailFieldSample World::detailSampleAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shape?impl_->shape->DetailSampleAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):DetailFieldSample{}; }
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DetailSampleAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):DetailFieldSample{}; }
|
||||
float World::densityAtLattice(Vec3i position) const
|
||||
{ return impl_&&impl_->shape?impl_->shape->DensityAtLattice(position.x,position.y,position.z):0.0f; }
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAtLattice(position.x,position.y,position.z):0.0f; }
|
||||
bool World::isSolidAt(Vec3d position) const { return densityAt(position)<0.0f; }
|
||||
|
||||
bool World::isSolidAlongSegment(Vec3d start,Vec3d end,double spacing) const
|
||||
{
|
||||
if(!impl_||!impl_->shape||!std::isfinite(spacing)||spacing<=0.0)return false;
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape
|
||||
||!std::isfinite(spacing)||spacing<=0.0)return false;
|
||||
const double dx=end.x-start.x,dy=end.y-start.y,dz=end.z-start.z;
|
||||
const double length=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||
const std::size_t intervals=std::max<std::size_t>(1,static_cast<std::size_t>(std::ceil(length/spacing)));
|
||||
@@ -1213,7 +1484,7 @@ namespace VoxelForgeCore
|
||||
|
||||
TileClass World::classifyBox(Vec3i origin,std::int32_t step,std::int32_t cells) const
|
||||
{
|
||||
if(!impl_||!impl_->shape)return TileClass::mixed;
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)return TileClass::mixed;
|
||||
const EVoxelTileClass result=impl_->shape->ClassifyBox(ToNative(origin),step,cells);
|
||||
return result==EVoxelTileClass::AllSolid?TileClass::allSolid:
|
||||
result==EVoxelTileClass::AllAir?TileClass::allAir:TileClass::mixed;
|
||||
@@ -1335,17 +1606,38 @@ namespace VoxelForgeCore
|
||||
// Les tuiles proches n'avaient aucun conflit; garder leur acces simple et associer les LOD grossiers.
|
||||
Heightfield2D::setThreadColumnCacheLegacyDirectMapped(
|
||||
options.legacyDirectMappedColumnCache||key.lod<=2);
|
||||
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt())
|
||||
{ error="world must be compiled before generating tiles";return std::nullopt; }
|
||||
if(!impl_||!impl_->owner)
|
||||
{ error="resolved world snapshot is empty";return std::nullopt; }
|
||||
const bool bShapeReady=impl_->shapeReady.load(std::memory_order_acquire);
|
||||
const bool bNativeOnly=!bShapeReady||options.diagnosticForceNativeOnly;
|
||||
if(!bShapeReady&&!options.diagnosticForceNativeOnly&&!isTileShapeFree(key))
|
||||
{error="tile is shape-dependent and must wait for shape compilation";return std::nullopt;}
|
||||
if(!bNativeOnly&&(!impl_->shape||!impl_->shape->IsBuilt()))
|
||||
{error="world shape publication is incomplete";return std::nullopt;}
|
||||
FScopedSharedColumnCaches sharedColumnCaches(options.useSharedColumnCache
|
||||
?impl_->sharedHeightfieldColumns.get():nullptr,
|
||||
options.useSharedColumnCache?impl_->sharedIslandColumns.get():nullptr);
|
||||
const auto& world=*impl_->owner;
|
||||
FStandaloneShapeWorld nativeOnlyShape;
|
||||
FIntVector nativeOnlyMinimum,nativeOnlyMaximum;
|
||||
std::shared_ptr<FStandaloneShapeWorld> page;
|
||||
if(impl_->tileRegionCount.load(std::memory_order_acquire)!=0)
|
||||
if(bNativeOnly)
|
||||
{
|
||||
if(!NativeOnlyBoundsForTile(*impl_,key,nativeOnlyMinimum,nativeOnlyMaximum))
|
||||
{error="shape-free guard: tile has no bounded native-only domain";return std::nullopt;}
|
||||
if(!options.diagnosticForceNativeOnly&&!isTileShapeFree(key))
|
||||
{error="shape-free guard: tile failed the native-only readiness proof";return std::nullopt;}
|
||||
nativeOnlyShape.InitializeNativeSurfaceOnly(*impl_->owner,impl_->worldAssembly.get(),
|
||||
nativeOnlyMinimum,nativeOnlyMaximum);
|
||||
}
|
||||
else if(impl_->tileRegionCount.load(std::memory_order_acquire)!=0)
|
||||
{
|
||||
std::shared_lock lock(impl_->tileRegionMutex);
|
||||
if(const auto found=impl_->tileRegions.find(key);found!=impl_->tileRegions.end())
|
||||
page=found->second;
|
||||
}
|
||||
const FStandaloneShapeWorld* tileShapeContext=page?page.get():impl_->shape.get();
|
||||
const FStandaloneShapeWorld* tileShapeContext=bNativeOnly?&nativeOnlyShape:
|
||||
(page?page.get():impl_->shape.get());
|
||||
UVoxelMarchingCubesMesher mesher;
|
||||
mesher.SetGenerator(world.Generator.get());
|
||||
mesher.SetGeometryVoxelSizeCm(static_cast<float>(world.CoreVoxelSizeMicrons)/10000.0f);
|
||||
@@ -1402,6 +1694,12 @@ namespace VoxelForgeCore
|
||||
}
|
||||
if(!sharpFeatures.IsEmpty())
|
||||
{
|
||||
if(bNativeOnly)
|
||||
{
|
||||
nativeOnlyShape.RecordNativeOnlyViolation();
|
||||
error="shape-free guard: tile intersects a shape feature";
|
||||
return std::nullopt;
|
||||
}
|
||||
// Streamed analytic pages omit the native assembly plan. Use the immutable base
|
||||
// shape only for tagged tiles so their mesher samples the same field as the tags.
|
||||
// Les pages analytiques omettent le plan natif ; utiliser la base uniquement pour
|
||||
@@ -1446,13 +1744,26 @@ namespace VoxelForgeCore
|
||||
}
|
||||
}
|
||||
}
|
||||
int32 MeshCells=cells,MeshStep=step;
|
||||
if(options.diagnosticCellStride>1&&level>=3)
|
||||
{
|
||||
const int32 Stride=static_cast<int32>(std::min<std::uint32_t>(
|
||||
options.diagnosticCellStride,static_cast<std::uint32_t>(std::max(1,cells))));
|
||||
MeshCells=FMath::Max(1,cells/Stride);
|
||||
MeshStep=FMath::Max(1,extent/MeshCells);
|
||||
}
|
||||
int64 sampleCount=0;std::vector<float> densityValues;
|
||||
FVoxelMarchingCubesStageTimings McStageTimings;
|
||||
TileMesh out;out.key=key;
|
||||
out.diagnostics.featureRefinedResolution=bFeatureRefinedResolution;
|
||||
if(options.profileMesherStages)
|
||||
{
|
||||
out.diagnostics.nativeOnlyRoute=bNativeOnly;
|
||||
out.diagnostics.analyticRegionPageRoute=page&&page->IsAnalyticOnly();
|
||||
}
|
||||
const bool bHasWorldAssembly=impl_->worldAssembly!=nullptr;
|
||||
const bool bNeedsFarShell=bHasWorldAssembly&&key.lod>=7
|
||||
&&(tileShape.HasFarWorld()||impl_->shape->HasFarWorld());
|
||||
&&(tileShape.HasFarWorld()||(bShapeReady&&impl_->shape->HasFarWorld()));
|
||||
bool bOverlapsPlayerEdit=false;
|
||||
if(tileShape.HasEditTransaction())
|
||||
{
|
||||
@@ -1492,6 +1803,15 @@ namespace VoxelForgeCore
|
||||
FStandaloneShapeWorld::ResetThreadClassificationStats();
|
||||
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(
|
||||
options.captureColumnCoordinates);
|
||||
if(options.captureColumnCoordinates)
|
||||
{
|
||||
constexpr std::int64_t SubvoxelsPerVoxel=WorldAssemblySubvoxelsPerVoxel;
|
||||
VoxelForgeCore::Heightfield2D::setThreadColumnCaptureBounds(
|
||||
static_cast<std::int64_t>(origin.X)*SubvoxelsPerVoxel,
|
||||
static_cast<std::int64_t>(origin.Y)*SubvoxelsPerVoxel,
|
||||
static_cast<std::int64_t>(origin.X+extent)*SubvoxelsPerVoxel,
|
||||
static_cast<std::int64_t>(origin.Y+extent)*SubvoxelsPerVoxel);
|
||||
}
|
||||
bool bNativeSurfaceTile=false;
|
||||
bool bNativeSurfaceBandSafe=false;
|
||||
double classificationColumnComputeMilliseconds=0.0;
|
||||
@@ -1504,6 +1824,8 @@ namespace VoxelForgeCore
|
||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds
|
||||
-columnMillisecondsBeforeClassification);
|
||||
bNativeSurfaceTile=Classification.bNativeSurface;
|
||||
if(options.profileMesherStages)
|
||||
out.diagnostics.nativeSurfaceTile=bNativeSurfaceTile;
|
||||
// NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its
|
||||
// bounded type-2/type-4 features. Unknown tile occupancy does not make that Z support
|
||||
// unknown; edits and cross-strate tiles are still excluded below.
|
||||
@@ -1520,7 +1842,8 @@ namespace VoxelForgeCore
|
||||
}
|
||||
if((out.diagnostics.tileClass==TileClass::allSolid
|
||||
||out.diagnostics.tileClass==TileClass::allAir)
|
||||
&&!options.hashDensity&&!options.captureDensitySamples&&!bNeedsFarShell)
|
||||
&&!options.hashDensity&&!options.captureDensitySamples&&!bNeedsFarShell
|
||||
&&!options.auditUniformBlockProofs)
|
||||
{
|
||||
out.diagnostics.meshingSkipped=true;
|
||||
const auto Counts=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||
@@ -1536,6 +1859,8 @@ namespace VoxelForgeCore
|
||||
out.diagnostics.columnCacheHits=Columns.columnHits;
|
||||
out.diagnostics.columnCacheMisses=Columns.columnMisses;
|
||||
out.diagnostics.columnCacheConflictMisses=Columns.columnConflictMisses;
|
||||
out.diagnostics.sharedColumnCacheHits=Columns.sharedColumnHits;
|
||||
out.diagnostics.columnEvaluations=Columns.columnEvaluations;
|
||||
out.diagnostics.classificationMilliseconds=Counts.Milliseconds;
|
||||
out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0,
|
||||
Counts.Milliseconds-classificationColumnComputeMilliseconds);
|
||||
@@ -1559,7 +1884,7 @@ namespace VoxelForgeCore
|
||||
int32 MeshBandMinZVoxel=options.bandMinZVoxel;
|
||||
int32 MeshBandMaxZVoxel=options.bandMaxZVoxel;
|
||||
bool bNativeHeightfieldBandLimited=false;
|
||||
if(bHasWorldAssembly&&bNativeSurfaceBandSafe
|
||||
if(bHasWorldAssembly&&bNativeSurfaceBandSafe&&!(page&&page->IsAnalyticOnly())
|
||||
&&!options.hashDensity&&!options.captureDensitySamples&&world.Manager!=nullptr)
|
||||
{
|
||||
// La surface 2D et les bornes de volume du manifeste definissent tout le support Z.
|
||||
@@ -1678,13 +2003,48 @@ namespace VoxelForgeCore
|
||||
out.diagnostics.featureListMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-FeatureListStart).count();
|
||||
const ScopedWorldAssemblyFeatureQueriesV1 featureScope(featureQueries);
|
||||
const bool bDiagnosticColumnSampling=options.diagnosticGlobalOctaveDrop!=0
|
||||
||options.diagnosticColumnInterpolationStride>1;
|
||||
const std::uint32_t PreviousOctaveDrop=bDiagnosticColumnSampling
|
||||
?Heightfield2D::threadColumnOctaveDrop():0;
|
||||
const std::int64_t PreviousInterpolationStride=bDiagnosticColumnSampling
|
||||
?Heightfield2D::threadColumnInterpolationStrideSubvoxels():0;
|
||||
if(bDiagnosticColumnSampling)
|
||||
{
|
||||
Heightfield2D::setThreadColumnOctaveDrop(options.diagnosticGlobalOctaveDrop);
|
||||
Heightfield2D::setThreadColumnInterpolationStrideSubvoxels(
|
||||
options.diagnosticColumnInterpolationStride>1&&level>=3
|
||||
?static_cast<std::int64_t>(MeshStep)*WorldAssemblySubvoxelsPerVoxel
|
||||
*options.diagnosticColumnInterpolationStride:0);
|
||||
}
|
||||
const double ColumnMillisecondsBeforeMeshing=
|
||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
||||
const auto MarchingCubesStart=std::chrono::steady_clock::now();
|
||||
FVoxelMeshData mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
|
||||
FVoxelUniformBlockProofAuditStats BlockProofAudit;
|
||||
FVoxelMeshData mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr,
|
||||
options.profileMesherStages?&McStageTimings:nullptr);
|
||||
options.profileMesherStages?&McStageTimings:nullptr,
|
||||
options.auditUniformBlockProofs?&BlockProofAudit:nullptr,
|
||||
options.forceFullDensityCapture);
|
||||
if(options.auditUniformBlockProofs)
|
||||
{
|
||||
out.diagnostics.uniformBlockProofsChecked=BlockProofAudit.CheckedBlocks;
|
||||
out.diagnostics.uniformBlockProofSamplesChecked=BlockProofAudit.CheckedSamples;
|
||||
out.diagnostics.uniformBlockProofFallbackBlocks=BlockProofAudit.FallbackBlocks;
|
||||
out.diagnostics.uniformBlockProofUnknownBlocks=BlockProofAudit.UnknownBlocks;
|
||||
out.diagnostics.uniformBlockProofViolations=BlockProofAudit.Violations;
|
||||
}
|
||||
if(bDiagnosticColumnSampling)
|
||||
{
|
||||
Heightfield2D::setThreadColumnOctaveDrop(PreviousOctaveDrop);
|
||||
Heightfield2D::setThreadColumnInterpolationStrideSubvoxels(PreviousInterpolationStride);
|
||||
}
|
||||
if(bNativeOnly&&tileShape.GetNativeOnlyViolationCount()!=0)
|
||||
{
|
||||
error="shape-free guard caught a mesher read outside the native SurfaceWorld field";
|
||||
return std::nullopt;
|
||||
}
|
||||
const double MarchingCubesWallMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-MarchingCubesStart).count();
|
||||
const double ColumnMillisecondsAfterMeshing=
|
||||
@@ -1695,6 +2055,10 @@ namespace VoxelForgeCore
|
||||
ColumnMillisecondsAfterMeshing-ColumnMillisecondsBeforeMeshing);
|
||||
if(options.profileMesherStages)
|
||||
{
|
||||
out.diagnostics.marchingCubesSkirtMilliseconds=McStageTimings.SkirtMilliseconds;
|
||||
out.diagnostics.marchingCubesExclusiveMilliseconds=std::max(0.0,
|
||||
out.diagnostics.marchingCubesExclusiveMilliseconds
|
||||
-out.diagnostics.marchingCubesSkirtMilliseconds);
|
||||
out.diagnostics.marchingCubesDensityGridMilliseconds=McStageTimings.DensityGridMilliseconds;
|
||||
out.diagnostics.marchingCubesCellClassificationMilliseconds=
|
||||
McStageTimings.CellClassificationMilliseconds;
|
||||
@@ -1715,12 +2079,23 @@ namespace VoxelForgeCore
|
||||
std::chrono::steady_clock::now()-MeshingStart).count();
|
||||
out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0;
|
||||
const auto FarShellStart=std::chrono::steady_clock::now();
|
||||
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:*impl_->shape;
|
||||
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:
|
||||
(bShapeReady?*impl_->shape:tileShape);
|
||||
out.diagnostics.farShellRepairSkipped=bSkipFarShellRepair;
|
||||
if(farShape.HasFarWorld()&&!bSkipFarShellRepair)
|
||||
{
|
||||
const auto BeforeStratum=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||
const int32 BeforeStratumTriangles=mesh.Triangles.Num()/3;
|
||||
std::array<std::uint64_t,6> BeforeStratumColumnCacheStats;
|
||||
double BeforeStratumColumnComputeMilliseconds;
|
||||
if(options.profileMesherStages)
|
||||
{
|
||||
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
|
||||
BeforeStratumColumnCacheStats={Columns.pointCalls,Columns.columnHits,
|
||||
Columns.columnMisses,Columns.columnConflictMisses,
|
||||
Columns.sharedColumnHits,Columns.columnEvaluations};
|
||||
BeforeStratumColumnComputeMilliseconds=Columns.columnComputeMilliseconds;
|
||||
}
|
||||
const auto StratumStart=std::chrono::steady_clock::now();
|
||||
AppendFarStratumShells(world,farShape,nativeKey,step,mesh);
|
||||
const auto AfterStratum=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||
@@ -1728,11 +2103,39 @@ namespace VoxelForgeCore
|
||||
std::chrono::steady_clock::now()-StratumStart).count();
|
||||
out.diagnostics.farStratumFieldCalls=
|
||||
AfterStratum.NativeFloatFieldCalls-BeforeStratum.NativeFloatFieldCalls;
|
||||
if(options.profileMesherStages)
|
||||
{
|
||||
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
|
||||
out.diagnostics.farStratumColumnPointCalls=
|
||||
Columns.pointCalls-BeforeStratumColumnCacheStats[0];
|
||||
out.diagnostics.farStratumColumnCacheHits=
|
||||
Columns.columnHits-BeforeStratumColumnCacheStats[1];
|
||||
out.diagnostics.farStratumColumnCacheMisses=
|
||||
Columns.columnMisses-BeforeStratumColumnCacheStats[2];
|
||||
out.diagnostics.farStratumColumnConflictMisses=
|
||||
Columns.columnConflictMisses-BeforeStratumColumnCacheStats[3];
|
||||
out.diagnostics.farStratumSharedColumnCacheHits=
|
||||
Columns.sharedColumnHits-BeforeStratumColumnCacheStats[4];
|
||||
out.diagnostics.farStratumColumnEvaluations=
|
||||
Columns.columnEvaluations-BeforeStratumColumnCacheStats[5];
|
||||
out.diagnostics.farStratumColumnComputeMilliseconds=
|
||||
Columns.columnComputeMilliseconds-BeforeStratumColumnComputeMilliseconds;
|
||||
}
|
||||
out.diagnostics.farShellTrianglesAdded=static_cast<std::uint64_t>(
|
||||
std::max(0,mesh.Triangles.Num()/3-BeforeStratumTriangles));
|
||||
|
||||
const auto BeforeConnector=AfterStratum;
|
||||
const int32 BeforeConnectorTriangles=mesh.Triangles.Num()/3;
|
||||
std::array<std::uint64_t,6> BeforeConnectorColumnCacheStats;
|
||||
double BeforeConnectorColumnComputeMilliseconds;
|
||||
if(options.profileMesherStages)
|
||||
{
|
||||
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
|
||||
BeforeConnectorColumnCacheStats={Columns.pointCalls,Columns.columnHits,
|
||||
Columns.columnMisses,Columns.columnConflictMisses,
|
||||
Columns.sharedColumnHits,Columns.columnEvaluations};
|
||||
BeforeConnectorColumnComputeMilliseconds=Columns.columnComputeMilliseconds;
|
||||
}
|
||||
const auto ConnectorStart=std::chrono::steady_clock::now();
|
||||
AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh);
|
||||
const auto AfterConnector=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||
@@ -1740,6 +2143,24 @@ namespace VoxelForgeCore
|
||||
std::chrono::steady_clock::now()-ConnectorStart).count();
|
||||
out.diagnostics.farConnectorFieldCalls=
|
||||
AfterConnector.NativeFloatFieldCalls-BeforeConnector.NativeFloatFieldCalls;
|
||||
if(options.profileMesherStages)
|
||||
{
|
||||
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
|
||||
out.diagnostics.farConnectorColumnPointCalls=
|
||||
Columns.pointCalls-BeforeConnectorColumnCacheStats[0];
|
||||
out.diagnostics.farConnectorColumnCacheHits=
|
||||
Columns.columnHits-BeforeConnectorColumnCacheStats[1];
|
||||
out.diagnostics.farConnectorColumnCacheMisses=
|
||||
Columns.columnMisses-BeforeConnectorColumnCacheStats[2];
|
||||
out.diagnostics.farConnectorColumnConflictMisses=
|
||||
Columns.columnConflictMisses-BeforeConnectorColumnCacheStats[3];
|
||||
out.diagnostics.farConnectorSharedColumnCacheHits=
|
||||
Columns.sharedColumnHits-BeforeConnectorColumnCacheStats[4];
|
||||
out.diagnostics.farConnectorColumnEvaluations=
|
||||
Columns.columnEvaluations-BeforeConnectorColumnCacheStats[5];
|
||||
out.diagnostics.farConnectorColumnComputeMilliseconds=
|
||||
Columns.columnComputeMilliseconds-BeforeConnectorColumnComputeMilliseconds;
|
||||
}
|
||||
out.diagnostics.farShellTrianglesAdded+=static_cast<std::uint64_t>(
|
||||
std::max(0,mesh.Triangles.Num()/3-BeforeConnectorTriangles));
|
||||
}
|
||||
@@ -1798,6 +2219,8 @@ namespace VoxelForgeCore
|
||||
out.diagnostics.columnCacheHits=Columns.columnHits;
|
||||
out.diagnostics.columnCacheMisses=Columns.columnMisses;
|
||||
out.diagnostics.columnCacheConflictMisses=Columns.columnConflictMisses;
|
||||
out.diagnostics.sharedColumnCacheHits=Columns.sharedColumnHits;
|
||||
out.diagnostics.columnEvaluations=Columns.columnEvaluations;
|
||||
out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds;
|
||||
out.diagnostics.columnEvaluationExclusiveMilliseconds=Columns.columnComputeMilliseconds;
|
||||
out.diagnostics.classificationMilliseconds=Counts.Milliseconds;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
@@ -121,6 +122,9 @@ WorldAssemblyFeatureColumnRangeV1 BuildWorldAssemblyFeatureColumnRangeV1(
|
||||
|
||||
namespace world_assembly_detail
|
||||
{
|
||||
using FSharedHeightfieldColumnCache =
|
||||
VoxelForgeCoreShared::TSharedColumnValueCache<HeightfieldPointSample,131072,4>;
|
||||
|
||||
// Preserve recipe order without allocating or looking up metadata in the point loop.
|
||||
// Garder l'ordre de recette sans allocation ni recherche de metadonnees par point.
|
||||
struct FeatureRange
|
||||
@@ -1031,6 +1035,7 @@ namespace world_assembly_detail
|
||||
// Audit-only override; normal tile generation resets this to zero before evaluating a recipe.
|
||||
// Surcharge reservee aux mesures; la generation remet zero avant d'evaluer une recette.
|
||||
inline thread_local std::uint32_t HeightfieldColumnOctaveDrop = 0;
|
||||
inline thread_local std::int64_t HeightfieldColumnInterpolationStrideSubvoxels = 0;
|
||||
|
||||
inline std::int32_t FractalNoiseQ16(std::int64_t xQ16, std::int64_t yQ16,
|
||||
std::int64_t zQ16, std::uint32_t octaves, bool ridged = false)
|
||||
@@ -1374,6 +1379,9 @@ namespace world_assembly_detail
|
||||
std::array<std::uint8_t, HeightfieldColumnCacheSetCount>> HeightfieldColumnCacheNextWay;
|
||||
inline thread_local HeightfieldColumnCacheStats HeightfieldColumnStats{};
|
||||
inline thread_local bool HeightfieldColumnCacheLegacyDirectMapped=false;
|
||||
inline thread_local std::array<std::int64_t,4> HeightfieldColumnCaptureBounds{};
|
||||
inline thread_local bool HeightfieldColumnCaptureBoundsValid=false;
|
||||
inline std::atomic<std::uint64_t> HeightfieldColumnCaptureSequence{0};
|
||||
|
||||
inline std::size_t HeightfieldColumnCacheSet(const WorldAssemblyRecipeV1& recipe,
|
||||
const WorldAssemblyStratumPlanV1& plan,std::int64_t x,std::int64_t y) noexcept
|
||||
@@ -1506,6 +1514,43 @@ namespace world_assembly_detail
|
||||
inline HeightfieldPointSample CachedColumn(const WorldAssemblyRecipeV1& recipe,
|
||||
const WorldAssemblyStratumPlanV1& plan,std::int64_t x,std::int64_t y)
|
||||
{
|
||||
const std::int64_t InterpolationStride=HeightfieldColumnInterpolationStrideSubvoxels;
|
||||
if(InterpolationStride>0)
|
||||
{
|
||||
const std::int64_t X0=FloorDiv(x,InterpolationStride)*InterpolationStride;
|
||||
const std::int64_t Y0=FloorDiv(y,InterpolationStride)*InterpolationStride;
|
||||
const std::int64_t X1=X0+InterpolationStride,Y1=Y0+InterpolationStride;
|
||||
const std::int64_t Fx=x-X0,Fy=y-Y0;
|
||||
struct FScopedInterpolationSampling
|
||||
{
|
||||
std::int64_t Previous=HeightfieldColumnInterpolationStrideSubvoxels;
|
||||
~FScopedInterpolationSampling()
|
||||
{HeightfieldColumnInterpolationStrideSubvoxels=Previous;}
|
||||
} SamplingScope;
|
||||
HeightfieldColumnInterpolationStrideSubvoxels=0;
|
||||
const HeightfieldPointSample C00=CachedColumn(recipe,plan,X0,Y0);
|
||||
const HeightfieldPointSample C10=CachedColumn(recipe,plan,X1,Y0);
|
||||
const HeightfieldPointSample C01=CachedColumn(recipe,plan,X0,Y1);
|
||||
const HeightfieldPointSample C11=CachedColumn(recipe,plan,X1,Y1);
|
||||
const auto Lerp=[&](std::int64_t A,std::int64_t B,std::int64_t T)
|
||||
{return A+((B-A)*T)/InterpolationStride;};
|
||||
const auto Bilinear=[&](std::int64_t A,std::int64_t B,
|
||||
std::int64_t C,std::int64_t D)
|
||||
{
|
||||
const std::int64_t Top=Lerp(A,B,Fx),Bottom=Lerp(C,D,Fx);
|
||||
return Lerp(Top,Bottom,Fy);
|
||||
};
|
||||
HeightfieldPointSample Out=C00;
|
||||
Out.groundHeightSubvoxels=Bilinear(C00.groundHeightSubvoxels,
|
||||
C10.groundHeightSubvoxels,C01.groundHeightSubvoxels,
|
||||
C11.groundHeightSubvoxels);
|
||||
Out.capHeightSubvoxels=Bilinear(C00.capHeightSubvoxels,
|
||||
C10.capHeightSubvoxels,C01.capHeightSubvoxels,
|
||||
C11.capHeightSubvoxels);
|
||||
return Out;
|
||||
}
|
||||
if(HeightfieldColumnOctaveDrop!=0)
|
||||
return EvaluateColumn(recipe,plan,x,y);
|
||||
auto& cache=HeightfieldColumnCache.Get();
|
||||
auto& nextWay=HeightfieldColumnCacheNextWay.Get();
|
||||
HeightfieldColumnCacheEntry* replacement=nullptr;
|
||||
@@ -1549,22 +1594,46 @@ namespace world_assembly_detail
|
||||
replacement=&cache[first+replacementWay];
|
||||
}
|
||||
auto& entry=*replacement;
|
||||
++HeightfieldColumnStats.columnMisses;
|
||||
const auto compute=[&]()
|
||||
{
|
||||
const std::uint64_t sequence=HeightfieldColumnStats.captureCoordinates
|
||||
?HeightfieldColumnCaptureSequence.fetch_add(1,std::memory_order_relaxed):0;
|
||||
const auto start=std::chrono::steady_clock::now();
|
||||
HeightfieldPointSample column=EvaluateColumn(recipe,plan,x,y);
|
||||
HeightfieldPointSample result=EvaluateColumn(recipe,plan,x,y);
|
||||
HeightfieldColumnStats.columnComputeMilliseconds+=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-start).count();
|
||||
++HeightfieldColumnStats.columnMisses;
|
||||
if(HeightfieldColumnStats.captureCoordinates&&HeightfieldColumnStats.coordinateCaptureComplete)
|
||||
++HeightfieldColumnStats.columnEvaluations;
|
||||
if(HeightfieldColumnStats.captureCoordinates
|
||||
&&HeightfieldColumnStats.coordinateCaptureComplete)
|
||||
{
|
||||
try
|
||||
{
|
||||
HeightfieldColumnStats.evaluatedCoordinates.push_back({x,y});
|
||||
const bool outside=HeightfieldColumnCaptureBoundsValid
|
||||
&&(x<HeightfieldColumnCaptureBounds[0]||x>HeightfieldColumnCaptureBounds[2]
|
||||
||y<HeightfieldColumnCaptureBounds[1]||y>HeightfieldColumnCaptureBounds[3]);
|
||||
HeightfieldColumnStats.evaluatedCoordinates.push_back({x,y,sequence,outside});
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
HeightfieldColumnStats.coordinateCaptureComplete=false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
HeightfieldPointSample column{};
|
||||
if(auto* shared=FSharedHeightfieldColumnCache::threadActive())
|
||||
{
|
||||
const VoxelForgeCoreShared::FColumnCacheKey key{
|
||||
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe)),
|
||||
recipe.contentHash,
|
||||
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
|
||||
plan.contentHash,x,y};
|
||||
bool computed=false;
|
||||
column=shared->getOrCompute(key,compute,computed);
|
||||
if(!computed)++HeightfieldColumnStats.sharedColumnHits;
|
||||
}
|
||||
else column=compute();
|
||||
entry={&recipe,&plan,recipe.contentHash,plan.contentHash,x,y,column};
|
||||
return column;
|
||||
}
|
||||
@@ -2049,15 +2118,40 @@ void Heightfield2D::resetThreadColumnCacheStats(bool captureCoordinates, bool cl
|
||||
Stats.columnHits=0;
|
||||
Stats.columnMisses=0;
|
||||
Stats.columnConflictMisses=0;
|
||||
Stats.sharedColumnHits=0;
|
||||
Stats.columnEvaluations=0;
|
||||
Stats.columnComputeMilliseconds=0.0;
|
||||
Stats.captureCoordinates=captureCoordinates;
|
||||
Stats.coordinateCaptureComplete=true;
|
||||
Stats.evaluatedCoordinates.clear();
|
||||
world_assembly_detail::HeightfieldColumnCaptureBoundsValid=false;
|
||||
}
|
||||
|
||||
void Heightfield2D::setThreadColumnCaptureBounds(std::int64_t minimumXSubvoxels,
|
||||
std::int64_t minimumYSubvoxels,std::int64_t maximumXSubvoxels,
|
||||
std::int64_t maximumYSubvoxels) noexcept
|
||||
{
|
||||
world_assembly_detail::HeightfieldColumnCaptureBounds={minimumXSubvoxels,
|
||||
minimumYSubvoxels,maximumXSubvoxels,maximumYSubvoxels};
|
||||
world_assembly_detail::HeightfieldColumnCaptureBoundsValid=true;
|
||||
}
|
||||
|
||||
void Heightfield2D::setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept
|
||||
{ world_assembly_detail::HeightfieldColumnCacheLegacyDirectMapped=enabled; }
|
||||
|
||||
void Heightfield2D::setThreadColumnOctaveDrop(std::uint32_t octaveDrop) noexcept
|
||||
{ world_assembly_detail::HeightfieldColumnOctaveDrop=octaveDrop; }
|
||||
|
||||
std::uint32_t Heightfield2D::threadColumnOctaveDrop() noexcept
|
||||
{ return world_assembly_detail::HeightfieldColumnOctaveDrop; }
|
||||
|
||||
void Heightfield2D::setThreadColumnInterpolationStrideSubvoxels(std::int64_t stride) noexcept
|
||||
{ world_assembly_detail::HeightfieldColumnInterpolationStrideSubvoxels=std::max<std::int64_t>(0,stride); }
|
||||
|
||||
std::int64_t Heightfield2D::threadColumnInterpolationStrideSubvoxels() noexcept
|
||||
{ return world_assembly_detail::HeightfieldColumnInterpolationStrideSubvoxels; }
|
||||
|
||||
|
||||
const HeightfieldColumnCacheStats& Heightfield2D::threadColumnCacheStats() noexcept
|
||||
{ return world_assembly_detail::HeightfieldColumnStats; }
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Linux10 — startup and region crossing
|
||||
|
||||
**Result:** no world-generation optimization was shipped. The dominant spawn gate is full shape compilation; the current Core API does not let tiles consume a partially built shape safely. L10 adds opt-in phase and crossing telemetry to the headless research path.
|
||||
|
||||
Runs used CPUs 0–11, normal priority, at most `-j12`, a 6 GiB address-space cap, and the unchanged `powersave` governor. Peak monitored RSS was 624,996 KiB (610.3 MiB). Final builds: Clang 19 with FP contraction off, SHA-256 `7d5360e2baa637f8b2b4679df9616580bffe689afd101470e311260f0ecd4975`; GCC 14 default, `5041cf15019766f453eb59fe8ff7fc4cd98ca6a904208b645e3d7ca4584e1195`. The L9 Clang timing/hash baseline is `Standalone/bin/linux/VoxelForgeCoreHeadless-linux9-clang`, SHA-256 `0cd3f0ef993ced2c615418c6e2b37b83f57eff6d024d7d1568c30ea51490384a`.
|
||||
|
||||
## 1. Startup phases
|
||||
|
||||
SurfaceWorld seed 0, W12, no hash; five runs on the optimized Clang build. Wall and process CPU are medians. Process CPU sums work across threads. First-tile time is from cold start; its CPU column is CPU since pool start.
|
||||
|
||||
| Phase | Wall ms | Process CPU ms | Threads used |
|
||||
|---|---:|---:|---|
|
||||
| Owner data load and resolve | 5.05 | 5.05 | 1 |
|
||||
| Assembly plan build | 51.04 | 51.03 | 1 |
|
||||
| Core snapshot | 0.53 | 0.54 | 1 |
|
||||
| First desired-set build, 2,553 tiles | 0.74 | 0.74 | 1 |
|
||||
| Shape request setup | 0.03 | 0.03 | 1 |
|
||||
| Shape compile | 222.59 | 856.79 | caller + up to 8 scheduler workers |
|
||||
| Streamer setup | 0.01 | 0.01 | 1 |
|
||||
| Worker-pool start | 0.41 | 1.27 | 12 requested; 8–11 had entered at print |
|
||||
| First tile starts at 299.62 ms | — | 1.59 since pool start | 12/12 entered |
|
||||
|
||||
Shape-compile wall ranged 215.89–324.88 ms; first tile started at 286.56–409.76 ms. Owner load, plan, local shape setup, and far-parent construction are serial. Before the pool starts, there are no sleeping stream workers, but most of the 12 allowed CPUs are unused. During the interval and detail-band phases the scheduler uses at most 8 workers, leaving up to 4 CPUs unused.
|
||||
|
||||
The phase-trace mode’s whole-view median was 1.436 s, versus 1.372 s with tracing off. The phase runs are for decomposition only; the performance ranking and A/B below use the optimized build with tracing disabled and hashing off. In that flag-off path, shape wall time was 0.222 s median, consistent with the phase trace.
|
||||
|
||||
## 2. Shape contents and first-tile dependencies
|
||||
|
||||
| Shape phase | Wall ms | Process CPU ms | Work / concurrency |
|
||||
|---|---:|---:|---|
|
||||
| Local owner shapes and spatial bins | 40.46 | 40.44 | Extracts owner primitives; 1 thread |
|
||||
| Rock intervals, signs, candidate bands | 40.20 | 187.14 | Packed column signs and candidate masks; up to 8 workers |
|
||||
| Detail band and sign-edge distances | 81.00 | 568.78 | Applies coherent detail to candidate nodes and builds distance proxies; up to 8 workers |
|
||||
| Route validation and shared-field setup | 1.12 | 1.13 | 5,642 passage samples; then shared primitive form |
|
||||
| Far parents and bins | 55.56 | 55.57 | 6,050 far rooms / 18,348 links; 1 thread |
|
||||
|
||||
The compile extracts 18,348 owner primitives (71 rooms, 74 tunnels, 2 passages), covers 138,188,413 sign nodes, allocates 36.71 MiB of packed words and 263.57 MiB of proxy address capacity, and visits 214,538 detail candidates (6,098 flips).
|
||||
|
||||
For this SurfaceWorld fixture, the first LOD0 tile’s density queries take the native `Heightfield2D` path before packed-shape lookup. Far-parent shapes are not needed by that first tile. However, `World::generateTile` rejects requests until the full base shape is marked built, and `compileTileRegion` also requires that base shape. So the current API gates the near tile on the interval, detail, validation, and far-parent work even when that tile does not sample those arrays. Other recipes and fallback/query paths do use the analytic primitives and packed signs/proxies.
|
||||
|
||||
There is no seed-independent owner shape payload to cache at build time. The assembly plan is built from owner data; region pages also re-run owner primitive extraction, spatial-bin construction, and shared-field packing. Those immutable owner-derived structures are a possible per-world reuse target, but the measured short crossings below built no pages, so this run does not price that work.
|
||||
|
||||
## 3. Moving crossing
|
||||
|
||||
Hashing off, W12, same camera and already-streamed initial view. Region compile was called, but all entering fine keys were covered by the initial shape bounds; it built zero regions.
|
||||
|
||||
| Camera step | New tiles | LOD breakdown | Region compile | Tile work wall | Crossing total |
|
||||
|---|---:|---|---:|---:|---:|
|
||||
| 8 m | 144 | LOD2: 128, LOD3: 16 | 0.010 ms | 12.61 ms | 12.81 ms |
|
||||
| 16 m | 288 | LOD0: 128, LOD1: 16, LOD2: 128, LOD3: 16 | 0.010 ms | 60.49 ms | 60.68 ms |
|
||||
|
||||
The 16 m crossing’s worker-time sum was 633 ms; its wall time was 60.5 ms. No plan or full shape compile repeated. A 24 m attempt terminated with `std::bad_alloc` after base shape compilation and before crossing metrics; the failing allocation site is unknown.
|
||||
|
||||
## 4. Ranked levers
|
||||
|
||||
| Rank | Lever | Upper bound from these measurements | Assessment |
|
||||
|---:|---|---|---|
|
||||
| 1 | Overlap near-tile work with shape compilation | 222.6 ms, 16.2% of 1.372 s whole-view-ready; first tile could start earlier by up to the compile time | Largest opportunity. Not built: tiles and region compilation currently require a fully built base shape. A safe version needs immutable partial-page publication and a wait before any tile reads an unfinished sign, proxy, primitive bin, or far field. |
|
||||
| 2 | Defer far-parent construction until far tiles need it | 55.6 ms, at most 4.1% of whole-view-ready and 18.6% of the first-tile delay | Could improve spawn, but far and near paths share mutable spatial-bin state today. Needs separate immutable bins and a synchronized readiness state; no crossing benefit was measured. |
|
||||
| 3 | Parallelize assembly-plan construction | 51.0 ms, at most 3.7% of whole-view-ready | Serial and seed/owner-specific. No safe deterministic parallel split was established. |
|
||||
| 4 | Raise shape scheduler cap from 8 to 12 | About 121 ms currently lies in the two worker phases; ideal linear scaling saves about 40 ms (2.9% of whole-view-ready) | Below the 3% bar before overhead, and no crossing benefit. Not built. |
|
||||
|
||||
For any future partial or deferred build, hashes can fail if a tile observes a partially written sign/proxy array, if region publication races with tile lookup, or if shared bins change while a tile is sampling. The page must be immutable before publication; worker-count and timing tests must compare W1 with W12 and repeat W12. Any asynchronous result must also follow the Epoch/MPSC shutdown-drain rules.
|
||||
|
||||
## 5. Final-build timing and hashes
|
||||
|
||||
Five interleaved W12 runs, no hash. Pair values are `whole_view_ready / stream_wall` in seconds.
|
||||
|
||||
| Pair | L9 `7650abb` Clang | L10 Clang | Delta, ms |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | 1.336 / 1.050 | 1.346 / 1.060 | +10 / +10 |
|
||||
| 2 | 1.351 / 1.062 | 1.365 / 1.073 | +14 / +11 |
|
||||
| 3 | 1.359 / 1.073 | 1.374 / 1.083 | +15 / +10 |
|
||||
| 4 | 1.363 / 1.077 | 1.375 / 1.081 | +12 / +4 |
|
||||
| 5 | 1.387 / 1.089 | 1.372 / 1.092 | −15 / +3 |
|
||||
| **Median** | **1.359 / 1.073** | **1.372 / 1.081** | **+13 / +8** |
|
||||
|
||||
The measurement-only candidate is +1.0% whole-view-ready and +0.7% stream median; this is no demonstrated speedup. Hashing was off for these timings.
|
||||
|
||||
Final rebuilt binaries passed SurfaceWorld and FloatingIslands seed-0 hash checks at W1, W8, and five W12 runs per compiler: Clang FP contraction off and GCC default. Every full-detail, whole-view, LOD0–8, and exact-query row matches the compiler-matched L9 baseline. Audit files: `Saved/Linux/l10_final_surface_hash_audit.txt` and `Saved/Linux/l10_islands_hash_audit.txt`.
|
||||
|
||||
| Recipe / scope | Density | Mesh | Payload | Topology |
|
||||
|---|---|---|---|---|
|
||||
| SurfaceWorld full detail | `0x0ee0579bd15cbbe4` | `0x32e5d4964d610fee` | `0x1932b96ee4b1d910` | `0x69743c6bad3447dc` |
|
||||
| SurfaceWorld whole view | `0xc10697a8f48380c7` | `0xe75c9119a1498cd5` | `0x49a9a8d1835a6c8c` | `0xedcb23288b5e8724` |
|
||||
| FloatingIslands full detail | `0x6d842d2878b08ef6` | `0x4b868a3184f08035` | `0x2cedf2236788752d` | `0xeac17d5e0cb5044a` |
|
||||
| FloatingIslands whole view | `0x5be4d8cde79b2a76` | `0x96f75041f6861b3a` | `0x3700a516cc89d50c` | `0x6b99948d6eab4f21` |
|
||||
|
||||
Exact-query hashes also match: SurfaceWorld density `0xe4d4a8eb95b35933`, detail `0xefcba20c0c70c415`, collision `0xe604823a249029bf`; FloatingIslands density `0x61daded9c303becb`, detail `0xefcba20c0c70c415`, collision `0x84d37463be9b0573`.
|
||||
|
||||
## 6. Failed or retried runs
|
||||
|
||||
- One phase smoke omitted the explicit owner-data path and resolved it under `Standalone/bin/data`; reran with `Standalone/data/owner_world.json`.
|
||||
- A no-camera phase smoke produced 2,370 tiles instead of the 2,553-tile fixture; discarded it and reran with the fixed camera.
|
||||
- A malformed `--workers=w8` command exited during argument parsing; reran with numeric `--workers=8`.
|
||||
- The first FloatingIslands matrix invocation omitted the three required streaming-mode switches. All 14 attempts exited before generation; corrected the command and all 14 final runs passed.
|
||||
- The 24 m crossing attempt failed with `std::bad_alloc` as described above.
|
||||
- The first hash-audit parser expected 13 rows; the actual audit contains 12 scopes. Corrected the parser and all final comparisons passed.
|
||||
|
||||
At spawn, the first tile starts about 300 ms after cold start, with roughly 223 ms in full shape compilation and 51 ms in the single-threaded plan; this round did not make that wait shorter. While moving, the 16 m crossing completed 288 entering tiles in about 61 ms and repeated no shape compile, so the measured delay was tile work rather than region setup.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- **Not committed:** the L10 telemetry code. It is +1.0% whole-view median with tracing off (4 of 5
|
||||
pairs slower), and the rule is zero cost when off. Saved as `Saved/Linux/claude/l10-telemetry.patch`
|
||||
(gitignored) for reuse; the tree is reset to `7650abb`.
|
||||
- **Key finding, for L11:** the first SurfaceWorld tiles take the native `Heightfield2D` path and don't
|
||||
read the cave shape, yet `World::generateTile` waits for the full base shape (~223 ms). A per-tile
|
||||
readiness check (start tiles whose recipe path provably never touches shape arrays) is lever #1 in
|
||||
safe form.
|
||||
- **Open bug, for L11:** the 24 m crossing died with `std::bad_alloc` under the 6 GiB **address-space**
|
||||
cap (`RLIMIT_AS`). That cap counts reservations, not resident memory; L10 also reports 263.57 MiB
|
||||
of proxy address *capacity*. It is not yet known whether this is a real memory blow-up on larger
|
||||
crossings or the cap counting reserved address space. L7's 32 m step also hit the 160M-node
|
||||
interval bound.
|
||||
@@ -0,0 +1,74 @@
|
||||
# VoxelForge L11 — Linux report
|
||||
|
||||
## 24 m crossing crash
|
||||
|
||||
The crash is real. The original binary threw `std::bad_alloc` after base shape compilation with both 6 GiB and 16 GiB `RLIMIT_AS`; the 16 GiB run peaked at 476,128 KiB RSS and 1,542,284 KiB virtual size. The allocation trace identifies a 432,345,551,342,665,824-byte resize in `BuildSpatialBins`, reached from `BuildAnalyticRegion` during `World::compileTileRegion`. The crossing code had coalesced disjoint entering tiles into a large LOD rectangle and tried to allocate dense per-axis bins that the analytic fallback never reads. L10's 263.57 MiB figure is the proxies' reserved virtual capacity; RSS samples record resident usage separately.
|
||||
|
||||
Analytic regions now skip those dense bins and compile in world-aligned 128-voxel pages. Pages are built privately and published together; coordinate, `bad_alloc`, and size failures return errors before partial pages are published. On the final L11 binary, 24 m and 32 m both passed (16 pages each); 1,000 m passed with 2,260 entering keys, 72 pages, 63 ms region-build wall time, 700,232 KiB peak RSS, and 2,578,588 KiB peak virtual size. The fixed 24 m run peaked at 533,420 KiB RSS and 2,578,588 KiB virtual size. Crossing pages build no packed interval nodes (`lattice_nodes=0`) and do not expand the immutable base sign/proxy arrays; they add analytic primitive/bin data. `retainTileRegions` prunes pages outside the desired set. The largest empirically verified step is 1,000 m. Work follows the destination's entering tile set; once the old and new views are disjoint, its page count stays bounded by the active view as travel distance increases. Keys outside Core's signed 32-bit voxel range are rejected with `tile region coordinate is out of range` before page construction. The earlier L7 32 m / 160-million-node failure was the same unbounded region geometry.
|
||||
|
||||
| Step | Entering keys | Pages | Added interval nodes | Base proxy nodes / virtual capacity |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 24 m | 432 | 16 | 0 | 185,024 / 263.57 MiB |
|
||||
| 1,000 m | 2,260 | 72 | 0 | 185,024 / 263.57 MiB |
|
||||
|
||||
## Shape readiness
|
||||
|
||||
`isTileShapeFree` accepts only LOD0 tiles whose padded mesh box maps to one valid SurfaceWorld plan, excludes FloatingIslands, plan features, routes, passages, edits, and overlaps with owner shape primitives or detail bounds. This covers the MC skirt and sample margin. Higher LODs are conservatively shape-dependent; the accepted path cannot use far parents. The native-only context is initialized without building sign arrays, proxies, owner primitive vectors, or spatial bins. Native sampling and classification paths check the padded bounds, reject unsupported fallbacks, and count any attempted shape-dependent read; tile generation then fails instead of returning a partial mesh.
|
||||
|
||||
The SurfaceWorld spawn view proved 27/27 near-ring tiles shape-free. A deliberately mis-marked distant tile `(0,0,100000,lod0)` reached the sampler and was rejected by the guard. After shape publication, native-only and full-shape generation matched all four hashes for all 27 tiles. Shape-dependent tiles waited for the release-published shape; tile jobs continue through the existing Epoch and MPSC result path, and published shape/page data is immutable. The FloatingIslands spawn proof accepted 0/27 tiles, so all waited for its shape. Slow-shape delay defaults to zero; audit and negative-control work is opt-in.
|
||||
|
||||
## No-hash timing pairs
|
||||
|
||||
Five interleaved W12 pairs, camera `0,-35,-487.94`, seed 0, Clang `-ffp-contract=off`, hashing off. Times are from process start. The L9 executable does not emit a first-tile completion event and starts tile workers only after shape compilation; its first-tile cell is therefore unavailable.
|
||||
|
||||
| Pair | First tile L9 / L11 | Near ring L9 / L11 | Whole view L9 / L11 |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | — / 153 ms | 343 / 166 ms | 1,439 / 1,429 ms |
|
||||
| 2 | — / 160 ms | 424 / 173 ms | 1,526 / 1,447 ms |
|
||||
| 3 | — / 145 ms | 347 / 163 ms | 1,447 / 1,443 ms |
|
||||
| 4 | — / 146 ms | 353 / 170 ms | 1,457 / 1,449 ms |
|
||||
| 5 | — / 153 ms | 376 / 173 ms | 1,478 / 1,464 ms |
|
||||
| Median | — / 153 ms | 353 / 170 ms | 1,457 / 1,447 ms |
|
||||
|
||||
The delayed maximum-overlap W12 run held shape compilation for 1.5 s: first tile 133 ms, near ring 166 ms, shape ready 1.696 s. Shape-free and full-shape near-ring hashes matched, and whole-view, full-detail, per-LOD, and exact-query hashes matched the baseline.
|
||||
|
||||
## Hashes
|
||||
|
||||
All 28 configurations passed against L10's committed `b370908` baseline: SurfaceWorld and FloatingIslands, seed 0, W1/W8/W12, Clang `-ffp-contract=off` and GCC default, with five W12 repeats per compiler and recipe. Each run matched all 12 rows: full-detail, whole-view, nine LODs, and exact queries. The delayed-overlap run also matched.
|
||||
|
||||
| Recipe | Full-detail density / mesh / payload / topology | Whole-view density / mesh / payload / topology | Exact-query density & unclamped / detail / collision |
|
||||
|---|---|---|---|
|
||||
| SurfaceWorld | `0x0ee0579bd15cbbe4` / `0x32e5d4964d610fee` / `0x1932b96ee4b1d910` / `0x69743c6bad3447dc` | `0xc10697a8f48380c7` / `0xe75c9119a1498cd5` / `0x49a9a8d1835a6c8c` / `0xedcb23288b5e8724` | `0xe4d4a8eb95b35933` / `0xefcba20c0c70c415` / `0xe604823a249029bf` |
|
||||
| FloatingIslands | `0x6d842d2878b08ef6` / `0x4b868a3184f08035` / `0x2cedf2236788752d` / `0xeac17d5e0cb5044a` | `0x5be4d8cde79b2a76` / `0x96f75041f6861b3a` / `0x3700a516cc89d50c` / `0x6b99948d6eab4f21` | `0x61daded9c303becb` / `0xefcba20c0c70c415` / `0x84d37463be9b0573` |
|
||||
|
||||
The per-LOD D/M/P/T rows match exactly in every configuration; the complete comparison is recorded in `Saved/Linux/l11_hash_audit.txt`.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- The 24 m reproduction failed with `std::bad_alloc` under both address-space caps (`l11-cross24-as6g.log`, `l11-cross24-as16g.log`); the allocation trace above isolated the oversized bin resize. Final-binary 24 m, 32 m, and 1,000 m reruns passed (`l11-final-cross-24m.log`, `l11-final-cross-32m.log`, `l11-final-cross-1000m.log`).
|
||||
- The first readiness proof admitted 512 LOD0 keys (`l11-readiness-audit.log`, `l11-readiness-audit2.log`). Although the 27 near-ring equivalence check passed, distant owner-shape tiles changed the whole-view density hash. Tightening the proof to test owner primitive bounds reduced early admission to the 27 safe near-ring tiles and restored every hash (`l11-readiness-audit-final.log`).
|
||||
- One rebuilt audit invocation omitted the owner JSON path and exited 2 before generation (`l11-readiness-audit-path-error.log`; `Standalone/bin/data/owner_world.json` was missing). It was rerun with `Standalone/data/owner_world.json` and passed.
|
||||
- The required hash matrix and five timing pairs had no failed runs. All monitored run processes stayed below the 6,000,000,000-byte RSS ceiling; governor remained `powersave`.
|
||||
|
||||
The standalone Core/headless builds passed with Clang and GCC. The Unreal adapter change was source-reviewed but could not be built here because UnrealBuildTool/UnrealEditor are not installed on this Linux machine.
|
||||
|
||||
At spawn on SurfaceWorld, the near ring is ready at a 170 ms median versus 353 ms on L9, and the first L11 tile is ready at 153 ms median before shape publication; L9 gates tile work on shape compilation and does not report that milestone. For fast movement, destination-page work stays bounded through the tested 1 km crossing, while invalid Core coordinates return an error before allocation.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- **Re-run by me, 5 interleaved W12 pairs, no hashing.** Near ring (`neighborhood_ready`) L9
|
||||
0.338/0.338/0.337/0.326/0.334 s vs L11 0.159/0.159/0.167/0.169/0.159 s (median 0.337 → 0.159 s,
|
||||
−53%). Whole view 1.337 → 1.365 s median (+2.1%, 4 of 5 pairs slower): a small cost, accepted for a
|
||||
2× faster spawn ring. W12 hash runs give density `c10697a8f48380c7`.
|
||||
- **Crossings re-run by me on L11** (`--region-crossing --camera-step=S,0,0`): 24 m gives 432 tiles,
|
||||
0.867 s wall, 10.05 worker-s, rc 0; 1,000 m gives 2,260 tiles, 5.90 s wall, 70.4 worker-s, rc 0. The L9
|
||||
binary lacks the option, so the original crash rests on Luna's reproduction and allocation trace.
|
||||
- **New finding, for L12:** the 24 m crossing costs ~23 ms of work per tile, against ~2 ms for L10's 16 m
|
||||
crossing (288 tiles, 0.633 worker-s). Tiles outside the base shape bounds, served from analytic
|
||||
region pages, look about 10× more expensive. That is a moving-player cost.
|
||||
- **Concurrency read:** `World::compile` builds the shape privately, then sets `impl_->shape` and
|
||||
`buildRequest` before a release store of `shapeReady`. Every shape reader acquire-checks
|
||||
`shapeReady` first (short-circuit), and shape-free tiles use a private native-only context. Sound.
|
||||
- **Unreal:** `Source/VoxelForge/Private/VoxelWorld.cpp` changed (compile outside the writer lock;
|
||||
proven shape-free tiles start before `bReady`). It is NOT compiled here, so it must be built on the
|
||||
owner's PC before it is trusted in game.
|
||||
@@ -0,0 +1,62 @@
|
||||
# VoxelForge L12 — Linux report
|
||||
|
||||
Governor stayed `powersave`. Builds and runs used CPUs 0–11, at most 12 jobs, a 6 GiB address-space cap, and RSS monitors. Clang and GCC builds passed; peak sampled RSS was 881 MiB for one GCC compiler process and 679 MiB for the 1 km crossing. The GCC build process group peaked at 4.35 GiB. No Unreal adapter files changed.
|
||||
|
||||
## LOD cost and change
|
||||
|
||||
The 24 m profile is like-for-like at LOD1 in the same SurfaceWorld crossing. L11 generated 160 LOD1 tiles: 32 base-shape tiles averaged 2.73 ms each, while 128 analytic-page tiles averaged 71.72 ms. Of the LOD1 worker time, 9.06 of 9.27 seconds went to the marching-cubes density grid; the page compiler added 15 ms and built no packed lattice nodes. The page route lacked the manifest input needed for the exact LOD1 block proofs. Across all 160 LOD1 tiles, the sample counter reported 5.64 million density samples, or 35,271 per tile.
|
||||
|
||||
Analytic pages now retain the immutable shared manifest for block classification while continuing to sample the legacy density field. The proof audit checked 27,479 blocks and 36,574,549 samples, with zero violations. On the plain 24 m profile, page-backed LOD1 fell to 5.60 ms per tile; all 160 LOD1 tiles reported 432,000 samples (2,700 per tile) and 435 ms in density-grid work. Base LOD1 tiles averaged 2.51 ms, so page tiles are now about 2.2× their base cost. No base-shape bounds changed; pages remain privately built, immutably published, and bounded, with 16 pages at 24/32 m and 72 at 1 km.
|
||||
|
||||
## Plain W12 crossings
|
||||
|
||||
Worker time is the sum across tile workers; per-tile values use that sum. L11 values are from the L11 executable and logs. L12 values include region compilation in wall time.
|
||||
|
||||
| Step | Tiles | L11 wall / worker-s / ms per tile | L12 wall / worker-s / ms per tile |
|
||||
|---:|---:|---:|---:|
|
||||
| 8 m | 144 | 0.038 s / 0.381 / 2.65 | 0.035 s / 0.360 / 2.50 |
|
||||
| 16 m | 288 | 0.831 s / 9.476 / 32.90 | 0.125 s / 1.096 / 3.80 |
|
||||
| 24 m | 432 | 0.869 s / 9.912 / 22.95 | 0.164 s / 1.596 / 3.69 |
|
||||
| 32 m | 576 | 0.898 s / 10.261 / 17.81 | 0.172 s / 1.691 / 2.94 |
|
||||
| 1 km | 2,260 | 5.892 s / 69.534 / 30.77 | 1.247 s / 14.003 / 6.20 |
|
||||
|
||||
All five L12 steps passed; peak crossing RSS was 679 MiB. The 8 m L11 comparison is `l12-baseline-8m-page.log`; 16 m is `l12-baseline-16m-page.log`; 24 m, 32 m, and 1 km are the L11 final crossing logs.
|
||||
|
||||
## Spawn and hashes
|
||||
|
||||
Three plain W12 spawn runs had medians of 146 ms first tile, 161 ms near ring, and 1.361 s whole view. L11 medians were 153 ms, 170 ms, and 1.447 s.
|
||||
|
||||
All 28 requested L1 hash configurations matched L11: SurfaceWorld and FloatingIslands seed 0, Clang `-ffp-contract=off` and GCC default, W1/W8/W12, with five W12 repeats. Every run matched all 12 rows: full detail, whole view, nine LODs, and exact queries. Clang/GCC full-detail and whole-view D/M/P/T plus exact-query values are unchanged from the L11 report. The L3 FP-contract preamble/self-test remains in place.
|
||||
|
||||
| Recipe | Full-detail density / mesh / payload / topology | Whole-view density / mesh / payload / topology | Exact density+unclamped / detail / collision |
|
||||
|---|---|---|---|
|
||||
| SurfaceWorld | `0x0ee0579bd15cbbe4` / `0x32e5d4964d610fee` / `0x1932b96ee4b1d910` / `0x69743c6bad3447dc` | `0xc10697a8f48380c7` / `0xe75c9119a1498cd5` / `0x49a9a8d1835a6c8c` / `0xedcb23288b5e8724` | `0xe4d4a8eb95b35933` / `0xefcba20c0c70c415` / `0xe604823a249029bf` |
|
||||
| FloatingIslands | `0x6d842d2878b08ef6` / `0x4b868a3184f08035` / `0x2cedf2236788752d` / `0xeac17d5e0cb5044a` | `0x5be4d8cde79b2a76` / `0x96f75041f6861b3a` / `0x3700a516cc89d50c` / `0x6b99948d6eab4f21` | `0x61daded9c303becb` / `0xefcba20c0c70c415` / `0x84d37463be9b0573` |
|
||||
|
||||
The 24 m crossing hash also matches L11 at W1 and W12 (432 rows each): density `0x174588abdc8c0d2c`, mesh `0x4c401e27d9d05dea`, payload `0xeb10d9801c5ef125`, topology `0xf59d12304d0dcb4a`.
|
||||
|
||||
The route-verified base/page audit does **not** pass the requested exact-tile invariant. For `(0,-5,-31,lod1)`, the base density hash is `0xe0672dae7528f8d7` and page density hash is `0x882280f67bfddd77`; mesh, payload, and topology match. All 48 base-covered LOD1 candidates had density mismatches; 10 also differed in mesh/payload/topology. Across eight route-verified LOD0/LOD1 pairs, all eight density hashes differed. The base SurfaceWorld route uses manifest density while L11 pages use the legacy density evaluator; switching page samples to the base field would change the L11 crossing hashes. I kept the legacy page field to preserve the required crossing output, so this pre-existing parity conflict remains unresolved.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- An attempted 24 m base-shape expansion exited 2 at the 160-million-node bound (`l12-baseline-24m-base.log`). A separate per-tile native-only proof promotion failed with `region OOM` and was reverted (`l12-probe-cross24-profile-w12.log`).
|
||||
- The first plain step command passed `32` as the camera distance while labeling it 8 m; output identified it as 32 m. It was retained as the 32 m result, and 8 m was rerun with the correct distance.
|
||||
- The first FloatingIslands matrix omitted `--auto-surface-camera`, so it hashed the wrong 2,553-tile view. It was rerun with the L11 seed-0 island placement; all 28 corrected matrix configurations passed. The comparison helper also needed two small script fixes before it reported the final 28/28 result.
|
||||
- One base/page audit initially treated a LOD2 base-route fallback (`page_keys=0`) as a match. The audit now verifies the generated route; that apparent pass was rejected and the route-verified parity failure above is the final result.
|
||||
|
||||
Walking into new ground should feel much better: the plain 24 m crossing fell from 0.869 s to 0.164 s, and the 1 km crossing from 5.892 s to 1.247 s. Page-backed LOD1 tiles are down from 71.7 to 5.6 ms each while the L11 crossing hashes stay exact; base/page density-hash parity remains a separate unresolved constraint.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Re-run by me (W12, no hashing, 3 interleaved pairs): 24 m crossing L11 0.874/0.914/0.898 s vs L12
|
||||
0.154/0.166/0.166 s (median 0.898 → 0.166 s, 5.4×). Spawn is unchanged (near ring 0.160–0.184 s).
|
||||
The 24 m crossing hash (density `0x174588abdc8c0d2c`, mesh `0x4c401e27d9d05dea`) matches L11 at W1
|
||||
and W12. Hash runs refill skipped samples, so time crossings with hashing off.
|
||||
- **Soundness of page-tile skipping:** the proof audit figures (27,479 blocks / 36,574,549 samples)
|
||||
are the base-shape audit from L9, not page tiles. The evidence for pages is the crossing hash
|
||||
equalling L11 (which filled every sample). That is necessary but narrow: one crossing, one seed.
|
||||
- ⚠️ **Route-dependent output (a pre-existing multiplayer bug, now measured):** the same tile key
|
||||
gives different density from the base shape (manifest field) than from an analytic page (legacy
|
||||
field). All 48 base-covered LOD1 candidates differed in density, 10 also in mesh/payload/topology.
|
||||
A tile's geometry then depends on streaming history (spawned near it vs walked to it), which
|
||||
breaks peer determinism. Top priority for L13.
|
||||
@@ -0,0 +1,140 @@
|
||||
# VoxelForge M3 Linux13 — route-invariant density
|
||||
|
||||
## Field decision and fix
|
||||
|
||||
The approved reference is the Core base shape built from the reviewed owner manifest: `Heightfield2D`
|
||||
for SurfaceWorld and `FloatingIslandFieldV1` for FloatingIslands. L12's base route sampled that
|
||||
field. Its analytic pages instead fell through to the copied legacy owner `GetDensityAt` evaluator,
|
||||
so the same key could sample different values after the player crossed into a page. In the L12
|
||||
SurfaceWorld audit, all 48 base-covered LOD1 candidates had different density; 10 also differed in
|
||||
mesh, payload, and topology. A 214,375-sample L12 negative-control run measured max/mean density
|
||||
delta `0.076171875 / 0.0133467594`, 29 sign changes, 170 edge-crossing changes, and up to 1.7778
|
||||
voxels of surface displacement. The manifest field is the approved Core world and is the reference
|
||||
kept here.
|
||||
|
||||
Changing the page manifest flag alone did not fix FloatingIslands: 8/8 page pairs still failed,
|
||||
with max density delta `433.925781`, 262,676 sign changes, and 17,188 edge-crossing changes across
|
||||
343,000 samples. The source fix binds each compiled page to its world's base `FStandaloneShapeWorld`
|
||||
before publication. Page density, lattice, detail, classification, and solid queries now delegate
|
||||
to that one evaluator; the legacy page-local evaluator is no longer an independent output route.
|
||||
`WorldAssemblyEvaluatorVersion` is bumped from 5 to 6.
|
||||
|
||||
## Route parity
|
||||
|
||||
The L12 evaluator-v5 negative control fails as required: 5/5 selected base/page pairs mismatch. After the fix,
|
||||
four audits passed: SurfaceWorld seeds 0, 1, and 17, plus FloatingIslands seed 0.
|
||||
|
||||
| Compared routes | Pairs | Mismatches |
|
||||
|---|---:|---:|
|
||||
| Base vs page (28 clipmap-requested pages; 26 base-covered page controls) | 54 | 0 |
|
||||
| Base vs native-only shape-free route (SurfaceWorld LOD0) | 27 | 0 |
|
||||
| Edited base vs edited page | 4 | 0 |
|
||||
| Same key built in initial and moved-world history | 2 | 0 |
|
||||
| Tile samples vs exact Core queries at LOD2–8 | 56 | 0 |
|
||||
|
||||
All 87 tile-generation pairs match density, mesh, payload, and topology hashes. The 56 higher-LOD
|
||||
pairs compare the sampled tile lattice to direct `densityAt` / `densityAtLattice` queries; these
|
||||
also match all sampled densities and solid/air signs. The owner fixture has
|
||||
`full_res_clip_levels=2`, so analytic page routes exist only at LOD0–1. In total, page comparisons
|
||||
covered 2,315,250 density samples with zero delta, sign, edge-crossing, or surface-root differences;
|
||||
6,172,229 exact-query samples had zero mismatches. The audit exits nonzero on any output-hash,
|
||||
sample, route-verification, exact-query, or proof-violation mismatch.
|
||||
|
||||
The history probe builds the same LOD0 key in a second Core world after a 96 m camera move. Both
|
||||
SurfaceWorld and FloatingIslands match all four hashes across those two build histories. The edit
|
||||
probe applies a sequence-1 density offset centered on a sampled sign crossing; it changes the field,
|
||||
and edited base/page results still match.
|
||||
|
||||
### Page proof audit
|
||||
|
||||
The fill-and-check audit generated page tiles with full density capture and uniform-block proof
|
||||
verification, so it checked the field those pages actually sample. Across the four audits it checked
|
||||
1,274 blocks / 1,695,694 samples: 518 blocks used the exact fallback, 6 were unknown, and there were
|
||||
zero false proofs or violations. SurfaceWorld walkability passed 5,642/5,642, 6,080/6,080, and
|
||||
4,785/4,785 samples for seeds 0, 1, and 17; FloatingIslands passed 4,000/4,000. Shape readiness
|
||||
passed 27/27 near-ring tiles where eligible. FloatingIslands has no eligible native-only tiles.
|
||||
|
||||
## Cost, hashing off
|
||||
|
||||
W12, seed 0, `--force-generate`, camera `0,-35,-487.94`, with the same crossing setup as L12:
|
||||
|
||||
| Move | Entering tiles | L12 crossing | L13 crossing | L13 peak RSS |
|
||||
|---|---:|---:|---:|---:|
|
||||
| 8 m | 144 | 0.0347 s | 0.0399 s | 468 MiB |
|
||||
| 16 m | 288 | 0.1249 s | 0.0856 s | 513 MiB |
|
||||
| 24 m | 432 | 0.1635 s | 0.1242 s | 512 MiB |
|
||||
| 32 m | 576 | 0.1717 s | 0.1692 s | 511 MiB |
|
||||
| 1 km | 2,260 | 1.2474 s | 0.7696 s | 673 MiB |
|
||||
|
||||
Three W12 spawn runs had medians of 157 ms first tile, 180 ms near ring, and 1.499 s whole view;
|
||||
L12 medians were 146 ms, 161 ms, and 1.361 s. Each spawn run generated the same 2,553-tile
|
||||
benchmark view. The final timings are in `Saved/Linux/l13-crossing-final-*.log` and
|
||||
`Saved/Linux/l13-spawn-final-*.log`.
|
||||
|
||||
## Determinism and invariants
|
||||
|
||||
All 28 required configurations passed: SurfaceWorld and FloatingIslands seed 0; Clang
|
||||
`-ffp-contract=off` and GCC default; W1/W8/W12, with five W12 repeats per compiler and recipe. Each
|
||||
run matched all 12 rows (full detail, whole view, LOD0–8, exact queries) against L12/L11.
|
||||
|
||||
| Recipe | Full detail D/M/P/T | Whole view D/M/P/T | Exact density / unclamped / detail / collision |
|
||||
|---|---|---|---|
|
||||
| SurfaceWorld | `0x0ee0579bd15cbbe4 / 0x32e5d4964d610fee / 0x1932b96ee4b1d910 / 0x69743c6bad3447dc` | `0xc10697a8f48380c7 / 0xe75c9119a1498cd5 / 0x49a9a8d1835a6c8c / 0xedcb23288b5e8724` | `0xe4d4a8eb95b35933 / 0xe4d4a8eb95b35933 / 0xefcba20c0c70c415 / 0xe604823a249029bf` |
|
||||
| FloatingIslands | `0x6d842d2878b08ef6 / 0x4b868a3184f08035 / 0x2cedf2236788752d / 0xeac17d5e0cb5044a` | `0x5be4d8cde79b2a76 / 0x96f75041f6861b3a / 0x3700a516cc89d50c / 0x6b99948d6eab4f21` | `0x61daded9c303becb / 0x61daded9c303becb / 0xefcba20c0c70c415 / 0x84d37463be9b0573` |
|
||||
|
||||
The crossing output changes as intended. The 24 m W1 and W12 moved-tile hashes match each other:
|
||||
`0x337ca62131576728 / 0x7612f48e2ddbbaab / 0x69a67ed4add83b87 / 0xf11ac1ebd0f7ddb8` (D/M/P/T),
|
||||
replacing L12's `0x174588abdc8c0d2c / 0x4c401e27d9d05dea / 0xeb10d9801c5ef125 / 0xf59d12304d0dcb4a`.
|
||||
|
||||
The L3 FP-contract preamble and runtime self-test, L4 `FLazy`, L7 shared column cache, L9/L12
|
||||
proofs, and L11 native-only readiness/crash fixes were preserved. The L6 screen-space controls
|
||||
still pass their six same-LOD checks and reject the missing/floating-surface, global-octave, and
|
||||
stride-2 negative controls as required. Its whole gate still rejects the known far-field error over
|
||||
the 1 px bound (exit 6); the route binding does not affect those LOD3–8 base-field comparisons.
|
||||
The asynchronous MPSC/Epoch path and `ARCHITECTURE.md` §8.10 invariants are unchanged. No Unreal
|
||||
adapter files were touched; the Core exact-query and collision-sign routes were checked, but Unreal
|
||||
collision cooking was not built here. `ARCHITECTURE.md` §8.36 and `CODEMAP.md` §3.22 document the new
|
||||
route law.
|
||||
|
||||
Builds and runs used CPUs 0–11, at most 12 workers, a 6 GiB address-space cap, and a 6,000,000,000
|
||||
byte RSS monitor. The governor remained `powersave`. Peak sampled RSS was 904 MiB across runs
|
||||
(673 MiB in the no-hash cost table); the GCC build process group peaked at 4.27 GiB. Clang and GCC
|
||||
builds passed. No system changes or Unreal builds were made.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- `l13-route-baseline-s0-w12.log` exited 2 because the inferred binary-relative owner JSON path did
|
||||
not exist; reruns used the explicit repository JSON. The first baseline capture used the wrong
|
||||
camera, then an audit pass had empty proof-skipped density captures; the final L12 negative
|
||||
control is `l13-route-baseline-s0-w12-r4.log`.
|
||||
- `l13-route-fi-diagnostic.log` and `l13-route-fi-diagnostic-r2.log` intentionally exposed the
|
||||
remaining FloatingIslands split before the shared-source binding; both exit 9 on 8/8 mismatches.
|
||||
- `l13-route-edit-s0.log` exited 9 because its brush did not change the selected tile. Centering the
|
||||
brush on a sampled sign crossing fixed the audit in `l13-route-edit-s0-r2.log`.
|
||||
- `l13-route-history-sw-s0.log` exited 9 because the second-world probe exceeded the 4,096-node
|
||||
region compiler envelope. The retry compiles one matching LOD0 anchor tile and passes.
|
||||
- The first `l13-cost-*` measurements omitted `--force-generate` and used the owner JSON start;
|
||||
those are not used above. The final crossing and spawn runs use L12's camera and force-generation
|
||||
setting. The screen-space run's exit 6 is its expected far-field rejection, not a retried run.
|
||||
- All final builds, the 28 hash runs, eight no-hash cost runs, and two crossing-hash runs completed
|
||||
within their limits. No commit, push, or stash was made.
|
||||
|
||||
## Multiplayer note for the owner
|
||||
|
||||
Peers with the same manifest, seed, evaluator version 6, and tile key now reconstruct the same Core
|
||||
density and tile output through each available base, page, or native-only route, regardless of
|
||||
arrival history; terrain geometry still does not need to cross the network. Player edits remain an
|
||||
explicit versioned diff that must be ordered and synchronized between peers.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- **Spawn cost re-run by me, 5 interleaved W12 pairs, no hashing:** whole view L12
|
||||
1.385/1.405/1.419/1.443/1.460 s vs L13 1.401/1.448/1.430/1.435/1.451 s (median 1.419 → 1.435 s,
|
||||
+1.1%, within noise); near ring 0.168 → 0.171 s. The +10% in the cost table above compares L13 runs
|
||||
with L12 numbers from another session; interleaved, there is no measurable spawn regression.
|
||||
- The fix is structural: pages are bound to their world's single base `FStandaloneShapeWorld`
|
||||
evaluator before publication, so routes can't diverge by construction. The negative control fails
|
||||
on L12 (5/5), and the 87 tile pairs, 56 exact-query pairs and 2.3M page samples show 0 mismatches
|
||||
after. The page lifetime is safe because the base shape is owned by the `World` and outlives its pages.
|
||||
- Crossing output changed, as intended (evaluator version 5 → 6); the spawn and island worlds are
|
||||
unchanged.
|
||||
@@ -0,0 +1,157 @@
|
||||
# Linux14 — far spawn work
|
||||
|
||||
No mesh or density code changed. The source changes add opt-in profiling for far-shell column
|
||||
lookups; counter snapshots are behind the existing profile guard, with no added timing or counter
|
||||
increments when profiling is off. The final plain build has the same output as L13.
|
||||
|
||||
## Far LOD cost and ranking
|
||||
|
||||
The first table is from five interleaved, W12, hashing-off pairs: L13 binary before and L14 binary
|
||||
after. Values are median worker-sums across that LOD; per-tile time is the L14 median divided by
|
||||
tile count. The ranking is L6, L8, L4, L5, L7 on both binaries.
|
||||
|
||||
| LOD | Tiles | L13 → L14 median worker-s (s) | L14 worker-ms/tile |
|
||||
|---:|---:|---:|---:|
|
||||
| 4 | 320 | 1.600 → 1.572 | 4.91 |
|
||||
| 5 | 144 | 1.421 → 1.411 | 9.80 |
|
||||
| 6 | 96 | 2.624 → 2.585 | 26.93 |
|
||||
| 7 | 48 | 1.152 → 1.158 | 24.13 |
|
||||
| 8 | 153 | 2.044 → 2.046 | 13.37 |
|
||||
|
||||
Phase detail below is one W12 `--profile-lod-work` run on the final Clang binary. Cache counters
|
||||
include all column work for that LOD, not just refinement. A “shared hit” is a hit from the
|
||||
world-shared cache; formula evaluations equal local misses minus shared hits.
|
||||
|
||||
| LOD | Calls | Local hits / misses | Shared hits | Formula evaluations (per tile) | Column compute (ms) | MC non-column (ms) | Ground/cap non-column (ms) | Connector shell non-column (ms) | Skirt (ms) | Assembly (ms) |
|
||||
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 4 | 1,845,787 | 1,579,868 / 265,919 | 175,805 | 90,114 (282) | 521 | 472 | 385 | 0 | 186 | 3 |
|
||||
| 5 | 877,542 | 701,124 / 176,418 | 105,477 | 70,941 (493) | 408 | 135 | 705 | 0 | 159 | 2 |
|
||||
| 6 | 760,952 | 418,443 / 342,509 | 63,209 | 279,300 (2,909) | 1,594 | 90 | 474 | 210 | 158 | 2 |
|
||||
| 7 | 337,328 | 257,969 / 79,359 | 8,878 | 70,481 (1,468) | 395 | 59 | 514 | 0 | 145 | 4 |
|
||||
| 8 | 644,565 | 508,799 / 135,766 | 29,545 | 106,221 (694) | 608 | 374 | 783 | 0 | 278 | 6 |
|
||||
|
||||
MC and refinement column-compute / non-column times (ms) were L4 `221/472` MC and `281/385`
|
||||
ground/cap; L5 `199/135` MC and `209/705` ground/cap; L6 `196/90` MC and `205/474` ground/cap;
|
||||
L7 `184/59` MC and `211/514` ground/cap; L8 `413/374` MC and `195/783` ground/cap. The column
|
||||
compute column overlaps those column portions. Timers are worker sums and may differ by a few ms
|
||||
from their subphase sums.
|
||||
|
||||
Stratum shells made zero field calls at LOD4–8 and took under 0.03 ms total. The profiler run
|
||||
measured about 1.408 s whole-view-ready; the unprofiled interleaved runs below are the timing result.
|
||||
|
||||
## Connector-shell repair
|
||||
|
||||
`AppendFarConnectorShells` is guarded to LOD6 and above. Coarse marching cubes can leave gaps in a
|
||||
route connector wall, so the repair checks four axial rays at connector stations and adds only the
|
||||
panels where the nearest coarse-mesh hit misses the source-field radius tolerance. LOD6 checks at
|
||||
128-voxel stations; LOD7–8 use 256-voxel stations. Only LOD6 had repair candidates in this spawn
|
||||
view: 5,066 triangles were added. The shell took 1.403 worker-s, including 1.193 s of column
|
||||
evaluation (about 0.210 s shell work outside those evaluations).
|
||||
|
||||
The LOD6 repair made 251,475 source/column calls: 44,596 worker-local hits, 206,879 local misses,
|
||||
30 shared-cache hits, and 206,849 formula evaluations. Thus 82.3% of calls evaluated a formula;
|
||||
the work is mostly uncached. A cache miss can include a point evicted by the four-way worker cache,
|
||||
so these counters do not prove every evaluated XY is unique. The L13-bound base evaluator already
|
||||
feeds the shared exact-XY cache; neighboring LOD samples mostly use different XY coordinates, and
|
||||
only 30 repair samples hit the shared cache. Reducing stations or rays would change the repair
|
||||
geometry, and no bit-identical reduction was proved.
|
||||
|
||||
## Ground and cap refinement
|
||||
|
||||
Ground refinement starts at LOD5 (32-voxel grid there, 64 at LOD6, then 128 at LOD7–8). Cap
|
||||
refinement starts at LOD4 (16-voxel grid there, then 128 at LOD5–7; LOD8 may use its configured
|
||||
interior override). Both query exact `Heightfield2D` columns and use those values for heights,
|
||||
normals, materials, clipping, and replacing nearby coarse faces. The existing shared cache is keyed
|
||||
by recipe/plan and exact XY, so aligned samples can already be reused across tiles and levels. The
|
||||
LOD-wide shared-hit counts are in the table above; they are not refinement-only counts.
|
||||
|
||||
Current safe skips are the plan Z-range check for ground and the cap-height-bound check for caps.
|
||||
No additional tile/cell skip was shown to preserve every sample that affects output; a grid point
|
||||
can also supply a neighboring normal or clipped cell. No new refinement sharing or skip was built.
|
||||
|
||||
## Ranked exact-output work
|
||||
|
||||
No exact-output change with a measured saving above 3% was proved, so no geometry optimization was
|
||||
built. The ranked work buckets are:
|
||||
|
||||
1. Ground/cap refinement across LOD4–8: about 3.95 worker-s including its column work. Removing all
|
||||
of it would be an unrealistic idealized 23% of 1.419 s at 12-way division; the sampled values
|
||||
currently affect geometry/materials/normals.
|
||||
2. LOD6 connector repair: 1.403 worker-s inclusive, an idealized upper bound near 8.2% at 12-way
|
||||
division. Most calls are uncached, and the missing panels are required for the current mesh.
|
||||
3. Far-LOD column formula work: 3.53 worker-s across LOD4–8, overlapping the two buckets above.
|
||||
Exact-XY sharing is already active; no redundant set large enough to remove was demonstrated.
|
||||
4. Marching-cubes non-column work is 1.13 worker-s and skirts 0.93 worker-s. Assembly is only
|
||||
0.017 worker-s; stratum shells are negligible. No exact reduction was established.
|
||||
|
||||
These idealized conversions divide worker-s by 12 and are not predicted wall-time gains. The L6
|
||||
screen-space gate was not used because this round made no geometry change.
|
||||
|
||||
## Interleaved cost, builds, and hashes
|
||||
|
||||
Five hashing-off W12 pairs ran in one session, alternating
|
||||
`Standalone/bin/linux/VoxelForgeCoreHeadless-linux13-clang` and
|
||||
`Standalone/bin/linux/VoxelForgeCoreHeadless-linux14-clang`. Every run generated 2,553 tiles at
|
||||
seed 0 and camera `0,-35,-487.94,0,-22`.
|
||||
|
||||
| Pair | L13 whole-view-ready (s) | L14 whole-view-ready (s) |
|
||||
|---:|---:|---:|
|
||||
| 1 | 1.416 | 1.410 |
|
||||
| 2 | 1.436 | 1.388 |
|
||||
| 3 | 1.416 | 1.424 |
|
||||
| 4 | 1.429 | 1.419 |
|
||||
| 5 | 1.430 | 1.426 |
|
||||
|
||||
Median was 1.429 s before and 1.419 s after (−0.7%, within this run spread). The telemetry is
|
||||
opt-in; the plain run shows no measurable cost when off.
|
||||
|
||||
Final binaries `Standalone/bin/linux/VoxelForgeCoreHeadless-linux14-clang` and
|
||||
`Standalone/bin/linux/VoxelForgeCoreHeadless-linux14-gcc` were rebuilt with `Standalone/BuildLinux.sh`,
|
||||
`-j12`, CPUs 0–11, and a 6 GiB address-space cap: Clang 19 with FP contraction off and GCC 14 with
|
||||
default contraction. Builds passed. Runtime peak RSS was 926,520 KiB; largest individual
|
||||
build-process RSS was 901,968 KiB (GCC), with a 4,557,196 KiB process-group peak. Governor remained
|
||||
`powersave`.
|
||||
|
||||
The final hash matrix passed 28/28: SurfaceWorld seed 0 and FloatingIslands seed 0, Clang/GCC,
|
||||
W1/W8/W12, with five W12 repeats per recipe/compiler. Full-detail, whole-view, all nine per-LOD,
|
||||
and exact-query rows match L13 exactly. Route parity also passed for SurfaceWorld seeds 0,
|
||||
1, and 17 and FloatingIslands seed 0: page/native-only routes, edits, history, higher-LOD exact
|
||||
queries, and proof checks had zero mismatches. The final 24 m crossing hash matched at W1/W12 on
|
||||
both compilers: `0x337ca62131576728 / 0x7612f48e2ddbbaab / 0x69a67ed4add83b87 / 0xf11ac1ebd0f7ddb8`
|
||||
(density/mesh/payload/topology).
|
||||
|
||||
| Recipe | Full-detail D/M/P/T | Whole-view D/M/P/T | Exact density / unclamped / detail / collision |
|
||||
|---|---|---|---|
|
||||
| SurfaceWorld | `0x0ee0579bd15cbbe4 / 0x32e5d4964d610fee / 0x1932b96ee4b1d910 / 0x69743c6bad3447dc` | `0xc10697a8f48380c7 / 0xe75c9119a1498cd5 / 0x49a9a8d1835a6c8c / 0xedcb23288b5e8724` | `0xe4d4a8eb95b35933 / 0xe4d4a8eb95b35933 / 0xefcba20c0c70c415 / 0xe604823a249029bf` |
|
||||
| FloatingIslands | `0x6d842d2878b08ef6 / 0x4b868a3184f08035 / 0x2cedf2236788752d / 0xeac17d5e0cb5044a` | `0x5be4d8cde79b2a76 / 0x96f75041f6861b3a / 0x3700a516cc89d50c / 0x6b99948d6eab4f21` | `0x61daded9c303becb / 0x61daded9c303becb / 0xefcba20c0c70c415 / 0x84d37463be9b0573` |
|
||||
|
||||
Hash logs and the 28-row comparator are in `Saved/Linux/l14-final-hash-*.log` and
|
||||
`Saved/Linux/l14-final-hash-audit.txt`. Final route logs are `Saved/Linux/l14-final-route-*.log`;
|
||||
crossing and pair logs use the `l14-final-crossing-*` and `l14-final-cost-*` names.
|
||||
|
||||
## Failed or retried runs
|
||||
|
||||
- The first SurfaceWorld hash matrix and first crossing checks omitted the spawn camera and used the
|
||||
headless origin. Their comparisons differed from L13; all 14 SurfaceWorld configurations and all
|
||||
four crossing configurations were rerun with the spawn camera and passed. FloatingIslands used
|
||||
its auto-surface camera and matched on the first run.
|
||||
- The first route probes also used the wrong camera selection. A later seed-1 probe used the fixed
|
||||
seed-0 camera, exited 9 on two LOD2 exact-query checks, and was rerun with `--auto-surface-camera`;
|
||||
the corrected seed-1 and seed-17 audits passed. The origin-camera route probes returned success
|
||||
but were not used as reference results.
|
||||
- Clang was rebuilt after the connector counters were added and again after folding aggregation
|
||||
into the existing opt-in guard. Both Clang builds and the final GCC build succeeded. No run hit
|
||||
the RSS, 15-minute stuck-step, or 30-minute limits.
|
||||
|
||||
Owner note: the highest measured far cost is still LOD6 connector repair, but its formula work is
|
||||
mostly uncached and its panels preserve the coarse route wall. The final interleaved plain runs show
|
||||
no measurable diagnostic overhead; no geometry change or L6 budget claim is made.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Accepted as a counting round. Interleaved W12, no hashing: L13 1.429 s vs L14 1.419 s median, so the
|
||||
opt-in counters cost nothing measurable when off; committed with the report.
|
||||
- Conclusion: exact-output far work has no remaining lever above 3%. The big buckets
|
||||
(ground/cap refinement ~3.95 worker-s, LOD6 connector repair ~1.40 worker-s) produce visible
|
||||
geometry. Further far savings need geometry changes, judged against today's same-LOD mesh within
|
||||
the owner's budget (1 px at 1440p, at the LOD's closest display distance, display only).
|
||||
@@ -0,0 +1,195 @@
|
||||
# Linux15 report
|
||||
|
||||
## Result
|
||||
|
||||
Null result. No far-mesh shortcut passed the full 1 px, shading, swap, and topology gates, so no
|
||||
recipe enables one by default. Evaluator version 6→7 adds the optional `far_display_mesh` declaration;
|
||||
omitting it keeps the L14 output path. The controls are in the shared Core layer and do not touch
|
||||
LOD0–2, exact queries, collision, edits, or server state.
|
||||
|
||||
Candidates measured:
|
||||
|
||||
- Refinement grid scale ×2 at LOD4, 5, 6, 7, and 8. The all-tile LOD4 and LOD5 runs reject; the
|
||||
remaining per-LOD probes exceed 1 px or change shading, and the LOD6–8 candidates worsen at
|
||||
least one adjacent swap.
|
||||
- Reusing connector station radii leaves geometry p99 at 1.07 px but reaches 15.23 px maximum;
|
||||
shading delta reaches 0.667 and the normal changes by 73.3°.
|
||||
- Coarsening refinement at LOD4–8 is the largest measured saving, but exceeds the display budget.
|
||||
Halving connector station density also saves time but opens gaps. Removing
|
||||
any one ray direction also opens gaps, and the removed-panel control finds 99 new holes.
|
||||
|
||||
## Screen-space gate
|
||||
|
||||
The gate samples 65×65 surface grids across all 1,145 displayed LOD3–8 tiles (5,208,134 samples;
|
||||
skirts excluded). Geometry is projected normal displacement against today's same-LOD mesh at its
|
||||
closest display distance, using 90° horizontal FOV and 2,560 px (0.000613592 rad/px). Shading delta
|
||||
is the maximum of fixed-light normalized Lambert change and material selector/blend-channel change;
|
||||
the limit is 1/255. Each number below is p50/p99/max. Exact-path baseline values are all zero.
|
||||
The gate rejects geometry max above 1 px, shading max above 1/255, any worse per-sample swap,
|
||||
missing surface, or new hole/crack/floater. Its own validation fails if S13, stride-2, missing
|
||||
surface, floating surface, missing fine-ring, or removed-panel controls are not rejected; the
|
||||
panel control must expose at least one new connector hole.
|
||||
|
||||
| LOD | Tiles | Geometry px: baseline → refine×2 all | Shading delta: baseline → refine×2 all | Refine×2 holes / cracks / floating |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 3 | 384 | 0/0/0 → 0/0/0 | 0/0/0 → 0/0/0 | 0 / 0 / 0 |
|
||||
| 4 | 320 | 0/0/0 → 0/2.370/606.562 | 0/0/0 → 0/0.023529/1 | 254 / 400 / 254 |
|
||||
| 5 | 144 | 0/0/0 → 0/12.728/95.453 | 0/0/0 → 0/1/1 | 1,183 / 76 / 138 |
|
||||
| 6 | 96 | 0/0/0 → 0.023/13.408/799.596 | 0/0/0 → 0/1/1 | 476 / 0 / 201 |
|
||||
| 7 | 48 | 0/0/0 → 0/13.242/248.905 | 0/0/0 → 0/1/1 | 30 / 0 / 1 |
|
||||
| 8 | 153 | 0/0/0 → 0.158/19.250/187.465 | 0/0/0 → 0.003922/1/1 | 45 / 0 / 946 |
|
||||
|
||||
The selected-patch swap report compares each baseline with refine×2 all. Values are p50/p99/max px.
|
||||
|
||||
| Swap (distance) | Before | After refine×2 all |
|
||||
|---|---:|---:|
|
||||
| 3→2 (96 m) | 0.611/3.767/6.045 | 0.611/3.767/6.045 |
|
||||
| 4→3 (192 m) | 0.752/4.630/7.513 | 0.752/4.630/7.513 |
|
||||
| 5→4 (384 m) | 0/9.580/31.783 | 2.435/32.321/86.615 |
|
||||
| 6→5 (768 m) | 0/9.754/70.340 | 0.343/19.142/78.077 |
|
||||
| 7→6 (1,536 m) | 0/9.040/42.355 | 0.200/15.109/50.994 |
|
||||
| 8→7 (3,072 m) | 0.826/18.030/47.182 | 1.528/24.547/64.449 |
|
||||
|
||||
The station×2 LOD6 screen scan measures 0/2/813.571 px, shading 0/0/1, with 1,349 holes and 1,690
|
||||
floaters. Its connector audit counts missing ray stations by stable tile/ray/station ID: holes 8→221
|
||||
(216 new), floating classifications 464→405 (111 new IDs). In the refine×2 all row above, the
|
||||
screen scan reports those all-tile holes, cracks, and floaters. Station×2 leaves all six selected-patch
|
||||
swap p50/p99/max rows at baseline values, including 6→5 at 0/9.754/70.340 px. All four ray directions are required:
|
||||
the four one-ray-removed
|
||||
controls create 1,008, 1,022, 1,171, and 1,158 new holes. The explicit panel-removal control creates
|
||||
99 new holes. The exact-path audit has zero new holes, cracks, or floating pieces; its raw connector
|
||||
audit counts remain 8 holes and 464 pre-existing floating classifications.
|
||||
|
||||
The LOD4×2 and LOD5×2 candidates also fail in isolation. Reusing station radii misses the LOD6
|
||||
geometry and shading limits. The four one-ray-removed masks add 1,008, 1,022, 1,171, and 1,158
|
||||
connector holes, respectively.
|
||||
|
||||
## Negative controls
|
||||
|
||||
- The S13 global-octave-drop and stride-2 column-interpolation controls are rejected at every LOD3–8.
|
||||
- Missing displayed surface, a floating surface, and missing fine-ring surface controls are rejected.
|
||||
- Removing connector panels is rejected with 99 new holes. The exact connector path has zero new
|
||||
station IDs classified as holes, cracks, or floaters.
|
||||
|
||||
## Speed
|
||||
|
||||
Five interleaved W12 pairs per candidate, hashing off, same 2,553-tile L1 view.
|
||||
`whole_view_ready` is the metric. Both candidates fail the quality gate.
|
||||
|
||||
Station×2 connector stations:
|
||||
|
||||
| Pair | L14 baseline | Station×2 | Change |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | 1.320 s | 1.304 s | −1.2% |
|
||||
| 2 | 1.363 s | 1.330 s | −2.4% |
|
||||
| 3 | 1.371 s | 1.332 s | −2.8% |
|
||||
| 4 | 1.393 s | 1.341 s | −3.7% |
|
||||
| 5 | 1.391 s | 1.345 s | −3.3% |
|
||||
| Median | 1.371 s | 1.332 s | −2.8% (3.3% median paired gain) |
|
||||
|
||||
Refinement grid scale ×2 at LOD4–8:
|
||||
|
||||
| Pair | L14 baseline | Refine×2 all | Change |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | 1.305 s | 1.164 s | −10.8% |
|
||||
| 2 | 1.336 s | 1.177 s | −11.9% |
|
||||
| 3 | 1.362 s | 1.169 s | −14.2% |
|
||||
| 4 | 1.378 s | 1.178 s | −14.5% |
|
||||
| 5 | 1.384 s | 1.190 s | −14.0% |
|
||||
| Median | 1.362 s | 1.177 s | −13.6% (14.0% median paired gain) |
|
||||
|
||||
## Hashes and invariants
|
||||
|
||||
The undeclared/default recipe path matches L14. LOD0–2 density/mesh/payload/topology hashes:
|
||||
|
||||
| LOD | Density | Mesh | Payload | Topology |
|
||||
|---:|---|---|---|---|
|
||||
| 0 | `769f2a706d585a72` | `33eeeb39fa443816` | `645621a1d968983f` | `c2457ed905036c9d` |
|
||||
| 1 | `69cc5a5c0a2c60af` | `b48335f5410c57d5` | `d15c5480162ab8ba` | `feca1bd025823604` |
|
||||
| 2 | `85deed5683a77202` | `53469895987aeea2` | `e55f8ae7e1532033` | `d5feb12a0e85c09b` |
|
||||
|
||||
Exact queries (density / unclamped / detail / collision) remain
|
||||
`e4d4a8eb95b35933 / e4d4a8eb95b35933 / efcba20c0c70c415 / e604823a249029bf`. The 24 m crossing's
|
||||
432 moved tiles match L14 at W1/W12 with Clang off and GCC default:
|
||||
`337ca62131576728 / 7612f48e2ddbbaab / 69a67ed4add83b87 / f11ac1ebd0f7ddb8` (D/M/P/T).
|
||||
Its LOD0–2 per-LOD hashes also match the three rows above.
|
||||
|
||||
Determinism passed 28/28 configurations: SurfaceWorld seed 0 and FloatingIslands seed 0, Clang 19
|
||||
FP-contract off and GCC 14 default, W1/W8/W12 and five W12 repeats. All per-LOD and whole-view
|
||||
hashes match L14. L13 route parity passed for SurfaceWorld seeds 0, 1, and 17 and FloatingIslands
|
||||
seed 0; page, native-only, higher-LOD, edit, and history mismatch counts are zero.
|
||||
|
||||
The L3 FP-contract preamble/self-test, L4 `FLazy`, L7 shared cache, L9/L12 proofs, L11 readiness and
|
||||
crash fix, L13 route binding, and CLAUDE.md async rules remain in place. The evaluator and optional
|
||||
recipe declaration are shared by all recipes. Builds and runs used CPUs 0–11, up to W12, normal
|
||||
priority, 6 GiB address-space limits, and the unchanged `powersave` governor. Peak RSS was 904,400 KB
|
||||
(GCC build); peak runtime RSS was 736,432 KB. No run exceeded the time or memory limits.
|
||||
|
||||
## Capture pairs
|
||||
|
||||
JPEGs are 1280×720 and each is under 74 KB. Before is L14; after is the rejected refinement×2
|
||||
candidate applied at LOD4–8.
|
||||
The L1 view uses `(0,-35,-487.94) m`, yaw 0°, pitch −22°. Horizon uses the same location at pitch −2°.
|
||||
The mesa frame aims from the L1 location at the L6 mesa band (`(-472,-1432,-463.151) m` in the
|
||||
gate) with yaw −161° and pitch 0.9°.
|
||||
|
||||
| View | Before | After refine×2 all |
|
||||
|---|---|---|
|
||||
| L1 camera | [l1-camera-before.jpg](captures/l1-camera-before.jpg) (59 KB) | [l1-camera-after-refineall.jpg](captures/l1-camera-after-refineall.jpg) (57 KB) |
|
||||
| Horizon | [horizon-before.jpg](captures/horizon-before.jpg) (63 KB) | [horizon-after-refineall.jpg](captures/horizon-after-refineall.jpg) (60 KB) |
|
||||
| LOD6 mesa rim | [lod6-mesa-rim-before.jpg](captures/lod6-mesa-rim-before.jpg) (74 KB) | [lod6-mesa-rim-after-refineall.jpg](captures/lod6-mesa-rim-after-refineall.jpg) (67 KB) |
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- The first Clang build wrapper exited 127 because `/usr/bin/time` is absent; the direct Clang
|
||||
`-ffp-contract=off` build succeeded. GCC 14 default also built successfully.
|
||||
- A later sequential-build monitor was interrupted after Clang completed and before its GCC step
|
||||
launched. Clang and GCC were rebuilt separately with the same compiler settings and 6 GiB cap.
|
||||
- The first hash matrix omitted `--seed=0` and was discarded. A second FloatingIslands matrix used
|
||||
the SurfaceWorld spawn camera; it was rerun with the L14 auto-surface camera. The corrected matrix
|
||||
passed all 28 configurations.
|
||||
- Initial SurfaceWorld route probes used no camera for seed 0 (the 2,370-tile bound), omitted
|
||||
`--region-crossing` for seed 0 (shape compiler safety bound), and used the fixed seed-0 camera for
|
||||
seed 1 (LOD2/exact-query parity mismatch; seed 17 was not run with that setup). They were rerun
|
||||
with L14's camera selection and region-crossing option; all four final route audits passed. The
|
||||
first 24 m crossing also omitted `--region-crossing` and exceeded the shape-build safety bound;
|
||||
all four corrected compiler/worker combinations passed.
|
||||
- The first connector audit compared aggregate counts. It was replaced with stable per-ray station
|
||||
ID comparison before the final gate. The initial L4 selected-tile gate missed a changed tile; the
|
||||
all-desired-tile coverage gate found and rejected it. The full-view gate is used in the table above.
|
||||
- The first JPEG encoding pass did not set CPU affinity or an address-space cap. All six files were
|
||||
re-encoded under CPUs 0–11 and the 6 GiB cap; sizes and pixels stayed the same.
|
||||
- The first five speed pairs included screen-gate color capture on ordinary streaming tiles. That
|
||||
diagnostic copy is now enabled only for gate runs; all five cost pairs were rerun and the table
|
||||
above uses only the final logs. The final exact gate passed and station×2 remained rejected.
|
||||
- No builds, captures, final audits, or cost runs timed out or breached their memory limits. The
|
||||
intentional candidate rejections are listed above, not treated as infrastructure failures.
|
||||
|
||||
Evidence logs are in `Saved/Linux`: the final baseline and station×2 all-tile gates are
|
||||
`l15-final-gatecolors-exact.log` and `l15-final-gatecolors-station2.log`; the best-payoff
|
||||
refinement×2 all-tile gate is `l15-final-gatecolors-refineall.log`. Single-LOD refinement coverage is in
|
||||
`l15-gate-alltiles-refine-l4x2.log` and `l15-gate-alltiles-refine-l5x2.log`. Single-LOD refinement
|
||||
and connector-ray probes are `l15-gate-refine-l{x}x2.log`, `l15-gate-reuse.log`, and
|
||||
`l15-gate-rays{14,13,11,7}.log`. Hash, route, and crossing evidence is in
|
||||
`l15-final-hash-audit.txt`, `l15-final-route-*.log`, and `l15-final-crossing-24m-*.log`; final
|
||||
station×2 speed pairs are `l15-final-speed-pair{1..5}-{before,after}.log`; refinement×2 pairs are
|
||||
`l15-final-speed-refineall-pair{1..5}-{before,after}.log`.
|
||||
|
||||
## Note for the owner
|
||||
|
||||
The default far terrain stays the same because every measured reduction exceeded the 1 px geometry
|
||||
or shading limit, worsened a swap, or opened holes. Refinement×2 across LOD4–8 saves 13.6% by the
|
||||
median whole-view ratio, but creates hundreds-pixel errors and visible gaps, so the captures show it
|
||||
for review only and it is not enabled.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- I looked at the LOD6 mesa-rim pair. After refine×2, the jagged crests are visibly smoothed, black
|
||||
holes appear in the cap/ceiling, and a dark sliver shows on the left horizon. That confirms the
|
||||
gate's rejection: the instrument is right this time, not the world.
|
||||
- **Not merged into `assembler/world`:** the candidate knobs and the evaluator 6→7 bump add no default
|
||||
output and would invalidate version stamps for nothing. The full L15 code (the extended gate with
|
||||
shading, swap and connector-ID audits, and the candidate controls) is kept on side branch
|
||||
`wip/l15-far-budget`. `assembler/world` stays at L14 code plus this report.
|
||||
- Conclusion of L14+L15: far terrain has no exact or within-budget saving left with today's design. The
|
||||
remaining far cost is the price of the current refinement and connector-repair geometry.
|
||||
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,232 @@
|
||||
# Linux16 — terrain cache design and Core prototype
|
||||
|
||||
This is a persistence feature. The measurements judge exactness, storage, and worker responsiveness;
|
||||
first-time terrain generation remains a separate cost. The prototype is headless and opt-in. Unreal
|
||||
integration remains for the owner's PC.
|
||||
|
||||
## Design
|
||||
|
||||
- **Cache identity.** Production keys should include a stable world ID; season ID and canonical
|
||||
season-content hash; seed and RNG contract; recipe, plan, biome/manifest and owner-content hashes;
|
||||
schema and `WorldAssemblyEvaluatorVersion` (6 here); tile coordinate and LOD; voxel size and tile
|
||||
dimensions; generation options; material/payload-section schema; mesher contract; and a generator
|
||||
build/FP-contract identity. Include a platform or compiler identity unless cross-build output has
|
||||
been proven equivalent. Clang and GCC matched this round's full hash matrix, but that alone does
|
||||
not make every future build interchangeable. Use canonical content hashes so whitespace or JSON
|
||||
key order does not change the world identity.
|
||||
- **Prototype identity.** The header and file name include the raw owner-snapshot hash, Core
|
||||
configuration, recipe/plan/manifest hashes, seed, schema, evaluator, voxel size, tile size,
|
||||
mesher/FP contract, generation-options hash, tile key and LOD. The edited-tile probe also includes
|
||||
edit sequence and tile edit digest. The prototype uses 64-bit FNV for identity inputs and CRC32
|
||||
for accidental file corruption. Production should use a 256-bit content hash and a stable world
|
||||
ID; the prototype hashes are evidence machinery, not a security boundary.
|
||||
- **Edits.** Keep the reusable cache pristine. On load, apply the authoritative versioned diff layer
|
||||
in sequence, then build the edited render/collision result. A derived edited-tile cache, if useful,
|
||||
must key on the last applied sequence and a digest of edits intersecting that tile. A newer edit
|
||||
invalidates only derived entries for touched tiles; it leaves pristine entries valid. The edit
|
||||
probe wrote and reread sequence 1 with digest `3cd5603ebf707d0c` and matched every array and hash.
|
||||
- **Server.** Never accept a client's cached mesh or collision as authoritative. The server may use
|
||||
its own locally generated cache for pristine meshes, then apply its ordered edit layer and build
|
||||
collision from that result. Client cache contents never cross the network; only validated edits
|
||||
do. A server restart can reuse its own cache after identity and payload validation.
|
||||
- **Invalidation and verification.** Season/content, seed/RNG, recipe, plan, evaluator, generator,
|
||||
mesher, FP, voxel-grid, LOD, options, or payload-schema changes select a different key. Evaluator
|
||||
bump and edit-sequence bump were rejected by the prototype. A canonical uncompressed-payload hash
|
||||
should be stored and verified at load; corruption or a mismatch is a cache miss, followed by
|
||||
regeneration on a worker. The prototype verifies CRCs and structure on every load, compares the
|
||||
complete mesh arrays and Core D/M/P/T hashes after every generated write/read, and regenerates a
|
||||
deliberately corrupted cached tile. This proves the sampled write/read route; a file's own hash
|
||||
cannot independently prove generator semantics on every later hit, so complete identity and
|
||||
version discipline remain essential.
|
||||
- **Stored data.** The prototype stores the final Core mesh: exact double positions/normals, float
|
||||
UVs, RGBA, section/group counts, indices, skirt masks, density sample count, and D/M/P/T hashes.
|
||||
This avoids remeshing on a hit and maps directly to future RMC sections. The indices and section
|
||||
ranges can feed server-side collision construction after edits. Density/sign tiles offer more
|
||||
freedom for future meshers but require meshing, materials, and collision work on load; density
|
||||
sizes were not benchmarked here. Keep collision ownership server-side in either design.
|
||||
- **Worker/smoothness path.** Cache reads, zlib decode, integrity checks, serialization, and writes
|
||||
run in the existing headless Core tile worker/result route. The Unreal game thread was not
|
||||
present on this machine; its later integration should submit cache work through the same worker
|
||||
queue and return the ordinary tile result. The prototype uses that route for all recipes and
|
||||
changes no recipe implementation. It exercises SurfaceWorld for cache round trips; the cache-off
|
||||
matrix and route audits cover SurfaceWorld and FloatingIslands.
|
||||
- **Read versus regenerate.** Track per-LOD generation and load/decode EWMA. If a demanded tile's
|
||||
estimated read/decode time exceeds generation time, schedule generation directly; if an attempted
|
||||
load fails, discard it and regenerate without blocking the result queue. Keep the same tile
|
||||
priority and permit the previous coarser LOD to remain visible while work finishes. On this L1
|
||||
set, warm read/decode averaged about 0.11 ms per LOD0 tile and 0.91–1.61 ms per LOD8/7 tile,
|
||||
compared with 3.97 ms LOD0 and 12.89–24.24 ms LOD8/6 mesh generation in the instrumented fill.
|
||||
- **Layout, crash safety, and eviction.** One file per tile under a world-content directory keeps
|
||||
reads and eviction independent; the prototype has 2,553 L1 records and 2,986 touched records after
|
||||
the 24 m crossing. It writes a same-directory `.tmp`, flushes and `fsync`s it, atomically renames
|
||||
to `.vftile`, then syncs the directory. Readers only open final `.vftile` names and reject bad
|
||||
lengths, versions, identities, CRCs, counts, indices, and padding. A crash before rename leaves a
|
||||
non-loadable temp file. Production startup should remove old temp files. Use a global 2 GB cap
|
||||
with an LRU index by last use, protect entries currently in use, reserve space under one lock,
|
||||
evict oldest unpinned records before writes, and skip the write if space still cannot be made.
|
||||
The prototype enforces the cap and skips writes when full; it does not implement LRU eviction.
|
||||
Its 4 KiB padding enabled `O_DIRECT` measurements and can be removed in production buffered I/O.
|
||||
|
||||
## Compression measurements
|
||||
|
||||
Available system zlib headers/library (zlib 1.3.1) were linked with `-lz`. The existing zstd 1.5.7
|
||||
CLI had no development headers, so it was tested as a per-tile CLI codec in LOD batches; its times
|
||||
include one process start per LOD. No installs or system changes were made. All totals below cover
|
||||
all 2,553 L1 tiles except Q16, whose first run had five route-audit tiles prepopulated before codec
|
||||
sampling. Sizes are payload bytes; the lossless cache also has headers and per-file 4 KiB padding.
|
||||
|
||||
| Encoding | Total bytes | Relative to raw | Exact Core arrays? | L1 gate |
|
||||
|---|---:|---:|---|---|
|
||||
| Raw canonical mesh | 107,342,349 | 100% | Yes | Pass |
|
||||
| zlib level 6 | 47,388,401 | 44.15% | Yes | Pass |
|
||||
| zstd level 1 (CLI, per tile) | 46,513,479 | 43.33% | Yes | Pass; 1.85% smaller than zlib payload |
|
||||
| Q16 positions + oct16 normals + delta-varint indices | 38,672,768 (2,548 tiles sampled) | 36.17% | No | Fail: 11.4482 px at LOD0; 6 degeneracy changes (2 at LOD4, 4 at LOD6) |
|
||||
| Q24 positions + oct16 normals + delta-varint indices | 42,862,996 | 39.94% | No | Pass: 0.0458 px max, 0.000060 Lambert delta max, 0.00361° normal error max, no winding/degeneracy changes |
|
||||
|
||||
Q24 is about 9.5% smaller than zlib's payload, but it changes floating-point positions/normals and
|
||||
cannot satisfy exact loaded-versus-regenerated hashes. The selected cache path is lossless zlib.
|
||||
The zstd CLI decoded every tile byte-for-byte, and was modestly smaller/faster in this test. There
|
||||
are no zstd development headers here, so it was not linked into the worker prototype. The L1 zlib
|
||||
records occupied 55,685,120 bytes on disk, including record framing and page padding.
|
||||
|
||||
Per-tile size averages by LOD:
|
||||
|
||||
| LOD | Tiles | Raw B | zlib B | zstd B | Q16 B* | Q24 B |
|
||||
|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 0 | 512 | 19,232.6 | 7,901.7 | 8,009.4 | 7,115.2 | 7,797.3 |
|
||||
| 1 | 448 | 15,437.8 | 6,715.2 | 6,715.6 | 5,394.2 | 6,244.9 |
|
||||
| 2 | 448 | 11,152.2 | 4,452.4 | 4,516.8 | 4,129.9 | 4,548.7 |
|
||||
| 3 | 384 | 11,750.3 | 4,813.1 | 4,885.5 | 4,361.7 | 4,801.7 |
|
||||
| 4 | 320 | 49,186.9 | 18,865.3 | 18,352.3 | 17,858.1 | 19,738.2 |
|
||||
| 5 | 144 | 83,106.3 | 37,693.0 | 37,004.3 | 30,247.6 | 33,494.7 |
|
||||
| 6 | 96 | 128,102.3 | 59,624.7 | 56,521.2 | 46,390.6 | 51,437.7 |
|
||||
| 7 | 48 | 308,206.8 | 141,387.7 | 139,348.3 | 110,361.7 | 121,997.0 |
|
||||
| 8 | 153 | 171,712.3 | 81,805.0 | 79,662.9 | 60,755.8 | 67,046.4 |
|
||||
|
||||
*Q16 averages are over the 2,548 newly generated tiles; five prepopulated tiles did not enter its
|
||||
codec sample. Raw, zlib, and Q24 averages are from the clean 2,553-tile fill.
|
||||
|
||||
Average codec encode/decode time per tile, microseconds:
|
||||
|
||||
| LOD | Raw E/D | zlib E/D | zstd CLI E/D | Q24 mesh E/D |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 0 | 38.60 / 12.31 | 736.63 / 79.49 | 72.14 / 47.97 | 16.28 / 18.78 |
|
||||
| 1 | 30.13 / 9.47 | 608.30 / 68.07 | 62.71 / 40.53 | 14.26 / 16.04 |
|
||||
| 2 | 18.94 / 4.64 | 286.44 / 37.86 | 48.50 / 34.65 | 8.20 / 7.55 |
|
||||
| 3 | 20.21 / 5.18 | 313.54 / 41.77 | 50.73 / 35.90 | 8.81 / 8.07 |
|
||||
| 4 | 90.46 / 27.53 | 1,508.23 / 189.40 | 134.50 / 78.66 | 41.58 / 41.93 |
|
||||
| 5 | 163.48 / 55.53 | 2,935.24 / 370.92 | 220.23 / 140.52 | 76.90 / 79.86 |
|
||||
| 6 | 254.98 / 96.01 | 4,900.50 / 611.07 | 361.50 / 184.35 | 127.39 / 136.19 |
|
||||
| 7 | 621.57 / 211.84 | 11,944.70 / 1,475.82 | 861.84 / 442.64 | 327.72 / 391.27 |
|
||||
| 8 | 325.34 / 109.80 | 6,478.56 / 784.32 | 486.54 / 289.96 | 167.25 / 186.01 |
|
||||
|
||||
## Smoothness, prototype, and budgets
|
||||
|
||||
- A clean write generated, losslessly stored, reloaded, and exactly compared all 2,553 L1 tiles.
|
||||
The whole-view D/M/P/T hash matched L14. The warm run hit 2,553/2,553 tiles, returned the same
|
||||
hash, and reached whole-view-ready in 0.450 s including 0.193 s of shape setup; tile streaming
|
||||
took 0.243 s.
|
||||
- The 24 m crossing covered 432 entering tiles. Its D/M/P/T hash was
|
||||
`337ca62131576728 / 7612f48e2ddbbaab / 69a67ed4add83b87 / f11ac1ebd0f7ddb8`, equal to L14.
|
||||
The cache route audit passed 9 pristine pairs, one edited pair, and one movement-history pair,
|
||||
all with exact arrays and zero D/M/P/T mismatches. The crossing fill generated and round-tripped
|
||||
428 tiles; four had already been created by the route audit.
|
||||
- Safety controls passed: evaluator bump rejected, edit-sequence bump rejected, a flipped compressed
|
||||
payload byte rejected by record CRC, and a truncated file rejected by size validation. A separate
|
||||
L1 run with one real tile corrupted regenerated that tile, hit the other 2,552, and restored the
|
||||
exact L14 whole-view hash.
|
||||
- `O_DIRECT` bypassed the kernel page cache for the 55,685,120-byte L1 record set: 0.269 s
|
||||
(197.6 MiB/s). Buffered first touch was 0.0191 s and the repeat was 0.0126 s, but those passes
|
||||
followed file writes/verification and were page-cache warm. No page cache was dropped; device
|
||||
firmware may also cache blocks, so `O_DIRECT` is page-cache-cold evidence rather than proof of a
|
||||
physical cold-device read. The ordinary worker hit path includes buffered read, zlib decode,
|
||||
validation, and tile construction.
|
||||
- The instrumentation measures raw, zlib, and mesh candidates on a miss, so its clean-fill
|
||||
whole-view-ready time was 2.674 s versus 1.718 s for the hash-on no-cache L1 run. This includes
|
||||
benchmark-only extra codec work. Production should encode only its selected lossless format on a
|
||||
miss. Warm disk work and decode stay on workers; the first tile was ready in 0.226 s on fill and
|
||||
0.208 s warm. These headless timings show worker completion; an Unreal frame-time check is still
|
||||
for the integration round.
|
||||
- The cache root peaked at 60,835,840 bytes after L1, the 24 m crossing, edit probe, and corruption
|
||||
controls; the enforced limit is 2,000,000,000 bytes. No eviction was needed. The actual cache
|
||||
implementation is a headless Core-worker prototype, not the Unreal plugin integration.
|
||||
|
||||
## Default-path cost and compatibility
|
||||
|
||||
Five interleaved W12 L1 pairs, hash off, 2,553 tiles each:
|
||||
|
||||
| Pair | L14 seconds | L16 cache-disabled seconds |
|
||||
|---:|---:|---:|
|
||||
| 1 | 1.450 | 1.446 |
|
||||
| 2 | 1.464 | 1.457 |
|
||||
| 3 | 1.458 | 1.453 |
|
||||
| 4 | 1.525 | 1.472 |
|
||||
| 5 | 1.459 | 1.483 |
|
||||
|
||||
Median: 1.459 s L14, 1.457 s L16, −0.14%; no measurable cache-off cost in this run.
|
||||
|
||||
All 28 L14 hash configurations passed (SurfaceWorld and FloatingIslands; Clang 19 FP off and GCC
|
||||
14 default FP; W1/W8/W12 with five W12 repeats). Every one matched all 12 full-detail, whole-view,
|
||||
nine LOD, and exact-query rows. The four default 24 m crossing checks (W1/W12 on both compilers)
|
||||
matched all 13 rows, including the 432-tile crossing hash. Default route parity passed for both
|
||||
compilers across SurfaceWorld seeds 0, 1, and 17 and FloatingIslands seed 0, using the existing
|
||||
96 m history probe. The cache route passed its separate L1 and 24 m crossing audits.
|
||||
|
||||
Representative SurfaceWorld seed-0 hashes (D/M/P/T):
|
||||
|
||||
| View | Density / mesh / payload / topology |
|
||||
|---|---|
|
||||
| Full detail, 960 tiles | `0ee0579bd15cbbe4 / 32e5d4964d610fee / 1932b96ee4b1d910 / 69743c6bad3447dc` |
|
||||
| Whole L1, 2,553 tiles | `c10697a8f48380c7 / e75c9119a1498cd5 / 49a9a8d1835a6c8c / edcb23288b5e8724` |
|
||||
| LOD0, 512 tiles | `769f2a706d585a72 / 33eeeb39fa443816 / 645621a1d968983f / c2457ed905036c9d` |
|
||||
| LOD1, 448 tiles | `69cc5a5c0a2c60af / b48335f5410c57d5 / d15c5480162ab8ba / feca1bd025823604` |
|
||||
| LOD2, 448 tiles | `85deed5683a77202 / 53469895987aeea2 / e55f8ae7e1532033 / d5feb12a0e85c09b` |
|
||||
| LOD3, 384 tiles | `6518caefb3be7e7f / ec513c90b853a614 / f62fa1137ff17f82 / ccf1dcf681965816` |
|
||||
| LOD4, 320 tiles | `62d4be96021992f4 / f1e4c6efcc2615a9 / 9dc5f9c88be02f33 / fe6f55125a07c3bf` |
|
||||
| LOD5, 144 tiles | `5e4a8c38b8cea6fc / 08ee56998aaf21e5 / 541eea8558f26cc8 / 6539374a8b3336d8` |
|
||||
| LOD6, 96 tiles | `e036e95b9006945b / 624a33f941a99b02 / 81b73e14f68bdf86 / 4de4c63ce1f80a56` |
|
||||
| LOD7, 48 tiles | `8ee358af56a30d8d / 46ab3c6184048b7c / 0a6164b7e68e80c5 / 0c26b85bb03bc831` |
|
||||
| LOD8, 153 tiles | `c3458e1cbdb9c471 / 46c03d0994d75192 / 8f89fc5bde12d230 / d16dc0cf62031898` |
|
||||
| Exact queries: density / unclamped / detail / collision | `e4d4a8eb95b35933 / e4d4a8eb95b35933 / efcba20c0c70c415 / e604823a249029bf` |
|
||||
|
||||
FloatingIslands full-detail was `6d842d2878b08ef6 / 4b868a3184f08035 / 2cedf2236788752d / eac17d5e0cb5044a`; whole-view was `5be4d8cde79b2a76 / 96f75041f6861b3a / 3700a516cc89d50c / 6b99948d6eab4f21`. Its exact-query hashes also matched L14: `61daded9c303becb / 61daded9c303becb / efcba20c0c70c415 / 84d37463be9b0573`.
|
||||
|
||||
## Runs and limits
|
||||
|
||||
- First Clang build was interrupted during a silent compile phase. The next build exposed a const
|
||||
error in the prototype file index; making that index mutable fixed it. Final Clang and GCC builds
|
||||
passed.
|
||||
- The first L1 command used the binary's incorrect default data path (`Standalone/bin/data`) and
|
||||
exited before writing. Supplying `Standalone/data/owner_world.json` passed.
|
||||
- The first codec run sampled 2,548 tiles because route parity had prepopulated five; it was kept
|
||||
for Q16 measurements. A clean fill then sampled all 2,553 Q24 tiles.
|
||||
- A route wrapper first required a moving hash when hashing was disabled; the route audit itself
|
||||
passed. Seed 17's 24 m history probe had no departed-tile candidate and failed on both L16 and
|
||||
the L14 control. The same L14 96 m history setup passed on both L16 compilers. The required 24 m
|
||||
crossing was separately checked and passed.
|
||||
- The first crossing-hash comparator used an incorrect old-log filename; correcting the filename
|
||||
let all four comparisons pass. No run exceeded 30 minutes or stalled for 15 minutes.
|
||||
- Final review found that the cache-hit route audit's `exact_arrays` field initially reported hash
|
||||
agreement without comparing the arrays themselves. The audit was changed to compare complete
|
||||
arrays; after rebuilding, both L1 and 24 m crossing route audits passed with zero mismatches.
|
||||
|
||||
Runs used CPUs 0–11, at most 12 build jobs, a 6 GiB address-space cap, and normal priority. The
|
||||
governor stayed `powersave`; peak sampled single-process RSS was 943,844 KiB (FloatingIslands route
|
||||
audit, GCC). No installs, system changes, or page-cache drops were made. The L3 FP-contract preamble
|
||||
and self-test, L4 `FLazy`, L7 shared cache, L9/L12 proofs, L11 readiness/crash fix, L13 route binding,
|
||||
and async worker/result rules remain enabled. Cache logic is recipe-agnostic.
|
||||
|
||||
**Owner note:** Saving generated terrain works exactly and fits well under the round's disk cap.
|
||||
Lossless zlib saved about 56% of the serialized payload. The smaller Q24 mesh encoding passed the
|
||||
screen gate but changed exact hashes, so the feature should use the lossless record format.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Design accepted: the key, the pristine cache plus ordered edits, a server that never trusts client
|
||||
caches, corruption/stale rejection, lossless zlib, and atomic writes.
|
||||
- **Not merged into `assembler/world`:** `Standalone/Source/StreamBenchmarkPipeline.h` (included by the
|
||||
Windows viewer `StandaloneViewer.cpp`) now unconditionally includes `TileCachePrototype.h`, which
|
||||
includes `<unistd.h>`, `<fcntl.h>` and `<zlib.h>`. That would break the MSVC build. Kept on side
|
||||
branch `wip/l16-tile-cache`. Before merging, the prototype must be platform-guarded (or the cache
|
||||
given a portable I/O and codec layer), and zlib needs a Windows source (UE ships zlib).
|
||||
- Cache-off cost re-checked by Luna at −0.14% (within noise).
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure upward mesh faces around the FI/SW stratum interface."""
|
||||
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]
|
||||
low_z = float(sys.argv[2]) if len(sys.argv) > 2 else -80.0
|
||||
high_z = float(sys.argv[3]) if len(sys.argv) > 3 else -45.0
|
||||
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, "faces": [], "spans": []})
|
||||
near_tiles = collections.defaultdict(list)
|
||||
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]
|
||||
z_values = []
|
||||
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
|
||||
normal_z = (n[a * 3 + 2] + n[b * 3 + 2] + n[c * 3 + 2]) / 3.0
|
||||
if normal_z < 0.65:
|
||||
continue
|
||||
z = (p[a * 3 + 2] + p[b * 3 + 2] + p[c * 3 + 2]) / 3.0
|
||||
if low_z <= z <= high_z:
|
||||
z_values.append(z)
|
||||
if not z_values:
|
||||
continue
|
||||
z_values.sort()
|
||||
row = per_lod[lod]
|
||||
row["tiles"] += 1
|
||||
row["faces"].extend(z_values)
|
||||
span = z_values[math.floor(.95 * (len(z_values) - 1))] - z_values[math.floor(.05 * (len(z_values) - 1))]
|
||||
row["spans"].append(span)
|
||||
extent = 32 * (1 << lod) * voxel_um / 1_000_000.0
|
||||
cx = (key[0] + 0.5) * extent
|
||||
cy = (key[1] + 0.5) * extent
|
||||
near_tiles[lod].append((math.hypot(cx - camera[0], cy - camera[1]), key,
|
||||
z_values[len(z_values) // 2], span, len(z_values)))
|
||||
|
||||
|
||||
def q(values, fraction):
|
||||
return values[math.floor(fraction * (len(values) - 1))] if values else math.nan
|
||||
|
||||
|
||||
print(f"camera_m={camera} yaw={yaw} pitch={pitch} boundary_band_m={low_z}..{high_z}")
|
||||
print("lod tiles_with_boundary_faces faces z_m_p05/median/p95 tile_span_m_p50/p95/max")
|
||||
for lod in sorted(per_lod):
|
||||
row = per_lod[lod]
|
||||
spans = sorted(row["spans"])
|
||||
print(f"{lod} {row['tiles']} {len(row['faces'])} "
|
||||
f"{q(sorted(row['faces']), .05):.4f}/{q(sorted(row['faces']), .5):.4f}/"
|
||||
f"{q(sorted(row['faces']), .95):.4f} "
|
||||
f"{q(spans, .5):.4f}/{q(spans, .95):.4f}/{max(spans):.4f}")
|
||||
print("nearest_xy_boundary_tiles_by_lod:")
|
||||
for lod in sorted(near_tiles):
|
||||
for distance, key, median, span, faces in sorted(near_tiles[lod])[:3]:
|
||||
print(f"lod={lod} key={key} xy_distance_m={distance:.2f} "
|
||||
f"boundary_median_z={median:.4f} within_tile_p05_p95_m={span:.4f} faces={faces}")
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "Json.h"
|
||||
#include "StandaloneCoreScheduler.h"
|
||||
#include "StandaloneSnapshotAdapter.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double CoreVoxelsPerMeter = 4.0;
|
||||
constexpr std::int64_t QPerCoreVoxel = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
|
||||
std::int64_t ToQ(double Meters)
|
||||
{
|
||||
return static_cast<std::int64_t>(std::llround(Meters*CoreVoxelsPerMeter*QPerCoreVoxel));
|
||||
}
|
||||
|
||||
void PrintLegacyBoundarySealTerms(const VoxelForgeCore::FloatingIslandManifestV1& Manifest,
|
||||
double X, double Y, double Z)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
const auto Xq=ToQ(X),Yq=ToQ(Y),Zq=ToQ(Z);
|
||||
const FloatingIslandFieldV1 field(Manifest.recipe,Manifest.plan);
|
||||
const std::int64_t fieldQ=field.densitySubvoxels(Xq,Yq,Zq);
|
||||
const std::int64_t bottom=Manifest.recipe.layoutMinimumZSubvoxels;
|
||||
const std::int64_t top=Manifest.recipe.layoutMaximumZSubvoxels;
|
||||
const std::int64_t seal=Manifest.recipe.boundarySealSubvoxels;
|
||||
std::int64_t bridged=fieldQ;
|
||||
bool applied=false;
|
||||
if(Zq>=bottom&&Zq<bottom+seal)
|
||||
{bridged=std::min(bridged,-std::max<std::int64_t>(1,Zq-bottom));applied=true;}
|
||||
if(Zq<=top&&Zq>top-seal)
|
||||
{bridged=std::min(bridged,-std::max<std::int64_t>(1,top-Zq));applied=true;}
|
||||
std::printf("[PreFixIslandTerms] xyz_m=%.3f,%.3f,%.3f native_field_q=%lld "
|
||||
"legacy_solid_seal_delta_q=%lld legacy_seal_active=%d pre_fix_wrapper_q=%lld\n",
|
||||
X,Y,Z,static_cast<long long>(fieldQ),
|
||||
static_cast<long long>(bridged-fieldQ),applied?1:0,static_cast<long long>(bridged));
|
||||
for(const auto& island:Manifest.plan.islands)
|
||||
{
|
||||
const bool inBounds=Xq>=island.influenceMinimumSubvoxels[0]
|
||||
&&Xq<=island.influenceMaximumSubvoxels[0]
|
||||
&&Yq>=island.influenceMinimumSubvoxels[1]
|
||||
&&Yq<=island.influenceMaximumSubvoxels[1]
|
||||
&&Zq>=island.influenceMinimumSubvoxels[2]
|
||||
&&Zq<=island.influenceMaximumSubvoxels[2];
|
||||
std::printf("[IslandGroup] ordinal=%u center_m=%.3f,%.3f,%.3f "
|
||||
"bounds_contains_sample=%d\n",
|
||||
island.ordinal,island.centerSubvoxels[0]/1024.0,island.centerSubvoxels[1]/1024.0,
|
||||
island.centerSubvoxels[2]/1024.0,inBounds?1:0);
|
||||
}
|
||||
for(const auto& edge:Manifest.plan.clusterEdges)
|
||||
{
|
||||
bool inBounds=true;
|
||||
const std::int64_t q[3]={Xq,Yq,Zq};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
inBounds=inBounds&&q[axis]>=edge.influenceMinimumSubvoxels[axis]
|
||||
&&q[axis]<=edge.influenceMaximumSubvoxels[axis];
|
||||
std::printf("[IslandConnector] id=%016llx bounds_contains_sample=%d\n",
|
||||
static_cast<unsigned long long>(edge.stableId),inBounds?1:0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PrintDensityLine(VoxelForgeCore::World& World,const char* Label,
|
||||
double X,double Y,double ZMinimum,double ZMaximum,double Step)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
std::printf("[DensityLine] label=%s xy_m=(%.6f,%.6f) z_m=%.2f..%.2f step_m=%.2f\n",
|
||||
Label,X,Y,ZMinimum,ZMaximum,Step);
|
||||
for(double Z=ZMinimum;Z<=ZMaximum+1.0e-9;Z+=Step)
|
||||
{
|
||||
const Vec3d Point{X*CoreVoxelsPerMeter,Y*CoreVoxelsPerMeter,
|
||||
Z*CoreVoxelsPerMeter};
|
||||
const double Density=World.densityAt(Point);
|
||||
const double Unclamped=World.unclampedDensityAt(Point);
|
||||
const DetailFieldSample Detail=World.detailSampleAt(Point);
|
||||
std::printf("[DensitySample] label=%s xyz_m=%.3f,%.3f,%.3f density_core_vox=%.9f "
|
||||
"density_cm=%.3f unclamped_core_vox=%.9f cave_influence=%.6f "
|
||||
"floor_influence=%.6f detail_displacement=%.6f sign=%s\n",Label,X,Y,Z,
|
||||
Density,Density*25.0,Unclamped,Detail.caveInfluence,Detail.floorInfluence,
|
||||
Detail.displacement,Density<0.0?"solid":"air");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc,char** argv)
|
||||
{
|
||||
if(argc!=2)
|
||||
{
|
||||
std::fprintf(stderr,"usage: %s owner-world-with-world-assembly.json\n",argv[0]);
|
||||
return 2;
|
||||
}
|
||||
VFJson::Value root;
|
||||
std::string error;
|
||||
if(!VFJson::Load(argv[1],root,error))
|
||||
{std::fprintf(stderr,"owner JSON parse failed: %s\n",error.c_str());return 2;}
|
||||
auto snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(root,0,3,8,false,false,error);
|
||||
if(!snapshot)
|
||||
{std::fprintf(stderr,"snapshot resolve failed: %s\n",error.c_str());return 2;}
|
||||
const auto* assembly=snapshot.worldAssembly();
|
||||
if(!assembly||assembly->floatingIslands.empty())
|
||||
{std::fprintf(stderr,"FloatingIslands manifest 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;}
|
||||
|
||||
constexpr double FlatX=-11.612443923950195;
|
||||
constexpr double FlatY=-118.22123718261719;
|
||||
constexpr double IslandX=-6.3;
|
||||
constexpr double IslandY=-19.057;
|
||||
constexpr double RingX=-4.889;
|
||||
constexpr double RingY=-16.008;
|
||||
std::printf("[DensityRoute] flat_square_xy_m=(%.6f,%.6f) island_ground_xy_m=(%.6f,%.6f) "
|
||||
"compiled=%d core_voxel_size_m=0.25 owner_to_core_chunk_voxels=16\n",
|
||||
FlatX,FlatY,IslandX,IslandY,world.isCompiled()?1:0);
|
||||
PrintDensityLine(world,"flat-square",FlatX,FlatY,2.5,9.0,0.25);
|
||||
PrintDensityLine(world,"fi-sw-boundary",FlatX,FlatY,-62.0,-58.0,0.25);
|
||||
PrintDensityLine(world,"island-center",IslandX,IslandY,-39.0,-20.0,0.5);
|
||||
PrintDensityLine(world,"lod0-lod1-ring-edge",RingX,RingY,-62.0,-20.0,1.0);
|
||||
|
||||
const auto& island=assembly->floatingIslands.front().manifest;
|
||||
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,7.75,8.00,8.25})
|
||||
PrintLegacyBoundarySealTerms(island,FlatX,FlatY,z);
|
||||
for(double z:{-35.0,-30.0,-25.0})
|
||||
PrintLegacyBoundarySealTerms(island,IslandX,IslandY,z);
|
||||
for(double z:{-60.0,-35.0,-28.0,-25.0})
|
||||
PrintLegacyBoundarySealTerms(island,RingX,RingY,z);
|
||||
return 0;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
@@ -0,0 +1,26 @@
|
||||
# Owner note — L17 FloatingIslands steps
|
||||
|
||||
The ledges were real generated mesh geometry caused by two boundary mistakes that made empty space
|
||||
look like flat ground: the FloatingIslands seal was applied as a solid cap across XY, and owner
|
||||
chunks were treated as 8 m of Core voxels instead of their actual 4 m. At the measured flat square,
|
||||
the native island field is air and cave/floor influence is zero. The point is about 100 m from the
|
||||
four islands.
|
||||
|
||||
I removed the false cap and corrected the shared owner/Core coordinate conversion. Evaluator version
|
||||
is 7. The before/after captures show the false sheet disappear and the actual island shape remain.
|
||||
SurfaceWorld’s L1 hash changes because it now selects its intended `Heightfield2D` route; its route,
|
||||
readiness and proof audits pass.
|
||||
|
||||
The export has nonempty meshes at LOD0–8, and the standalone viewer enables and draws all levels by
|
||||
default. I cannot identify the screenshot’s dark square as a whole missing LOD from the Linux data;
|
||||
it may be a specific tile or viewer draw/upload issue. I did not change the Windows viewer. Please
|
||||
capture the view below and, if the square remains, repeat with `--capture-only-lod=0` through `8`.
|
||||
|
||||
The low-view camera is `(-22,-19,-22.8) m`, yaw 0°, pitch −4°, matching the Linux island pair.
|
||||
|
||||
```bat
|
||||
VoxelForgeCoreStandalone.exe --world-assembler-prototype --recipe=floating-islands --clip-radius=3 --max-clip-level=8 --workers=12 --camera=-22,-19,-22.8,0,-4 --capture=Standalone\Research\WorldAssemblerM3\Linux17\captures\floating-islands-windows.png --frame-limit=60
|
||||
```
|
||||
|
||||
Use a different `--capture=` filename for each isolated LOD. Send the all-level image plus the
|
||||
isolated image that corresponds to the square.
|
||||
@@ -0,0 +1,182 @@
|
||||
# VoxelForge L17 — FloatingIslands boundary report
|
||||
|
||||
Branch `assembler/world`, starting HEAD `9a7c87e`. No cache code was changed. The shared evaluator
|
||||
version is now 7; the recipe schema stays at 1.
|
||||
|
||||
## Cause and fix
|
||||
|
||||
FloatingIslands uses the sparse `FloatingIslandFieldV1` manifest field. SurfaceWorld uses the
|
||||
separate planar `Heightfield2D` field. The owner voxel is 12.5 cm and the Core voxel is 25 cm, so
|
||||
one 32-voxel owner chunk is **16 Core voxels / 4 m**. Several shared paths divided Core positions
|
||||
by 32 directly, treating that chunk as 8 m. That doubled stratum bounds, selected the wrong
|
||||
generator route near the SurfaceWorld boundary, and made the legacy layer envelope extend over
|
||||
empty FloatingIslands space.
|
||||
|
||||
The FloatingIslands `boundarySeal` also had been applied as a solid cap over all XY. At the owner’s
|
||||
flat-square probe `(-11.612444,-118.221237) m`, the raw island field is air: at Z=3.25 m it was
|
||||
`+81,809` subvoxels. The old seal added `−82,065`, yielding `−256` (`−1` Core voxel, or −25 cm);
|
||||
at Z=3.50 m it yielded `−1` subvoxel (−0.098 cm), then air again at Z=3.75 m. The wrong 8 m
|
||||
legacy layer bound produced a second false top near Z=8 m (old world density at 7.75 m was
|
||||
−0.743 Core voxels; at 8 m it was +0.073). The owner’s 4.5 m LOD0 jump is real mesh geometry
|
||||
created by those bad boundaries, often inside a tile; it is not a renderer artifact or island relief.
|
||||
Nested clipmap rectangles made its placement look like a tile-boundary-only issue.
|
||||
|
||||
The fix uses the resolved owner/Core scale for manager lookups, layer bounds, readiness, and
|
||||
adjacent native strata; removes the XY-wide FloatingIslands seal cap and its all-solid proof; and
|
||||
clips adjacent native FI/SW signed-distance fields at their shared physical boundary. This is in
|
||||
Core and applies to every generator route. SurfaceWorld still uses `Heightfield2D`.
|
||||
|
||||
The cave-floor lead does not match the samples. Cave and floor influence were exactly zero at the
|
||||
flat square, the island center, and the sampled ledge/ring point. The native FI field and its island
|
||||
or connector bounds account for the surface. The owner world compiled 70 rooms, 69 tunnels and 2
|
||||
passages in this run; those legacy primitives do not contribute to the measured FI ledge.
|
||||
|
||||
## Before/after measurements
|
||||
|
||||
`RingProbe.py` samples upward-facing mesh surfaces on adjacent XY tile edges only when the tiles’
|
||||
vertical bands overlap. Values are median / p95 / maximum absolute height difference, in cm. `n/a`
|
||||
means this sparse-island export had no same-band LOD0↔1 surface pair after removing the false cap;
|
||||
it is not a zero-error result. Higher rings include the deep SurfaceWorld boundary and other deep
|
||||
surfaces, so those rows are not all FloatingIslands ground.
|
||||
|
||||
| LOD edge | Before (cm) | After (cm) |
|
||||
|---|---:|---:|
|
||||
| 0→1 | 3.451 / 8.920 / 449.158 (266 samples) | n/a (no same-band pair) |
|
||||
| 1→2 | 26.642 / 148.262 / 149.862 (514) | 0 / 0 / 0 (234) |
|
||||
| 2→3 | 0.134 / 198.730 / 773.318 (596) | 0 / 0 / 0 (207) |
|
||||
| 3→4 | 3.269 / 47.875 / 188.531 (277) | 0 / 0 / 0 (288) |
|
||||
| 4→5 | 29.975 / 130.496 / 445.444 (544) | 0 / 0 / 30.957 (258) |
|
||||
| 5→6 | 12.234 / 127.224 / 536.573 (266) | 12.234 / 127.224 / 536.573 (266) |
|
||||
| 6→7 | 29.013 / 301.752 / 42,624.479 (264) | 28.892 / 291.861 / 945.477 (264) |
|
||||
| 7→8 | 83.843 / 1,225.416 / 3,755.407 (275) | 83.843 / 1,225.416 / 3,755.407 (275) |
|
||||
|
||||
The valid worst pre-fix LOD0↔1 sample is the false cap: 3.500 m vs 7.992 m, a 449.158 cm
|
||||
difference. Upward-face within-tile span p95 / maximum changed from 4.490 / 4.491 m to 1.186 /
|
||||
1.927 m on LOD0. The remaining LOD0 span is the actual island shape. LOD1–3 after-fix upward
|
||||
faces at the measured lower boundary are exactly at −60 m. The island tops around the matched
|
||||
camera are roughly −30 to −25 m.
|
||||
|
||||
A separate exploratory column comparison initially reported a 34.7 m difference by pairing an
|
||||
island top near −25 m with a −60 m SurfaceWorld surface from a vertically disjoint tile. That
|
||||
comparison was rejected: it is not a same-band ring seam. The saved `RingProbe.py` output is the
|
||||
cross-LOD evidence used above.
|
||||
|
||||
## History, viewer and SurfaceWorld
|
||||
|
||||
Prebuilt L7, L9, L12, L13 and L14 binaries/logs all have the same legacy hashes: whole-view D/M/P/T
|
||||
`92ba6a57a8690c00 / 9c6b56dfab6241c9 / 82394dd7c368a061 / 445e40bdb69b0bdb`; full-detail
|
||||
`93ee3b6fb6b446f3 / 3ebc72d2027ab80b / cd5f55a96de12458 / 2f61c29af247b547`. The pattern
|
||||
predates L7; these measurements show no output change at L9 or L13. S9 `e7586a1`, S12 `212814a`
|
||||
and S14 `ec2afe9` do not contain the headless pipeline source, so they cannot be measured this way.
|
||||
No old commit was rebuilt: `git worktree add` failed because `.git/worktrees` is read-only, and the
|
||||
available old binaries were used as-is.
|
||||
|
||||
The headless mesh export has nonempty tiles at **all** LODs 0–8 (matched view counts:
|
||||
15, 64, 40, 48, 48, 96, 48, 48, 153). The standalone viewer initializes all nine `bShowLevel`
|
||||
flags to true and its draw loop visits all streaming batches. There is no evidence that a whole LOD
|
||||
is absent from generated data or globally filtered by the viewer. The screenshot’s dark square
|
||||
cannot be mapped to a particular tile/LOD from this Linux export; a localized viewer draw/upload
|
||||
or camera issue remains unconfirmed. No viewer change was made because the Windows D3D11 build and
|
||||
capture are unavailable here. The Unreal comparison does not identify the cause of that square.
|
||||
|
||||
SurfaceWorld L1 hashes changed for the shared coordinate fix. Old L14 LOD1 D/M/P/T was
|
||||
`5043975b1e5b5a3d / 1c2a5658f10fdeaa / 23b6779ef35c3a0a / 1a6d2e2d152b4f84`; L17 is
|
||||
`393ad79fd8c07132 / 47c9ba17e9f5ceaa / d5d6a948ac880e17 / 8b85ea08b2c440c4`. Before, readiness
|
||||
was 0/27; now it is 27/27 on the near ring. The old mapping selected the wrong route at the
|
||||
SurfaceWorld stratum; the corrected mapping selects its manifest `Heightfield2D` plan. Page/native
|
||||
route parity and exact queries pass, so this is the intended shared-route output change.
|
||||
|
||||
## Gates, captures and limits
|
||||
|
||||
All 28 determinism rows passed: FI and SW, Clang 19 `-ffp-contract=off` and GCC 14 default,
|
||||
W1/W8/W12 with five W12 repeats. Each recipe had one unique full, whole, nine-LOD and exact-query
|
||||
hash tuple across all configurations. FI whole-view D/M/P/T is
|
||||
`8339509b4d5ec231 / 70e5b2b9085b517c / 4e72e53745f20ed0 / 1a2643e5138e0407`; SW whole-view is
|
||||
`b286ee4f4c38b7fc / 6862deb8ab9c2af9 / f23480a4953457f9 / 62977edfd04b3afe`.
|
||||
|
||||
FI route parity: 8 page pairs, 14 higher-LOD exact pairs, 782,026 exact samples, zero mismatches;
|
||||
uniform proof: 19,288 blocks / 25,672,328 samples / zero violations. SW: 5 page and 9 native-only
|
||||
pairs, 814,625 exact samples, zero mismatches; uniform proof: 13,297 blocks / 17,698,307 samples /
|
||||
zero violations. Readiness negative controls were caught. The Clang FP-contraction-off and GCC
|
||||
default builds passed; the Core startup FP self-test did not reject either. No async/MPSC/Epoch
|
||||
code changed; `FLazy`, shared column caches, L9/L12 proof paths and L13 route binding remain in the
|
||||
tested build.
|
||||
|
||||
Readable Linux capture mode adds directional light with soft ambient, gentle height tint, and an
|
||||
optional faint tile-edge overlay. Five 1600×900 JPEGs, all below 300 KB, are in `captures/`:
|
||||
|
||||
- `floating-islands-flat-square-before.jpg` / `floating-islands-flat-square-after.jpg`: same owner
|
||||
probe camera `(-11.6124,-118.2212,-19.8452) m`, yaw 0, pitch 0, before/after.
|
||||
- `floating-islands-island-view-before.jpg` / `floating-islands-island-view-after.jpg`: same low
|
||||
island camera `(-22,-19,-22.8) m`, yaw 0, pitch −4°, before/after.
|
||||
- `surface-world-l1-readable-tiles.jpg`: SurfaceWorld L1 sanity view, camera `(0,-35,30) m`, yaw 0,
|
||||
pitch −60°.
|
||||
|
||||
All are 1600×900 JPEGs; SHA256 is included for byte-level review.
|
||||
|
||||
| Capture | Bytes | SHA256 |
|
||||
|---|---:|---|
|
||||
| `floating-islands-flat-square-before.jpg` | 70,166 | `c41ed7645735ebefea97fbc1aed167b66a3e06c019c25b1c7d8cae9449713999` |
|
||||
| `floating-islands-flat-square-after.jpg` | 47,612 | `f8fb5f937ac65be9db70a41473c7a10d7f35943ba6ef5c94ab03fde9ded5c544` |
|
||||
| `floating-islands-island-view-before.jpg` | 70,813 | `5c7a8eda446f51a51ab22e45ed7286bf4180e69d8e3b2180780349facadacd32` |
|
||||
| `floating-islands-island-view-after.jpg` | 51,429 | `3aeedef903fce62e81ea1f07bb460dd2beb42b893bbd9bf29220973951295b4c` |
|
||||
| `surface-world-l1-readable-tiles.jpg` | 53,351 | `23f1753ed100c87607e22d2adc4f8e8a2856b8af3f1d37aa4f4cec9420a92b93` |
|
||||
|
||||
Clang/GCC compile peak RSS was 435,588 / 902,000 KiB. Determinism peak was 538,140 KiB; FI/SW
|
||||
route-audit peaks were 621,404 / 763,112 KiB. The governor remained `powersave`; runs were limited
|
||||
to CPUs 0–11, at most 12 workers, 6 GiB address space; llvmpipe used 12 threads.
|
||||
|
||||
## Failed or retried attempts
|
||||
|
||||
- The first headless invocation omitted the explicit owner-world input; it was rerun with
|
||||
`Standalone/data/owner_world.json`.
|
||||
- An initial SurfaceWorld export failed the native-only readiness guard; corrected owner/Core
|
||||
mapping and rerun passed.
|
||||
- A SurfaceWorld `--max-clip-level=1` attempt expanded to 35,435,983 tiles and failed the bounded
|
||||
compiler (20.985 s, 3,988,160 KiB peak RSS). Retried with `--max-level=1` (960 tiles), which
|
||||
passed.
|
||||
- `BoundaryProbe.py` was first called with unsupported named Z arguments; rerun with positional
|
||||
bounds passed.
|
||||
- The first XY-only ring comparison mixed vertically disjoint surfaces; replaced by the
|
||||
overlapping-band `RingProbe.py` used above. The exploratory column probe was also rejected for
|
||||
pairing different vertical bands.
|
||||
- The first offscreen SurfaceWorld command reversed scene/output arguments and failed to open the
|
||||
mesh; rerun with the documented order passed.
|
||||
- The first density-probe invocation used `Standalone/data/owner_world.json` without a resolved FI
|
||||
manifest and failed. It was rerun with `Saved/Linux/l17-fi-owner-snapshot.json` and passed.
|
||||
- `/usr/bin/time` was unavailable and an early process monitor mishandled a zombie child; the
|
||||
process-tree monitor in `MeasureBuild.py` replaced both attempts.
|
||||
- `git worktree add` could not write `.git/worktrees`; no worktree was created and no old commit was
|
||||
built outside the repository.
|
||||
- Mesa could not create `/home/jahni/.cache` in the read-only environment, disabled shader caching,
|
||||
and completed all captures. No file was written outside the repository.
|
||||
|
||||
## Windows confirmation
|
||||
|
||||
Run from the repository root on Windows to capture the low island view:
|
||||
|
||||
```bat
|
||||
VoxelForgeCoreStandalone.exe --world-assembler-prototype --recipe=floating-islands --clip-radius=3 --max-clip-level=8 --workers=12 --camera=-22,-19,-22.8,0,-4 --capture=Standalone\Research\WorldAssemblerM3\Linux17\captures\floating-islands-windows.png --frame-limit=60
|
||||
```
|
||||
|
||||
For isolation, repeat with `--capture-only-lod=0` through `--capture-only-lod=8`, changing the
|
||||
capture filename each time. Please send the all-level image and the isolated image matching the
|
||||
dark square; those will identify whether it is a per-tile draw gap or a particular LOD payload.
|
||||
|
||||
## Orchestrator review (Claude) — NOT MERGED
|
||||
|
||||
- **The fix breaks SurfaceWorld.** I exported the L1 view (camera `0,-35,-487.94`) from L14 and from
|
||||
L17 and rendered both in the new readable mode (`Saved/Linux/claude/sw/{before,after}-{l1,hz}.jpg`).
|
||||
After L17, the ground under the spawn camera is gone (black void below), the terrain sits far
|
||||
away, and the mesh export shrank from 58.7 MB to 31.7 MB. Halving the strate chunk size (8 m → 4 m)
|
||||
moves and squashes every stratum: a world change, not a fix.
|
||||
- **What the approved world uses:** the Core world the owner played and approved places strata
|
||||
with 8 m chunks (32 Core voxels of 25 cm). The "owner chunk = 4 m" claim must be checked against
|
||||
what the Unreal Core route actually does (`AVoxelWorld::CoreVoxelSizeCm`, the Core bridge,
|
||||
`ResolvedWorldSnapshot`), not assumed.
|
||||
- **Probably real and separable:** the FloatingIslands `boundarySeal` applied as a solid cap over all
|
||||
XY (the false floors at 3.5/8 m). That needs its own minimal fix with SurfaceWorld unchanged.
|
||||
- **Still open in FloatingIslands after the fix:** dark tile-aligned rectangles on the ground (the L17
|
||||
"after" captures, lower left).
|
||||
- Kept on side branch `wip/l17-fi-scale`. Only the capture tool's readable shading
|
||||
(`LinuxOffscreenCapture.cpp`) is taken into `assembler/world`.
|
||||
@@ -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}")
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the L17 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/l17-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-linux17-final-clang",
|
||||
"gcc-default": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux17-final-gcc",
|
||||
}
|
||||
RECIPES = {
|
||||
"floating-islands": "-10,-31,-22,0,-8",
|
||||
"surface-world": "0,-35,-75,0,0",
|
||||
}
|
||||
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:
|
||||
reference = None
|
||||
for row in rows:
|
||||
if row["recipe"] != recipe:
|
||||
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={row['compiler']} repeat={row['repeat']}", flush=True)
|
||||
all_pass = False
|
||||
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)
|
||||
@@ -0,0 +1,27 @@
|
||||
camera_m=(-22.0, -19.0, -22.799999237060547) yaw=0.0 pitch=-4.0 boundary_band_m=-80.0..-45.0
|
||||
lod tiles_with_boundary_faces faces z_m_p05/median/p95 tile_span_m_p50/p95/max
|
||||
1 64 131072 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
||||
2 40 20480 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
||||
3 48 24576 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
||||
4 48 22945 -60.0000/-60.0000/-60.0000 0.0000/0.9167/1.8008
|
||||
5 48 19554 -60.0000/-60.0000/-60.0000 0.0000/1.0819/1.7900
|
||||
6 9 134 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
||||
nearest_xy_boundary_tiles_by_lod:
|
||||
lod=1 key=(-2, -2, -4, 1) xy_distance_m=5.39 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=2048
|
||||
lod=1 key=(-2, -1, -4, 1) xy_distance_m=11.18 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=2048
|
||||
lod=1 key=(-1, -2, -4, 1) xy_distance_m=14.87 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=2048
|
||||
lod=2 key=(-1, 1, -2, 2) xy_distance_m=67.27 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
||||
lod=2 key=(1, -1, -2, 2) xy_distance_m=70.06 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
||||
lod=2 key=(-2, 1, -2, 2) xy_distance_m=71.87 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
||||
lod=3 key=(-3, -1, -1, 3) xy_distance_m=138.61 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
||||
lod=3 key=(-1, -3, -1, 3) xy_distance_m=141.35 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
||||
lod=3 key=(-3, 0, -1, 3) xy_distance_m=147.12 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
||||
lod=4 key=(-3, -1, -1, 4) xy_distance_m=301.38 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
||||
lod=4 key=(-1, -3, -1, 4) xy_distance_m=303.92 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
||||
lod=4 key=(-3, 0, -1, 4) xy_distance_m=309.34 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
||||
lod=5 key=(-3, -1, -1, 5) xy_distance_m=627.54 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=343
|
||||
lod=5 key=(-1, -3, -1, 5) xy_distance_m=629.98 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
||||
lod=5 key=(-3, 0, -1, 5) xy_distance_m=635.24 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.4903 faces=142
|
||||
lod=6 key=(-3, 0, -1, 6) xy_distance_m=1287.71 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=2
|
||||
lod=6 key=(0, -3, -1, 6) xy_distance_m=1291.28 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=16
|
||||
lod=6 key=(1, 2, -1, 6) xy_distance_m=1520.36 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=3
|
||||
@@ -0,0 +1,35 @@
|
||||
camera_m=(-22.0, -19.0, -22.799999237060547) yaw=0.0 pitch=-4.0 boundary_band_m=-80.0..-45.0
|
||||
lod tiles_with_boundary_faces faces z_m_p05/median/p95 tile_span_m_p50/p95/max
|
||||
1 65 144957 -60.4936/-60.4872/-59.5000 0.0059/0.9906/2.2272
|
||||
2 75 29057 -76.1525/-61.3304/-60.7788 0.8725/2.9863/8.5642
|
||||
3 87 26484 -73.0968/-63.0185/-62.4964 1.3211/11.6236/14.1057
|
||||
4 48 25679 -72.6651/-62.8690/-62.2182 3.2371/15.9700/17.0106
|
||||
5 48 26381 -67.4390/-62.7489/-61.8320 4.2721/11.9903/14.5467
|
||||
6 14 390 -73.7183/-67.0059/-62.3980 4.0318/11.7718/12.7473
|
||||
7 18 483 -74.7834/-71.1169/-67.5754 4.0317/7.3030/8.1412
|
||||
8 22 176 -75.3669/-69.9656/-63.4334 3.5327/3.5370/13.7478
|
||||
nearest_xy_boundary_tiles_by_lod:
|
||||
lod=1 key=(-2, -2, -4, 1) xy_distance_m=5.39 boundary_median_z=-60.4712 within_tile_p05_p95_m=0.0177 faces=2048
|
||||
lod=1 key=(-2, -1, -4, 1) xy_distance_m=11.18 boundary_median_z=-60.4718 within_tile_p05_p95_m=0.0158 faces=2048
|
||||
lod=1 key=(-1, -2, -4, 1) xy_distance_m=14.87 boundary_median_z=-60.4550 within_tile_p05_p95_m=0.0212 faces=2048
|
||||
lod=2 key=(-1, 1, -2, 2) xy_distance_m=67.27 boundary_median_z=-60.7421 within_tile_p05_p95_m=0.2955 faces=496
|
||||
lod=2 key=(1, -1, -3, 2) xy_distance_m=70.06 boundary_median_z=-74.2743 within_tile_p05_p95_m=2.9398 faces=97
|
||||
lod=2 key=(1, -1, -2, 2) xy_distance_m=70.06 boundary_median_z=-60.7387 within_tile_p05_p95_m=1.2725 faces=496
|
||||
lod=3 key=(-3, -1, -1, 3) xy_distance_m=138.61 boundary_median_z=-62.5408 within_tile_p05_p95_m=0.3465 faces=512
|
||||
lod=3 key=(-1, -3, -1, 3) xy_distance_m=141.35 boundary_median_z=-62.5378 within_tile_p05_p95_m=1.6699 faces=512
|
||||
lod=3 key=(-3, 0, -1, 3) xy_distance_m=147.12 boundary_median_z=-62.5837 within_tile_p05_p95_m=0.3165 faces=512
|
||||
lod=4 key=(-3, -1, -1, 4) xy_distance_m=301.38 boundary_median_z=-62.3784 within_tile_p05_p95_m=4.5901 faces=546
|
||||
lod=4 key=(-1, -3, -1, 4) xy_distance_m=303.92 boundary_median_z=-62.3534 within_tile_p05_p95_m=10.4664 faces=596
|
||||
lod=4 key=(-3, 0, -1, 4) xy_distance_m=309.34 boundary_median_z=-62.3696 within_tile_p05_p95_m=6.1327 faces=544
|
||||
lod=5 key=(-3, -1, -1, 5) xy_distance_m=627.54 boundary_median_z=-62.0651 within_tile_p05_p95_m=9.5826 faces=523
|
||||
lod=5 key=(-1, -3, -1, 5) xy_distance_m=629.98 boundary_median_z=-61.9414 within_tile_p05_p95_m=2.3891 faces=526
|
||||
lod=5 key=(-3, 0, -1, 5) xy_distance_m=635.24 boundary_median_z=-62.0996 within_tile_p05_p95_m=9.3777 faces=567
|
||||
lod=6 key=(-3, 0, -1, 6) xy_distance_m=1287.71 boundary_median_z=-69.5358 within_tile_p05_p95_m=4.1243 faces=26
|
||||
lod=6 key=(0, -3, -1, 6) xy_distance_m=1291.28 boundary_median_z=-70.4254 within_tile_p05_p95_m=12.7473 faces=41
|
||||
lod=6 key=(0, 2, -1, 6) xy_distance_m=1328.41 boundary_median_z=-63.9805 within_tile_p05_p95_m=0.0000 faces=1
|
||||
lod=7 key=(0, -3, -1, 7) xy_distance_m=2596.50 boundary_median_z=-70.2015 within_tile_p05_p95_m=4.1663 faces=36
|
||||
lod=7 key=(2, -1, -1, 7) xy_distance_m=2628.64 boundary_median_z=-70.1072 within_tile_p05_p95_m=3.5366 faces=8
|
||||
lod=7 key=(-3, 1, -1, 7) xy_distance_m=2976.49 boundary_median_z=-70.9568 within_tile_p05_p95_m=4.8223 faces=24
|
||||
lod=8 key=(-1, 2, -1, 8) xy_distance_m=5235.77 boundary_median_z=-65.8628 within_tile_p05_p95_m=3.5343 faces=8
|
||||
lod=8 key=(-1, -4, -1, 8) xy_distance_m=7218.88 boundary_median_z=-71.4663 within_tile_p05_p95_m=3.5332 faces=8
|
||||
lod=8 key=(-4, 0, -1, 8) xy_distance_m=7221.71 boundary_median_z=-70.6999 within_tile_p05_p95_m=3.5335 faces=8
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,13 @@
|
||||
version=1 voxel_um=250000 tiles=2370 camera_m=(-22.0, -19.0, -22.799999237060547) yaw=0.0 pitch=-4.0
|
||||
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
|
||||
0 512 15 30819 54860 9176 12102 -29.7482/-28.0275/-25.2555 0.5335/1.1856/1.9265
|
||||
1 448 64 94272 163840 32768 131072 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
||||
2 320 40 28476 45376 15296 20480 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
||||
3 208 48 58599 93736 30968 24576 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
||||
4 240 48 105724 164240 27638 22945 -60.0000/-60.0000/-60.0000 0.0000/0.9167/1.8008
|
||||
5 144 96 96543 156615 22706 109798 -506.3981/-502.5195/-60.0000 1.7900/110.1491/138.3581
|
||||
6 96 48 114212 157583 28194 91871 -506.2913/-503.3460/-411.9775 78.1123/124.2575/136.4059
|
||||
7 96 48 142940 234358 31690 95029 -506.1172/-503.3633/-412.8203 90.7064/105.1855/106.9902
|
||||
8 306 153 290960 421667 73118 147296 -505.8558/-503.1657/-422.5029 82.1507/93.3314/110.2594
|
||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
||||
key=(-2, -2, -4, 1) xy_distance_m=5.39 vertices=1473 triangles=2560 skirt_triangles=512 upward_triangles=2048 z_m=-60.000/-60.000/-60.000 tile_span_p05_p95_m=0.000
|
||||
@@ -0,0 +1,13 @@
|
||||
version=1 voxel_um=250000 tiles=2370 camera_m=(-22.0, -19.0, -22.799999237060547) yaw=0.0 pitch=-4.0
|
||||
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
|
||||
0 512 15 30819 54860 9176 12102 -29.7482/-28.0275/-25.2555 0.5335/1.1856/1.9265
|
||||
1 448 64 94272 163840 32768 131072 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
||||
2 320 40 28476 45376 15296 20480 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
||||
3 208 48 58599 93736 30968 24576 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
||||
4 240 48 105724 164240 27638 22945 -60.0000/-60.0000/-60.0000 0.0000/0.9167/1.8008
|
||||
5 144 96 96543 156615 22706 109798 -506.3981/-502.5195/-60.0000 1.7900/110.1491/138.3581
|
||||
6 96 48 114212 157583 28194 91871 -506.2913/-503.3460/-411.9775 78.1123/124.2575/136.4059
|
||||
7 96 48 142940 234358 31690 95029 -506.1172/-503.3633/-412.8203 90.7064/105.1855/106.9902
|
||||
8 306 153 290960 421667 73118 147296 -505.8558/-503.1657/-422.5029 82.1507/93.3314/110.2594
|
||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
||||
key=(-2, -2, -4, 1) xy_distance_m=5.39 vertices=1473 triangles=2560 skirt_triangles=512 upward_triangles=2048 z_m=-60.000/-60.000/-60.000 tile_span_p05_p95_m=0.000
|
||||
@@ -0,0 +1,19 @@
|
||||
version=1 voxel_um=250000 tiles=2370 camera_m=(-22.0, -19.0, -22.799999237060547) yaw=0.0 pitch=-4.0
|
||||
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
|
||||
0 512 143 588792 892284 293148 300091 3.5000/7.6738/8.2568 0.4215/4.4898/4.4913
|
||||
1 448 119 509571 954666 323268 265175 -60.4932/-60.4491/8.1722 0.3356/0.9926/4.2986
|
||||
2 320 130 93432 150346 51220 55675 -76.2021/-60.8983/8.0124 0.3035/2.9398/8.5642
|
||||
3 208 135 91227 146168 33040 51843 -70.7997/-62.4979/8.0125 1.0249/11.2411/19.6108
|
||||
4 240 103 173336 278284 61076 58421 -505.3757/-411.1270/-62.3657 8.2807/88.3431/118.1829
|
||||
5 144 96 137096 173158 40930 117557 -506.3330/-502.1182/-62.4087 10.4471/110.1491/138.5524
|
||||
6 96 48 151997 168987 43320 92193 -506.3014/-503.3398/-410.9469 78.1123/124.2575/136.4059
|
||||
7 96 96 236120 321403 61348 120078 -505.8770/-502.4482/8.0020 0.0000/103.3187/110.4079
|
||||
8 306 306 553491 741221 166096 225866 -505.4486/-472.2318/8.0020 0.0000/92.1234/110.0400
|
||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
||||
key=(-3, -3, 0, 0) xy_distance_m=2.24 vertices=8058 triangles=11660 skirt_triangles=3796 upward_triangles=3768 z_m=3.500/3.500/7.999 tile_span_p05_p95_m=4.445
|
||||
key=(-3, -3, 1, 0) xy_distance_m=2.24 vertices=668 triangles=1052 skirt_triangles=376 upward_triangles=676 z_m=8.000/8.087/8.252 tile_span_p05_p95_m=0.203
|
||||
key=(-2, -2, -4, 1) xy_distance_m=5.39 vertices=2562 triangles=8192 skirt_triangles=2048 upward_triangles=2048 z_m=-60.480/-60.471/-60.457 tile_span_p05_p95_m=0.018
|
||||
key=(-4, -3, 0, 0) xy_distance_m=6.08 vertices=7502 triangles=10753 skirt_triangles=3764 upward_triangles=2893 z_m=3.500/3.500/8.000 tile_span_p05_p95_m=4.486
|
||||
key=(-4, -3, 1, 0) xy_distance_m=6.08 vertices=1617 triangles=2507 skirt_triangles=928 upward_triangles=1579 z_m=8.000/8.099/8.372 tile_span_p05_p95_m=0.248
|
||||
key=(-3, -2, 0, 0) xy_distance_m=7.28 vertices=7772 triangles=10994 skirt_triangles=4024 upward_triangles=2874 z_m=3.500/3.500/8.000 tile_span_p05_p95_m=4.491
|
||||
key=(-3, -2, 1, 0) xy_distance_m=7.28 vertices=1883 triangles=2904 skirt_triangles=1116 upward_triangles=1788 z_m=8.000/8.115/8.363 tile_span_p05_p95_m=0.280
|
||||
@@ -0,0 +1,19 @@
|
||||
version=1 voxel_um=250000 tiles=2370 camera_m=(-11.612443923950195, -118.22123718261719, -19.84520721435547) yaw=0.0 pitch=0.0
|
||||
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
|
||||
0 512 128 579561 840946 290676 288126 3.5000/7.7495/8.2586 0.3781/4.4867/4.4932
|
||||
1 448 124 503677 964366 331082 258850 -63.8505/-60.4908/8.1722 0.2965/2.9282/5.7538
|
||||
2 320 127 83460 135059 46994 53146 -71.5347/-60.5904/8.0124 0.5224/2.7093/6.0349
|
||||
3 208 126 93575 151669 35042 52200 -75.6995/-61.9814/8.0125 0.5630/13.5938/55.4526
|
||||
4 240 103 172283 277754 60622 58086 -505.4072/-410.6667/-62.3188 6.7752/88.3431/118.1829
|
||||
5 144 96 137096 173158 40930 117557 -506.3330/-502.1182/-62.4087 10.4471/110.1491/138.5524
|
||||
6 96 48 151997 168987 43320 92193 -506.3014/-503.3398/-410.9469 78.1123/124.2575/136.4059
|
||||
7 96 96 236120 321403 61348 120078 -505.8770/-502.4482/8.0020 0.0000/103.3187/110.4079
|
||||
8 306 306 553491 741221 166096 225866 -505.4486/-472.2318/8.0020 0.0000/92.1234/110.0400
|
||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
||||
key=(-2, -15, 0, 0) xy_distance_m=2.25 vertices=7941 triangles=11569 skirt_triangles=3860 upward_triangles=3613 z_m=3.500/3.500/8.000 tile_span_p05_p95_m=4.472
|
||||
key=(-2, -15, 1, 0) xy_distance_m=2.25 vertices=1159 triangles=1557 skirt_triangles=684 upward_triangles=873 z_m=8.000/8.082/8.275 tile_span_p05_p95_m=0.250
|
||||
key=(-1, -8, -4, 1) xy_distance_m=4.03 vertices=2562 triangles=8192 skirt_triangles=2048 upward_triangles=2048 z_m=-60.494/-60.494/-60.493 tile_span_p05_p95_m=0.001
|
||||
key=(-2, -16, 0, 0) xy_distance_m=5.79 vertices=8315 triangles=12274 skirt_triangles=3928 upward_triangles=4250 z_m=3.500/7.740/8.000 tile_span_p05_p95_m=4.459
|
||||
key=(-2, -16, 1, 0) xy_distance_m=5.79 vertices=677 triangles=700 skirt_triangles=520 upward_triangles=180 z_m=8.000/8.008/8.057 tile_span_p05_p95_m=0.036
|
||||
key=(-1, -15, 0, 0) xy_distance_m=7.93 vertices=6918 triangles=9878 skirt_triangles=3384 upward_triangles=2398 z_m=3.500/3.500/8.000 tile_span_p05_p95_m=4.453
|
||||
key=(-1, -15, 1, 0) xy_distance_m=7.93 vertices=1611 triangles=2700 skirt_triangles=652 upward_triangles=2048 z_m=8.001/8.202/8.323 tile_span_p05_p95_m=0.242
|
||||
@@ -0,0 +1,12 @@
|
||||
version=1 voxel_um=250000 tiles=2553 camera_m=(0.0, -35.0, -487.94000244140625) yaw=0.0 pitch=-22.0
|
||||
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
|
||||
0 512 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
||||
1 448 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
||||
2 448 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
||||
3 384 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
||||
4 320 52 139437 209316 34996 8129 -56.2002/-42.7865/-32.7998 6.2138/32.5482/53.6770
|
||||
5 144 102 100254 158707 25586 96013 -506.9092/-503.1813/-378.0397 12.8110/109.5153/138.3581
|
||||
6 96 61 110399 149031 17088 92803 -506.4873/-503.3776/-412.3926 53.2054/124.2575/136.4059
|
||||
7 48 48 132222 219052 16356 95003 -506.1172/-503.3639/-412.8861 90.7064/105.1855/106.9902
|
||||
8 153 153 229080 384768 82518 147226 -505.8568/-503.1676/-422.8789 82.0687/93.3180/110.0400
|
||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
||||
@@ -0,0 +1,5 @@
|
||||
version=1 voxel_um=250000 tiles=960 camera_m=(-11.612443923950195, -118.22123718261719, -506.7861328125) yaw=0.0 pitch=0.0
|
||||
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
|
||||
0 512 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
||||
1 448 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
||||
@@ -0,0 +1,5 @@
|
||||
version=1 voxel_um=250000 tiles=960 camera_m=(0.0, -35.0, -75.0) yaw=0.0 pitch=0.0
|
||||
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
|
||||
0 512 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
||||
1 448 76 153596 265964 51168 82938 -43.9418/-14.9892/-6.3079 0.8803/5.3022/10.8345
|
||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
||||
@@ -0,0 +1,5 @@
|
||||
version=1 voxel_um=250000 tiles=960 camera_m=(0.0, -35.0, -42.0) yaw=0.0 pitch=-8.0
|
||||
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
|
||||
0 512 25 80260 139176 28088 34692 -43.9640/-33.5509/-32.8013 0.7072/4.9851/7.0117
|
||||
1 448 131 295583 474122 103438 230953 -33.6783/3.9507/4.1428 0.3514/4.4247/10.8345
|
||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
if len(sys.argv) < 4 or sys.argv[2] != "--":
|
||||
raise SystemExit("usage: MeasureProcess.py LOG -- COMMAND [ARGS...]")
|
||||
|
||||
log_path = sys.argv[1]
|
||||
command = sys.argv[3:]
|
||||
os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
|
||||
start = time.monotonic()
|
||||
with open(log_path, "wb") as log_file:
|
||||
process = subprocess.Popen(command, stdout=log_file, stderr=subprocess.STDOUT,
|
||||
start_new_session=True)
|
||||
max_hwm_kib = 0
|
||||
max_rss_kib = 0
|
||||
max_pid = process.pid
|
||||
max_name = ""
|
||||
deadline = start + 1800.0
|
||||
while process.poll() is None:
|
||||
if time.monotonic() >= deadline:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
try:
|
||||
process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
process.wait()
|
||||
print("MONITOR_TIMEOUT_SECONDS=1800", file=log_file, flush=True)
|
||||
break
|
||||
stat_by_pid = {}
|
||||
for entry in os.scandir("/proc"):
|
||||
if not entry.name.isdigit():
|
||||
continue
|
||||
pid = int(entry.name)
|
||||
try:
|
||||
with open(f"/proc/{pid}/stat", "r", encoding="ascii") as stat_file:
|
||||
fields = stat_file.read().rsplit(")", 1)[1].split()
|
||||
ppid = int(fields[1])
|
||||
stat_by_pid[pid] = ppid
|
||||
except (OSError, ValueError, IndexError):
|
||||
continue
|
||||
descendants = {process.pid}
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for pid, ppid in stat_by_pid.items():
|
||||
if ppid in descendants and pid not in descendants:
|
||||
descendants.add(pid)
|
||||
changed = True
|
||||
for pid in descendants:
|
||||
try:
|
||||
with open(f"/proc/{pid}/status", "r", encoding="ascii") as status_file:
|
||||
status = status_file.read().splitlines()
|
||||
values = {line.split(":", 1)[0]: line.split(":", 1)[1].strip()
|
||||
for line in status if ":" in line}
|
||||
hwm = int(values.get("VmHWM", "0 kB").split()[0])
|
||||
rss = int(values.get("VmRSS", "0 kB").split()[0])
|
||||
name = values.get("Name", "")
|
||||
except (OSError, ValueError, IndexError):
|
||||
continue
|
||||
if hwm > max_hwm_kib:
|
||||
max_hwm_kib, max_pid, max_name = hwm, pid, name
|
||||
max_rss_kib = max(max_rss_kib, rss)
|
||||
time.sleep(0.05)
|
||||
result = process.wait()
|
||||
elapsed = time.monotonic() - start
|
||||
for line in (
|
||||
f"MONITOR_EXIT_CODE={result}",
|
||||
f"MONITOR_WALL_SECONDS={elapsed:.3f}",
|
||||
f"MONITOR_PEAK_PROCESS_HWM_KIB={max_hwm_kib}",
|
||||
f"MONITOR_PEAK_PROCESS_PID={max_pid}",
|
||||
f"MONITOR_PEAK_PROCESS_NAME={max_name}",
|
||||
f"MONITOR_MAX_SAMPLED_PROCESS_RSS_KIB={max_rss_kib}",
|
||||
):
|
||||
log_file.write((line + "\n").encode("utf-8"))
|
||||
raise SystemExit(result)
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
# World Assembler M3 — Linux L5
|
||||
|
||||
**Result:** no octave tail is safely removable under the declared bounds. The exact far meshes already exceed the 1 px field-error and LOD-swap budgets, and the cutoff flag is 2.2% slower in the W12 view. The candidate therefore generated the exact same meshes; recipe version stays 1.
|
||||
|
||||
## Build and machine
|
||||
|
||||
Built `VoxelForgeCoreHeadless-linux5-l5-final7` and its capture target on `assembler/world` at `7770074` with Clang 19 `-ffp-contract=off`, then GCC 14 default contraction, both with 12 jobs. W1/W12 hashes match across both compilers; the L3 FP-contract preamble and runtime self-test remain active. Runs used CPUs 0–11, normal priority, and the 6 GiB process limit. Maximum process HWM was 898,692 KiB (GCC compile); the largest runtime process was 454,828 KiB. CPU governor stayed `powersave`. `/dev/dri/renderD128` was absent, so EGL used Mesa llvmpipe with `LP_NUM_THREADS=12`. No packages or system settings were changed.
|
||||
|
||||
## Capture and gate
|
||||
|
||||
`LinuxOffscreenCapture.cpp` reads the `.vfmesh` export from the headless streaming pipeline, creates a surfaceless EGL/GLES3 pbuffer, and writes 2560×1440 PNGs with libpng. `ffmpeg` made 1280×720 review JPEGs; the largest is 73,923 bytes. The shader combines surface normals and 80 m height bands. Only the small JPEGs were opened for review.
|
||||
|
||||
The gate samples a 65×65 XY grid on the closest native-surface tile at each LOD, excluding skirts. It reports two comparisons: displayed mesh against exact `Heightfield2D` ground/cap roots, and against a regenerated exact same-LOD mesh. The second is the zero-delta regression control; the first measures the actual field error. Swap error compares the coarse tile with its fine children at those same field roots. Full missing-surface, 2 m floating-surface, and missing fine-ring injections were rejected at every LOD3–8; so were both S13 controls.
|
||||
|
||||
This is a sampled heightfield gate, not a full camera-ray rasterizer. It can miss cracks narrower than one grid cell (1 m at LOD3, growing to 32 m at LOD8), and its direct-field row does not model tunnel walls or other non-heightfield faces. The same-LOD mesh comparison covers extra and missing mesh intersections at sampled points; a denser or full-screen geometry pass would be needed to certify sub-cell cracks and non-heightfield surfaces.
|
||||
|
||||
## Closest display distances and budgets
|
||||
|
||||
The code sweeps clipmap desired sets across X-boundary phases and measures tile AABB distance. The result matches the clipmap ring formula `3 × 32 chunks × 2^(LOD−1) × 0.25 m`; LOD0 is the player tile. The pixel angle is `90° / 2560 = 0.000613592 rad`.
|
||||
|
||||
| LOD | Closest display distance | 1 px at that distance | Active rule |
|
||||
|---:|---:|---:|---:|
|
||||
| 0 | 0 m | 0 cm | exact, 2 cm |
|
||||
| 1 | 24.001 m | 1.473 cm | exact, 2 cm |
|
||||
| 2 | 48.001 m | 2.945 cm | exact, 2 cm |
|
||||
| 3 | 96.001 m | 5.891 cm | 1 px |
|
||||
| 4 | 192.001 m | 11.781 cm | 1 px |
|
||||
| 5 | 384.001 m | 23.562 cm | 1 px |
|
||||
| 6 | 768.001 m | 47.124 cm | 1 px |
|
||||
| 7 | 1,536.001 m | 94.248 cm | 1 px |
|
||||
| 8 | 3,072.001 m | 188.496 cm | 1 px |
|
||||
|
||||
## Exact-field error and swap pop
|
||||
|
||||
Baseline and cutoff values are identical. The exact same-LOD regression control is 0.0000 px p50/p99/max at all LOD3–8, with no missing or floating samples. Against the continuous `Heightfield2D` roots, however, the current exact meshes already exceed 1 px at every LOD. The owner’s expected near-zero control holds for exact-versus-exact mesh output, not for the true-field comparison.
|
||||
|
||||
| LOD | Exact field p50 / p99 / max px (baseline → cutoff) | Swap p50 / p99 / max px | Swap cracks / floating |
|
||||
|---:|---:|---:|---:|
|
||||
| 3 → 2 | 0.9035 / 4.5839 / 7.3277 → same | 0.6714 / 4.1861 / 7.3277 | 0 / 0 |
|
||||
| 4 → 3 | 1.1170 / 5.6056 / 8.3307 → same | 0.8269 / 5.1958 / 8.2105 | 0 / 0 |
|
||||
| 5 → 4 | 1.5667 / 45.3010 / 126.2667 → same | 0.0000 / 20.7227 / 63.2266 | 0 / 0 |
|
||||
| 6 → 5 | 0.5870 / 21.2445 / 82.3615 → same | 0.0000 / 21.2445 / 82.3615 | 0 / 0 |
|
||||
| 7 → 6 | 0.3393 / 16.4341 / 47.9760 → same | 0.0000 / 16.4341 / 47.9760 | 0 / 0 |
|
||||
| 8 → 7 | 0.9616 / 24.9479 / 50.0934 → same | 0.9616 / 24.9479 / 50.0934 | 0 / 0 |
|
||||
|
||||
The baseline gate rejects on the field row and every swap row. The cutoff run returns the same rejection. Fault injections also rejected: missing-surface samples were 4,225–13,642; floating-surface samples 4,225–13,641; missing fine-ring cracks 4,225–8,450.
|
||||
|
||||
| LOD | S13 octave-drop p99 / max px | Stride-2 column interpolation p99 / max px |
|
||||
|---:|---:|---:|
|
||||
| 3 | 960.2333 / 998.8654 | 9.3917 / 13.9093 |
|
||||
| 4 | 3.7405 / 5.1103 | 9.2115 / 15.3435 |
|
||||
| 5 | 105.7114 / 129.3089 | 125.1435 / 191.6424 |
|
||||
| 6 | 218.7777 / 749.9535 | 82.1098 / 148.9485 |
|
||||
| 7 | 95.1867 / 129.1796 | 40.6881 / 80.6012 |
|
||||
| 8 | 38.1461 / 282.3288 | 28.8473 / 278.6236 |
|
||||
|
||||
Both controls were rejected at every LOD3–8. The octave-drop case produced missing samples at LOD3, 5–8 and floating samples at LOD5–8; stride-2 interpolation produced missing/floating samples at LOD5, 7 and 8.
|
||||
|
||||
## Cutoff contract and detail cost
|
||||
|
||||
The version-2 shared visual-detail contract declares stable octave terms with wavelength, octave count, displacement bound, and noise bound. A suffix is eligible only when its wavelengths are undersampled and its full displacement bound fits within one sixty-fourth of the tile’s pixel budget. Skipped zero-mean terms retain the original normalization denominator. Recipes with no terms keep the exact path; `FloatingIslandFieldV1` declares none, and its baseline/cutoff LOD hashes match. This is separate from content recipe version 1, which stays unchanged because output did not change.
|
||||
|
||||
SurfaceWorld declares 17 terms per LOD. No term or octave tail qualifies at LOD3–8. Sample steps are 16, 32, 64, 128, 256, and 512 voxels respectively. The bounds explain the result: even the smallest declared maximum displacement is 256 voxels, against a 0.2356-voxel 1 px budget at LOD3. The cutoff skips zero octaves, so eligible octave time is 0 ms.
|
||||
|
||||
| Octave | Terms | Wavelength range (voxels) | Max displacement range (voxels) |
|
||||
|---:|---:|---:|---:|
|
||||
| 0 | 17 | 25–10,000 | 1,024–640,000 |
|
||||
| 1 | 17 | 12.5–5,000 | 512–320,000 |
|
||||
| 2 | 15 | 6.25–1,250 | 256–160,000 |
|
||||
| 3 | 4 | 41.664–250 | 3,968–24,800 |
|
||||
|
||||
W12 profile from the exact baseline; octave milliseconds are summed across evaluated calls/workers. They are all measured detail work, not removable work under the bound above.
|
||||
|
||||
| LOD | Worker sum s | Octave 0 ms | Octave 1 ms | Octave 2 ms | Octave 3 ms | Detail total ms (% worker sum) | Eligible ms |
|
||||
|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 3 | 1.660 | 71.585 | 69.911 | 68.535 | 15.407 | 225.437 (13.58%) | 0 |
|
||||
| 4 | 3.321 | 135.512 | 131.224 | 128.645 | 26.506 | 421.887 (12.70%) | 0 |
|
||||
| 5 | 2.283 | 82.131 | 80.450 | 77.545 | 16.262 | 256.388 (11.23%) | 0 |
|
||||
| 6 | 3.587 | 153.409 | 148.854 | 144.175 | 29.984 | 476.422 (13.28%) | 0 |
|
||||
| 7 | 1.447 | 35.301 | 34.493 | 33.138 | 7.069 | 110.001 (7.60%) | 0 |
|
||||
| 8 | 2.685 | 58.763 | 57.751 | 56.277 | 11.848 | 184.638 (6.88%) | 0 |
|
||||
|
||||
The cutoff is recipe-generic, display-mesh-only, and activated only for LOD3+. Exact density, detail, collision, edit, and walkability paths remain outside it. LOD0–2 keep the 2 cm path. ARCHITECTURE §8.10 invariants remain: lazy thread-local caches, two-pass MC, SSE noise, and clipmap streaming. The cutoff state is scoped to mesh generation under the async-safety rules.
|
||||
|
||||
## W12 speed, interleaved with L4
|
||||
|
||||
Three L4/candidate pairs, all W12, measured on the L1 camera. The candidate is the final build with `--visual-detail-cutoff`.
|
||||
|
||||
| Measure (median) | L4 binary | Candidate | Change |
|
||||
|---|---:|---:|---:|
|
||||
| Whole-view ready | 2.183 s | 2.232 s | +2.2% |
|
||||
| Stream wall | 1.895 s | 1.963 s | +3.6% |
|
||||
| Far worker sum, LOD3–8 | 12.247 s | 12.458 s | +1.7% |
|
||||
|
||||
| LOD | L4 worker sum s | Candidate worker sum s |
|
||||
|---:|---:|---:|
|
||||
| 0 | 3.799 | 4.094 |
|
||||
| 1 | 5.304 | 5.573 |
|
||||
| 2 | 1.281 | 1.340 |
|
||||
| 3 | 1.102 | 1.138 |
|
||||
| 4 | 2.723 | 2.719 |
|
||||
| 5 | 1.898 | 1.976 |
|
||||
| 6 | 2.875 | 2.944 |
|
||||
| 7 | 1.243 | 1.282 |
|
||||
| 8 | 2.355 | 2.400 |
|
||||
|
||||
There is no speed benefit because nothing qualifies for skipping; the small regression is cutoff setup and bookkeeping.
|
||||
|
||||
## Hashes and captures
|
||||
|
||||
Baseline and cutoff hashes match for all LOD0–8. The old L4 binary’s aggregate hashes also match: whole view `c10697a8f48380c7 / e75c9119a1498cd5 / 49a9a8d1835a6c8c / edcb23288b5e8724` and full detail `0ee0579bd15cbbe4 / 32e5d4964d610fee / 1932b96ee4b1d910 / 69743c6bad3447dc`. The recipe stays version 1 because output did not change.
|
||||
|
||||
| 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 |
|
||||
|
||||
Nine direct-Core query probes also match baseline/cutoff and W1/W12 Clang/GCC: density and unclamped `e4d4a8eb95b35933`, detail `efcba20c0c70c415`, collision `e604823a249029bf`.
|
||||
|
||||
| Before / after pair | View |
|
||||
|---|---|
|
||||
| `before_l1.jpg` / `after_l1.jpg` | Exact L1 camera `0,-35,-487.94,0,-22` |
|
||||
| `before_horizon.jpg` / `after_horizon.jpg` | Same position, pitch −2° |
|
||||
| `before_boundary_lod2_3.jpg` / `after_boundary_lod2_3.jpg` | 20 m above local heightfield, aimed at the 96 m LOD3 entry band |
|
||||
|
||||
The boundary camera is `0,-35,-480.77246,0,-12`; the local heightfield is at −500.77246 m, placing the ground intersection about 96 m away.
|
||||
|
||||
The six review JPEGs are byte-identical within each pair. PNGs are in `Saved/Linux/captures/`; JPEGs are in `Linux5/captures/`.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- The first headless invocation omitted required streaming flags; the next used the default binary-relative data path and could not open the owner JSON. Both were retried with all flags and the explicit `Standalone/data/owner_world.json` path.
|
||||
- The first process-monitor wrapper errored after its Clang build had completed; the run was repeated with `MeasureProcess.py` to record process HWM and duration.
|
||||
- The first capture named a nonexistent mesh path and exited 2; the exported `.vfmesh` path was corrected.
|
||||
- The first gate build failed on a duplicate `Distances` local; the corrected build passed. A first stride-2 control changed the MC lattice instead of reproducing S13’s every-second-column interpolation; the final control uses bilinear column interpolation.
|
||||
- Initial comparison against only the continuous field mixed surface interpolation with unrelated tunnel intersections. The final instrument reports exact heightfield roots separately from the exact-mesh regression and ring checks.
|
||||
- Early W12 cutoff candidates took 2.70–2.91 s. The zero-eligible path and distance setup were moved off the expensive profiling/phase-sweep path; the final interleaved candidate median is 2.232 s. It still does not improve L4.
|
||||
- One final GCC W12 command omitted `--stream-pipeline` and exited 2; the corrected rerun passed. The initial −50° boundary capture mostly showed near ground; the final pair uses the camera listed above.
|
||||
- The final baseline and cutoff gate commands exit 6 by design: both correctly reject the exact-field error and swap pops. No process exceeded 30 minutes or stalled for 15 minutes.
|
||||
|
||||
Core/API changes are in `VoxelForgeCore.h`, `WorldAssemblerInternal.h`, `FloatingIslandsInternal.h`, and `VoxelForgeCore.cpp`. Standalone changes are in `StreamingWorld.h`, `StreamBenchmarkPipeline.h`, `StreamBenchmarkHeadless.cpp`, `LinuxScreenSpaceGate.h`, `LinuxOffscreenCapture.cpp`, and `BuildLinux.sh`; process measurements are in `MeasureProcess.py`.
|
||||
|
||||
**For the owner:** Far terrain will look exactly as it does now because no octave tail met the bound. Existing far LOD swaps exceed the 1 px target, so the mesh sampling and transition error need attention before octave skipping can be used.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- **Not merged into `assembler/world`:** kept on side branch `wip/l5-far-cutoff`. With the cutoff flag OFF,
|
||||
the L5 build is slower than L4. My interleaved W12 runs: L4 2.054/2.097/2.179 s vs L5 `final7`
|
||||
2.127/2.193/2.208 s (medians 2.097 → 2.193, +4.6%), same world. Cost and no benefit.
|
||||
- **Instrument caveat:** the gate prints `metric=vertical_height_error`. On steep slopes and cliffs, a
|
||||
small horizontal offset becomes a large vertical error, so vertical error overstates screen
|
||||
displacement. p50 is ~1 px but p99/max reach 20–126 px, the pattern such an artifact produces. The
|
||||
claims "today's far meshes exceed 1 px" and "LOD swaps pop by up to 126 px" are therefore NOT yet
|
||||
established. They need a screen-space measure: error along the surface normal or along camera rays,
|
||||
projected at the display distance.
|
||||
- The cutoff criterion (full displacement supremum ≤ 1/64 px) cannot pass for any SurfaceWorld octave
|
||||
(smallest bound 256 voxels vs 0.24), so the null result was decided by the criterion, not measured.
|
||||
- The capture path (EGL/GLES pbuffer; llvmpipe, since the sandbox hides `/dev/dri`) works and is worth keeping.
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,102 @@
|
||||
# Linux6 report
|
||||
|
||||
## Result
|
||||
|
||||
The screen-space gate rejects today’s far meshes at every sampled LOD3–8: the largest measured same-field error is 92.0 px, and the largest sampled swap pop is 70.3 px at LOD6→5. Same-LOD regeneration is exact, the reported swaps have no cracks or floaters, and no display mesh changed in this round.
|
||||
|
||||
Source base: `wip/l5-far-cutoff` at `b21a19d`; L4 reference: `7770074`.
|
||||
|
||||
## Screen-space method
|
||||
|
||||
For one surface tile nearest the L1 camera at each LOD, the gate rasterizes triangles on a 65×65 XY grid, excluding skirts. It compares the mesh hit to direct `Heightfield2D` ground/cap roots; slopes use ±1-voxel finite differences. For vertical residual `Δz` and slopes `sx,sy`, it computes normal distance `|Δz|/sqrt(1+sx²+sy²)`, projects that normal onto the camera’s screen plane, then divides by the one-pixel world distance at that LOD’s closest display distance. The view uses 90° horizontal FOV, 2560 px, yaw 0°, pitch −22°; one pixel is 0.000613592 rad. Same-LOD regeneration is the mesh reference for swaps; direct field roots are the reference for true surface error.
|
||||
|
||||
The known failure control is a 1 m cell on an 89.427° plane, shifted sideways by 5 mm (0.005 cell): its vertical metric says 8.488 px, while its true normal distance is 5 mm and projected screen error is 0.0849 px. This confirms that vertical height can badly overstate visible displacement.
|
||||
|
||||
| LOD | Closest distance (m) | Field error p50 / p99 / max (px) | Max normal distance (m) |
|
||||
|---:|---:|---:|---:|
|
||||
| 3 | 96 | 0.81 / 4.16 / 6.00 | 0.423 |
|
||||
| 4 | 192 | 1.01 / 5.12 / 7.93 | 0.952 |
|
||||
| 5 | 384 | 0.79 / 16.37 / 92.02 | 25.122 |
|
||||
| 6 | 768 | 0.51 / 9.75 / 70.34 | 37.644 |
|
||||
| 7 | 1,536 | 0.29 / 9.04 / 42.36 | 42.714 |
|
||||
| 8 | 3,072 | 0.83 / 18.03 / 47.18 | 93.175 |
|
||||
|
||||
| Swap | Distance (m) | Pop p50 / p99 / max (px) | Max normal distance (m) |
|
||||
|---|---:|---:|---:|
|
||||
| 3→2 | 96 | 0.61 / 3.77 / 6.05 | 0.367 |
|
||||
| 4→3 | 192 | 0.75 / 4.63 / 7.51 | 0.896 |
|
||||
| 5→4 | 384 | 0.00 / 9.58 / 31.78 | 8.139 |
|
||||
| 6→5 | 768 | 0.00 / 9.75 / 70.34 | 37.644 |
|
||||
| 7→6 | 1,536 | 0.00 / 9.04 / 42.36 | 42.714 |
|
||||
| 8→7 | 3,072 | 0.83 / 18.03 / 47.18 | 93.175 |
|
||||
|
||||
The tails are not confined to cliffs. The field maximum slopes at LOD5–8 are 8.34°, 14.09°, 6.76°, and 9.33°; LOD5’s p99 is 19.8 px below 30° and 21.1 px at 45–60°. The sampled LOD6→5 maximum is at `(-472, -1432, -463.151) m`; [the marked offscreen capture](captures/lod6_to_5_max_pop.jpg) shows that location at the 768 m swap distance. This is a sampled-tile result, not a whole-world supremum.
|
||||
|
||||
All same-LOD mesh comparisons at LOD3–8 pass with zero error. The S13 octave-drop and corrected stride-2 column-interpolation controls are rejected at every LOD3–8, as are missing displayed surface, 2 m floating surface, and missing fine-ring injections. The measured swap rows have zero cracks and floaters; they reject solely on displacement.
|
||||
|
||||
## Far worker time, W12
|
||||
|
||||
The L1 view has 2553 tiles. Clean-run phase totals for LOD3–8 are shown below. A separate temporary timer measured complete `FractalNoiseQ16` calls; “column outside” is clean column time minus that subtotal. The timer added 3–6% to worker time, so the split between those two column subphases is approximate; it has been removed from the final tree.
|
||||
|
||||
| LOD | Worker (s) | Column outside noise (ms) | Fractal/noise calls (ms) | MC (ms) | Ground refine (ms) | Cap refine (ms) | Skirts (ms) | Assembly (ms) |
|
||||
|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 3 | 1.084 | 171 | 276 | 473 | 0 | 0 | 118 | 33 |
|
||||
| 4 | 2.581 | 1,000 | 534 | 440 | 0 | 304 | 196 | 102 |
|
||||
| 5 | 1.829 | 658 | 358 | 100 | 464 | 22 | 161 | 70 |
|
||||
| 6 | 2.761 | 1,608 | 284 | 59 | 385 | 58 | 162 | 70 |
|
||||
| 7 | 1.192 | 234 | 198 | 32 | 275 | 202 | 153 | 96 |
|
||||
| 8 | 2.224 | 264 | 469 | 256 | 424 | 299 | 333 | 177 |
|
||||
| **Total** | **11.671** | **3,934** | **2,119** | **1,360** | **1,548** | **885** | **1,123** | **548** |
|
||||
|
||||
Column work outside the fractal calls is the largest single phase (33.7% of far worker time); all column work is 51.9%. Ground and cap refinement total 2.43 worker-s, skirts 1.12 worker-s, MC 1.36 worker-s, and assembly 0.55 worker-s. The earlier 7–13% profile timed individual Perlin calls; the L6 subtotal times complete fractal calls, including their octave accumulation.
|
||||
|
||||
The listed subphases total 11.52 worker-s; about 0.15 worker-s remains in timer and per-tile residuals.
|
||||
|
||||
The overlap scan counted about 411,000 cross-worker neighboring duplicate XY evaluations at LOD3–8 (about 166,000 at LOD4 alone). Exact column reuse is the largest plausible lever because it targets the largest phase. I did not build it: no candidate has yet shown a screen-gated, greater-than-3% whole-view win. Stride-2 interpolation is not a safe substitute; its control fails at every far LOD.
|
||||
|
||||
## Cost with the feature off
|
||||
|
||||
Three interleaved W12 runs per binary, same L1 camera and forced generation. `whole_view_ready` is the comparison metric; the six whole-view hashes matched exactly.
|
||||
|
||||
| Binary | Runs (s) | Median |
|
||||
|---|---|---:|
|
||||
| L4 reference | 2.082, 2.180, 2.183 | 2.180 s |
|
||||
| L6 cleaned tree | 2.127, 2.185, 2.223 | 2.185 s (+0.2%) |
|
||||
|
||||
Run-to-run spread was about 0.10 s, so the 5 ms median difference is within noise. The no-op visual cutoff contract, plan, and CLI flag were removed. The opt-in research gate retains its negative controls; skirt timing uses the existing optional stage-timing pointer, which is null on the default path. Governor stayed `powersave`; runs used CPUs 0–11, W12, 6 GiB address-space limits, and normal priority. Peak runtime RSS was 403 MiB; peak build process RSS was 874 MiB (GCC).
|
||||
|
||||
## Determinism and hashes
|
||||
|
||||
Clang 19 `-ffp-contract=off` W1/W12 and GCC default W1/W12 produced the same hashes. LOD0–2, exact queries, and all whole-view hashes equal L4; no recipe bump is needed.
|
||||
|
||||
- Recipe / plan / manifest: `83b616a3f4fe781d` / `1ca84619c8600ee5` / `e55991385bf0ca29` (recipe version 1).
|
||||
- Whole view (density / mesh / payload / topology): `c10697a8f48380c7` / `e75c9119a1498cd5` / `49a9a8d1835a6c8c` / `edcb23288b5e8724`.
|
||||
- Full detail (density / mesh / payload / topology): `0ee0579bd15cbbe4` / `32e5d4964d610fee` / `1932b96ee4b1d910` / `69743c6bad3447dc`.
|
||||
- Exact queries (density / unclamped / detail / collision): `e4d4a8eb95b35933` / `e4d4a8eb95b35933` / `efcba20c0c70c415` / `e604823a249029bf`.
|
||||
- SHA-256 L4 binary / Clang L6 binary / GCC L6 binary: `6efb8f19a41ca3cd23aca9294ceee1e9cdd1d3dc8da23b3ee61ef8ae943aa763` / `3f5a4f88b0d911e8e66af9ef72e294926a5507a73d44fcfe868df27c666bf01e` / `de872e8fdaa9f40e8a6c096700fd998bcf35f232b06f2b4e35a374b4b0e9fa07`.
|
||||
- Capture JPEG SHA-256: `483b434f3ad752dd09b82cd1c78fe81ee8cd31803ecf4dcd3c2918a8ae5d66b1` (156 KiB).
|
||||
|
||||
## Failed or retried runs
|
||||
|
||||
- First Clang build failed on a missing view argument in `LinuxScreenSpaceGate.h`; fixed and rebuilt successfully.
|
||||
- First gate pass paired extra mesh hits incorrectly and reported same-LOD floaters at LOD5–8. The reference pairing was corrected; the final gate reports zero same-LOD displacement and no floaters.
|
||||
- A `/usr/bin/time -v` wrapper attempt exited 127 because that binary is unavailable; the runs were retried with `/proc` RSS monitoring.
|
||||
- The final quality gates exit 6 by design because measured displacement exceeds the 1 px rule. Builds, captures, determinism runs, and cost runs completed successfully. EGL could not write `~/.cache` in this environment, disabled shader caching, and rendered successfully on llvmpipe.
|
||||
|
||||
## Note for the owner
|
||||
|
||||
Today’s `assembler/world` remains at L4; this sampled view measures up to 92.0 px from the field and a 70.3 px LOD6→5 pop at the tested display distances, with the largest point marked in the capture. This round changes no default mesh or recipe output, removes the no-op cutoff, and leaves W12 cost within run-to-run noise.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Cost re-run by me (interleaved W12, L1 view): L4 2.098/2.138/2.171 s vs L6 2.116/2.150/2.197 s
|
||||
(median +0.6%, within noise), density `c10697a8f48380c7`; the L6 binary SHA matches the report.
|
||||
Merged into `assembler/world` with L5 squashed in.
|
||||
- The marked LOD6→5 maximum sits on the **rim of a flat-topped hill (mesa edge)**, not on open gentle
|
||||
ground: the "8–14° slope" is the field slope at the sample, on the plateau top beside the drop. The
|
||||
coarse mesh rounds the rim by about a cell (16–32 m at LOD5–6), so the pop is a real **silhouette snap
|
||||
at sharp rims** (tens of px at 768 m), while p50 stays at 0 px. That is ordinary LOD-switch behaviour
|
||||
without geomorphing or cross-fade, so a fix belongs in rendering (Unreal), not in generation. The
|
||||
owner decides whether it is visible enough to matter.
|
||||
- Next speed lever, per the far breakdown: exact cross-tile column reuse (column work outside noise is
|
||||
34% of far worker time; ~411k duplicate XY evaluations at LOD3–8).
|
||||
|
After Width: | Height: | Size: 156 KiB |
@@ -0,0 +1,171 @@
|
||||
# Linux7 — shared terrain columns
|
||||
|
||||
**Decision: keep the shared second tier.** On the L1 view it cut median W12 whole-view time by
|
||||
19.0%, well above the 3% threshold, and the 8 m camera crossing was also faster. All final output
|
||||
hashes match the `9898e05` binary.
|
||||
|
||||
Runs used the `linux6` base, Clang 19 with FP contraction off, GCC 14 defaults, CPUs 0–11, normal
|
||||
priority, a 6 GiB address-space cap, and the unchanged `powersave` governor. Static timings below
|
||||
are three alternating base/candidate W12 runs of 2,553 tiles.
|
||||
|
||||
## 1. What was recomputed
|
||||
|
||||
Three complete W12 captures per mode on the L1 view. Counts below are medians of actual column
|
||||
formula executions after worker-local cache misses, grouped by LOD. “Same worker” means the worker
|
||||
had computed that XY before and later missed its local entry. Halo/normal repeats are separated
|
||||
before worker attribution; the parenthesized value is the total computed halo/normal probes,
|
||||
including unique ones. Each field's median is independent, so row medians can differ slightly from
|
||||
their sums.
|
||||
|
||||
| LOD | Base total / unique XY | Same worker | Different worker | Halo repeats / probes | Shared total / unique XY |
|
||||
|---:|---:|---:|---:|---:|---:|
|
||||
| 0 | 556,134 / 128,240 | 22,307 | 358,908 | 46,679 / 70,972 | 135,720 / 127,370 |
|
||||
| 1 | 452,626 / 104,284 | 16,393 | 287,791 | 39,245 / 59,082 | 85,658 / 85,479 |
|
||||
| 2 | 144,251 / 32,274 | 14,667 | 74,377 | 22,933 / 30,942 | 28,019 / 28,019 |
|
||||
| 3 | 91,158 / 33,256 | 0 | 46,387 | 11,564 / 20,339 | 16,213 / 16,213 |
|
||||
| 4 | 272,776 / 91,325 | 100 | 167,083 | 14,266 / 28,089 | 90,105 / 90,065 |
|
||||
| 5 | 178,511 / 71,957 | 1 | 98,687 | 7,892 / 20,042 | 70,932 / 70,930 |
|
||||
| 6 | 342,763 / 280,334 | 20 | 56,204 | 6,205 / 19,977 | 279,266 / 279,202 |
|
||||
| 7 | 79,400 / 71,468 | 1 | 4,951 | 2,980 / 14,329 | 70,492 / 70,492 |
|
||||
| 8 | 137,062 / 106,219 | 0 | 22,138 | 8,704 / 22,609 | 106,219 / 106,219 |
|
||||
|
||||
Across the per-LOD medians, the base made 2.255 million formula evaluations: 919 thousand unique
|
||||
XYs and 1.336 million repeats. Of the repeats, 83.6% were from another worker, 4.0% were same-worker
|
||||
evictions, and 12.0% were halo/normal probes. The shared candidate made 883 thousand evaluations,
|
||||
60.9% fewer; about 8.6 thousand repeats remained. This is mostly a worker-sharing problem, not a
|
||||
local-cache-capacity problem.
|
||||
|
||||
## 2. Design
|
||||
|
||||
`SharedColumnCache.h` is a bounded, four-way second-tier value cache used by both `Heightfield2D`
|
||||
and FloatingIslands. Its key includes recipe and plan pointer identity plus content hash, exact XY,
|
||||
and an optional exact sub-domain identity; FloatingIslands supplies the island stable ID because
|
||||
different anchors can use the same XY. LOD is omitted because the column value is LOD-independent.
|
||||
The heightfield table has 524,288 slots and the island table has 65,536. Both are allocated with
|
||||
the `World::Impl` and destroyed with it. The Standalone streamer stops and joins workers before
|
||||
world teardown; engine shutdown must keep the same drain-before-destroy ordering as the existing
|
||||
generator state.
|
||||
|
||||
The S14 `FLazy` thread-local caches stay first, so their hits remain worker-private. Shared hits read
|
||||
versioned atomic entries without taking a lock; a miss takes a set-local spin lock, rechecks, then
|
||||
computes and publishes. A hit racing with a slot replacement can fall through as a miss. Fixed
|
||||
capacity bounds memory and replacement bounds reuse as the player moves. No page ring, prefetch, or
|
||||
neighbor affinity was needed. The shared table is not an immutable page store; atomic fields and
|
||||
version checks give readers a consistent value while a slot can be replaced. Workers only read
|
||||
recipe/plan identity and content hashes; they do not mutate generator state or call UObject/engine
|
||||
APIs. Epoch, shutdown, and MPSC result handling were not changed. The L3 FP-contract preamble and
|
||||
self-test remain intact, as does the `FLazy` rule for large thread-local storage.
|
||||
|
||||
## 3. Speed and memory
|
||||
|
||||
| W12 static whole view | Base | Shared | Change |
|
||||
|---|---:|---:|---:|
|
||||
| `whole_view_ready` median | 2.165 s | 1.754 s | −19.0% |
|
||||
| Peak RSS median (max) | 401.5 MiB (402.6) | 467.8 MiB (468.0) | +66.3 MiB median |
|
||||
|
||||
Per-LOD worker sum is the sum of tile worker seconds, not wall time. Values are medians across the
|
||||
same three static runs.
|
||||
|
||||
| LOD | Base worker sum | Shared worker sum | Change |
|
||||
|---:|---:|---:|---:|
|
||||
| 0 | 3.830 s | 2.667 s | −30.4% |
|
||||
| 1 | 5.301 s | 4.085 s | −22.9% |
|
||||
| 2 | 1.296 s | 0.795 s | −38.7% |
|
||||
| 3 | 1.116 s | 0.725 s | −35.1% |
|
||||
| 4 | 2.692 s | 1.674 s | −37.8% |
|
||||
| 5 | 1.911 s | 1.459 s | −23.7% |
|
||||
| 6 | 2.903 s | 2.621 s | −9.7% |
|
||||
| 7 | 1.242 s | 1.227 s | −1.2% |
|
||||
| 8 | 2.318 s | 2.271 s | −2.0% |
|
||||
|
||||
The moving trial warmed the first view, moved the camera 8 m, then generated 144 entering tiles
|
||||
(128 LOD0, 16 LOD1) on the same world and worker pool. It ran three times per cache mode.
|
||||
|
||||
| Moving check | Shared | Thread-local | Change |
|
||||
|---|---:|---:|---:|
|
||||
| New-tile wall median | 52.39 ms | 82.00 ms | −36.1% |
|
||||
| LOD0 worker sum median | 403.44 ms | 693.67 ms | −41.8% |
|
||||
| LOD1 worker sum median | 138.34 ms | 182.29 ms | −24.1% |
|
||||
| Peak RSS median (max) | 466.1 MiB (466.9) | 466.1 MiB (467.6) | Same cache allocation in both modes |
|
||||
|
||||
## 4. Hashes
|
||||
|
||||
Every W1/W12 result matched the base for Clang FP contraction off and GCC default: full detail,
|
||||
whole view, each LOD, and exact queries. The L1 base hashes are:
|
||||
|
||||
| Scope | Density | Mesh | Payload | Topology |
|
||||
|---|---|---|---|---|
|
||||
| Full detail, 960 tiles | `0x0ee0579bd15cbbe4` | `0x32e5d4964d610fee` | `0x1932b96ee4b1d910` | `0x69743c6bad3447dc` |
|
||||
| Whole view, 2,553 tiles | `0xc10697a8f48380c7` | `0xe75c9119a1498cd5` | `0x49a9a8d1835a6c8c` | `0xedcb23288b5e8724` |
|
||||
|
||||
| LOD | Density | Mesh | Payload | Topology |
|
||||
|---:|---|---|---|---|
|
||||
| 0 | `0x769f2a706d585a72` | `0x33eeeb39fa443816` | `0x645621a1d968983f` | `0xc2457ed905036c9d` |
|
||||
| 1 | `0x69cc5a5c0a2c60af` | `0xb48335f5410c57d5` | `0xd15c5480162ab8ba` | `0xfeca1bd025823604` |
|
||||
| 2 | `0x85deed5683a77202` | `0x53469895987aeea2` | `0xe55f8ae7e1532033` | `0xd5feb12a0e85c09b` |
|
||||
| 3 | `0x6518caefb3be7e7f` | `0xec513c90b853a614` | `0xf62fa1137ff17f82` | `0xccf1dcf681965816` |
|
||||
| 4 | `0x62d4be96021992f4` | `0xf1e4c6efcc2615a9` | `0x9dc5f9c88be02f33` | `0xfe6f55125a07c3bf` |
|
||||
| 5 | `0x5e4a8c38b8cea6fc` | `0x08ee56998aaf21e5` | `0x541eea8558f26cc8` | `0x6539374a8b3336d8` |
|
||||
| 6 | `0xe036e95b9006945b` | `0x624a33f941a99b02` | `0x81b73e14f68bdf86` | `0x4de4c63ce1f80a56` |
|
||||
| 7 | `0x8ee358af56a30d8d` | `0x46ab3c6184048b7c` | `0x0a6164b7e68e80c5` | `0x0c26b85bb03bc831` |
|
||||
| 8 | `0xc3458e1cbdb9c471` | `0x46c03d0994d75192` | `0x8f89fc5bde12d230` | `0xd16dc0cf62031898` |
|
||||
|
||||
Exact queries (9 probes): density and unclamped `0xe4d4a8eb95b35933`, detail
|
||||
`0xefcba20c0c70c415`, collision `0xe604823a249029bf`. The FloatingIslands seed-0 fixture also
|
||||
passes at Clang/GCC W1/W12 with recipe hash `0x39a24198ad8cf88f`; its whole-view hash matches the
|
||||
base: density `0xa8fc5f8f4eaf77a3`, mesh `0xfff7d8ac8923eb9a`, payload `0xd730c51d959c1c59`,
|
||||
topology `0xbb33ddb76cd386be`.
|
||||
|
||||
## 5. Transfer estimate and Windows work
|
||||
|
||||
The majority of the opportunity is cross-worker, so it does not disappear just because the 5800X3D
|
||||
has a larger L3. Same-worker evictions are only 4% of baseline repeats; that capacity-driven part
|
||||
may shrink. The candidate adds about 66 MiB peak RSS, and a 96 MiB L3 could keep much of the shared
|
||||
table resident, though other game data will compete for it. I expect the win to transfer and clear
|
||||
3%, likely landing in the mid-single to low-double digits; that percentage is an estimate until the
|
||||
5800X3D run.
|
||||
|
||||
Windows to-do:
|
||||
|
||||
1. Build the Core route with MSVC `/fp:precise`; keep the FP-contract self-test and check the
|
||||
64-bit atomic slot path on the supported Windows target.
|
||||
2. Run the W1/W12 whole-view, full-detail, per-LOD, exact-query, and seed-0 FloatingIslands checks
|
||||
against the base binary.
|
||||
3. Interleave three static W12 A/B runs on the 5800X3D, then repeat the 8 m camera crossing; record
|
||||
peak RSS and the machine's unchanged governor.
|
||||
4. Exercise real streaming shutdown/profile changes and confirm workers drain before world/cache
|
||||
destruction, with Epoch checks and MPSC result queues still intact.
|
||||
|
||||
## 6. Failed and corrected runs
|
||||
|
||||
- The first Clang W1 hash invocation omitted the explicit data path; it exited 2 after resolving
|
||||
`Standalone/bin/data/owner_world.json`. The retry passed with
|
||||
`Standalone/data/owner_world.json` supplied.
|
||||
- The first moving step was 32 m and failed the shape interval 160-million-node safety bound. The
|
||||
benchmark was retried at 8 m, which completed three times in each cache mode.
|
||||
- The first shared FloatingIslands cache key omitted the island stable ID. The W12 fixture exposed
|
||||
an LOD4 hash mismatch. Adding the exact sub-domain ID fixed it; final hashes match at W1/W12 for
|
||||
both compilers.
|
||||
- An early fixture command passed `--seed=0`, which also overrode the surface-world seed. The final
|
||||
fixture comparison omits that override; FloatingIslands itself still runs its seed-0 check.
|
||||
- No run exceeded 30 minutes or remained stuck for 15 minutes. All final Clang and GCC builds
|
||||
succeeded with 12 jobs.
|
||||
|
||||
## Owner note
|
||||
|
||||
Linux measured a 19.0% W12 whole-view gain and a 36.1% gain on the 8 m crossing, at about 66 MiB
|
||||
more peak RSS. Please run the Windows hash matrix and static/moving A/B on the 5800X3D before using
|
||||
the laptop percentage as an owner-machine estimate.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- **Concurrency read:** a seqlock over all-atomic fields (seq_cst), with the version checked before
|
||||
and after the key/value loads; readers fall back to a miss on any mismatch. Writers hold the set
|
||||
spin lock and mark the version odd while writing. There is no non-atomic shared access, so a
|
||||
reader can't return a torn value. The per-tile scoped `setThreadActive`/restore binds each thread
|
||||
to its own world's table. Default on (`useSharedColumnCache = true`).
|
||||
- **Re-run by me** (W12, L1 view, interleaved): base `linux6` 2.014/2.059/2.092 s vs L7 `linux7shared`
|
||||
1.657/1.730/1.729 s, then 1.718/1.705/1.722 s (median 2.059 → 1.722 s, -16%). The whole-view hash was
|
||||
identical in all 8 L7 runs at W12, plus W1 (8.036 s) and W8 (1.799 s).
|
||||
- Luna's island-key bug was caught by the W12 hash fixture before review, so the determinism check
|
||||
works as intended.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Linux8: work remaining after the shared cache
|
||||
|
||||
## Per-LOD profile
|
||||
|
||||
W12 work and stage times below are medians of five interleaved timing runs. Cache columns are call counts (`worker-local hit / shared hit / shared miss`); cache outcomes were not timed separately. Unique XY per tile is the median across three W12 coordinate-capture runs. Skirt time needs the optional mesher timer, so those values come from the final gated profile; mesh assembly is the five-run median. Stage timers do not add exactly to worker time because they omit classification and other work, and some are inclusive of column computation.
|
||||
|
||||
| LOD | Tiles | Work (s) | Columns (ms) | MC (ms) | Ground/cap (ms) | Far shell (ms) | Skirt/assembly (ms) | Samples/tile | Local/shared/miss calls | Unique XY/tile |
|
||||
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 0 | 512 | 2.649 | 462 | 1,348 | 0/0 | 0.09 | 281/80 | 10,383.8 | 9.400M/405k/136k | 265 |
|
||||
| 1 | 448 | 4.064 | 309 | 3,487 | 0/0 | 0.08 | 186/76 | 42,875.0 | 19.354M/357k/86k | 191 |
|
||||
| 2 | 448 | 0.781 | 133 | 575 | 0/0 | 0.08 | 99/35 | 4,133.8 | 1.743M/115k/28k | 63 |
|
||||
| 3 | 384 | 0.730 | 89 | 591 | 0/0 | 0.07 | 108/33 | 6,859.0 | 2.130M/73k/16k | 42 |
|
||||
| 4 | 320 | 1.640 | 508 | 651 | 0/372 | 0.06 | 187/102 | 6,859.0 | 1.718M/182k/90k | 282 |
|
||||
| 5 | 144 | 1.419 | 397 | 275 | 650/22 | 0.03 | 157/72 | 6,859.0 | 698k/109k/71k | 493 |
|
||||
| 6 | 96 | 2.551 | 1,569 | 237 | 409/58 | 1,379 | 161/71 | 6,859.0 | 418k/63k/279k | 2,909 |
|
||||
| 7 | 48 | 1.201 | 393 | 200 | 294/208 | 0.00 | 149/99 | 6,859.0 | 258k/9k/70k | 1,469 |
|
||||
| 8 | 153 | 2.186 | 584 | 661 | 447/308 | 0.01 | 263/182 | 6,859.0 | 508k/30k/106k | 694 |
|
||||
|
||||
## LOD6 and LOD1
|
||||
|
||||
LOD6 generated 279,207 distinct XY columns across 96 tiles (2,909 per tile), close to the 280,334 L7 count. A bracket around `AppendFarConnectorShells` measured 206,849 formula evaluations there, about 74% of LOD6's column evaluations, and 1.379 worker-seconds; MC used about 35k evaluations, ground refinement about 37k, and cap none. The repair runs at LOD6 while stratum-shell repair starts at LOD7. The final gate profile recorded 13,291 halo/normal probes and only 16 repeated halo columns, so they do not explain the spike. The gate samples native terrain and does not certify dropping connector-shell geometry; no connector-shell work was shown to be screen-inert.
|
||||
|
||||
LOD1 has zero feature-refined tiles: it uses the normal 32-cell lattice at step 2. Its 448 tiles each consumed 42,875 density samples (35³); LOD0 averaged 10,383.8, or 5.32M total versus LOD1's 19.21M. The mesher's 8-cell proof-block early-outs are enabled at step 1 and step 8, but deliberately disabled at step 2, so LOD0 can skip uniform blocks while LOD1 fills each tile's grid. MC is LOD1's cost: 3.487 worker-seconds, with 3.345 seconds in density-grid fill in the detailed run. Cell classification was 123 ms, normals 13 ms, interpolation 8 ms, and dedup/stream 59 ms. Column computation was 309 ms; halo/normal work is not the cause.
|
||||
|
||||
## Wall time and levers
|
||||
|
||||
The median W12 tile work was 17.299 worker-seconds, or 1.442 seconds at ideal use of 12 workers. Measured stream wall was 1.446 seconds, only 4 ms above that estimate. Whole-view readiness was 1.727 seconds: shape compilation was 215 ms and another 67 ms elapsed before stream work began in owner/plan/setup. The residual is startup, not an idle worker or a long tile tail.
|
||||
|
||||
The largest lever is LOD1 density-grid filling: 3.345 worker-seconds, a theoretical ceiling of 279 ms (16.2% of a 1.72-second view) if eliminated at perfect 12-way scaling. Next are LOD6 connector-shell repair at 1.379 worker-seconds (115 ms, 6.7%), LOD0 MC at 1.348 (112 ms, 6.5%), then LOD4 MC and LOD5 ground refinement at about 0.65 each (54 ms, 3.1%). These are upper bounds, not expected savings, and the work overlaps other stages. No product optimization was built: the candidates above threshold either produce required geometry or need an exact, output-preserving kernel change that this count round did not prove.
|
||||
|
||||
The L6 gate on the final W12 profile passed same-LOD regeneration at LOD3–8 and rejected all negative controls. The existing far mesh still fails its direct-field 1 px comparison at every LOD3–8 (maxima: 6.00, 7.93, 92.02, 70.34, 42.36, 47.18 px); this round changes no geometry. No recipe bump is needed.
|
||||
|
||||
## Interleaved W12 timing and hashes
|
||||
|
||||
`linux8` is the Clang 19 `-ffp-contract=off` measurement build; `linux8gcc` is GCC 14 default. Neither changes geometry. SHA256: L7 baseline `fbbd468e351c8a0e00c903a7988a3765158e9400ed6a0111898da4d7f5ae6c85`; Clang `linux8` `82b159359a754fb33c38a93c8b7cafe5d4cfc08c41582cbfd21328307f5e3c19`; GCC `linux8gcc` `5614db5158a3f404383eea27c3748d7cf05b3a0c545b9e95f272e09bed0ee300`.
|
||||
|
||||
| Pair | L7 `linux7shared` (s) | Clang `linux8` (s) | Delta |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | 1.733 | 1.677 | −56 ms |
|
||||
| 2 | 1.670 | 1.702 | +32 ms |
|
||||
| 3 | 1.685 | 1.727 | +42 ms |
|
||||
| 4 | 1.692 | 1.727 | +35 ms |
|
||||
| 5 | 1.725 | 1.741 | +16 ms |
|
||||
| Median | 1.692 | 1.727 | +35 ms (+2.1%) |
|
||||
|
||||
Pair deltas range from −56 to +42 ms, so this diagnostic build shows no separable speed change. Every W12 pair had the same whole-view hashes: density `0xc10697a8f48380c7`, mesh `0xe75c9119a1498cd5`, payload `0x49a9a8d1835a6c8c`, topology `0xedcb23288b5e8724`. W1/W8/W12 and Clang/GCC also agreed on all nine per-LOD hashes and exact-query hashes: density/unclamped `0xe4d4a8eb95b35933`, detail `0xefcba20c0c70c415`, collision `0xe604823a249029bf`. Thus LOD0–2 and exact queries are unchanged from L7.
|
||||
|
||||
The final Clang and GCC builds took 29.876 s and 45.562 s; peak build RSS was 437,844 KiB and 901,892 KiB. Highest measured runtime RSS was 587,344 KiB. Runs used CPUs 0–11, `-j12`, a 6 GiB address-space limit, normal priority, and the unchanged `powersave` governor.
|
||||
|
||||
## Failed or retried runs
|
||||
|
||||
- `l8_build_clang.log` failed after 27.608 s: the first print patch referenced local phase sets outside their scope. The corrected `l8_build_clang_retry.log` and `l8_build_clang_retry2.log` succeeded; the later phase-instrumented `l8_build_clang_final.log` and `l8_build_gcc_final.log` also succeeded.
|
||||
- The initial no-camera probes in `l8_hash_clang_w1.log`, `l8_hash_clang_w8.log`, `l8_hash_clang_w12.log`, and `l8_baseline_l7_hash_w1.log` generated 2,370 tiles, not this 2,553-tile fixture. They were discarded and rerun with `--camera=0,-35,-487.94,0,-22`.
|
||||
- `l8_profile_gate_w12.log` and `l8_profile_gate_final.log` returned exit 6 because the gate correctly rejects the existing far-mesh field error. Same-LOD checks passed and all fault/negative controls were rejected as required. All corrected hash, profile-repeat, and speed runs exited 0; none timed out.
|
||||
|
||||
Build and runtime logs are in `Saved/Linux/l8_*.log`. No commit, push, or stash was made.
|
||||
|
||||
**Owner note:** LOD6's extra columns are predominantly connector-shell repair work, while LOD1 is expensive because all 448 tiles fill the normal density grid; neither points to wasted halo or normal work. The next useful experiment is a bit-exact LOD1 density-grid optimization, while any LOD6 resolution change needs a connector-specific screen gate first.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- **Why LOD1 fills every sample:** `VoxelMarchingCubesMesher.cpp` gates the 8-cell proof blocks to
|
||||
`Step == 1 || Step == 8`, and its comment says steps 2/16/32 skip them because "their meshing work is
|
||||
already smaller than repeating a full tile classifier per block" and to keep "the established
|
||||
coarse-LOD latency profile". That tuning predates the Core heightfield path; on the owner's
|
||||
SurfaceWorld view, LOD1's grid fill is now the largest single cost (3.35 worker-s). Next round: L9.
|
||||
- Only this report is committed. Luna's L8 instrumentation (Core and pipeline diffs) stays uncommitted
|
||||
in the tree for L9 to reuse or remove; its cost against L7 was +2.1% median with pair deltas from
|
||||
-56 to +42 ms, which is not separable, so it doesn't go in until proven free.
|
||||
@@ -0,0 +1,149 @@
|
||||
# Linux9: LOD1 uniform-block proof
|
||||
|
||||
LOD1 now omits density fills only for 8-cell blocks proved uniform by the Core SurfaceWorld heightfield. It fills 924,232 samples across the 448 tiles (2,063 per tile), down from 19,208,000 (42,875 per tile): 95.2% fewer. All output hashes match the `4b1108b` baseline.
|
||||
|
||||
## Why the old gate excluded steps
|
||||
|
||||
`git blame` points to the original mesher gate: steps 2, 16 and 32 were disabled because the per-block classifier was expected to cost more than the smaller grids saved, and to preserve the old coarse-LOD latency. It gives no correctness reason at step 2; the history does not cite block edges, normal halos or skirts. Step 1 and step 8 were already enabled (step 8 is LOD2). This round enables step 2 for native SurfaceWorld tiles; steps 16/32 remain off pending a separate cost profile.
|
||||
|
||||
## Proof and fallback
|
||||
|
||||
The tile and each proof box must stay inside one native SurfaceWorld stratum, with no overlapping edit. For every XY lattice point in the padded 8-cell box, `Heightfield2D::classifyLatticeBox` evaluates the exact ground and cap columns at the step-2 stride. For density `min(z-ground, cap-z)`, `minZ > maxGround && maxZ < minCap` proves air; `maxZ < minGround || minZ > maxCap` proves solid. These extrema bound every discrete MC density sample; the box includes the cell samples and the one-sample normal halo (`-1..+9`). No nominal noise parameter is used as a bound.
|
||||
|
||||
Feature boxes that can add volume, edits, invalid input, mixed strata, and any Mixed/Unknown verdict stay on the exact fill path. The default FloatingIslands view does not use this Step-2 tile gate; its Unknown whole boxes (1,635) remain filled. The native classifier path is shared, and a W12 island audit also forced 9,216 uniform Step-2 candidates through fill-and-check.
|
||||
|
||||
The generic `Heightfield2D::interval` was sound but too broad here: it gave bounds `[-2,883,809, 1,032,090]` subvoxels around z=-2,114, so all 28,672 Step-2 blocks were Mixed. The exact lattice-column extrema produced useful verdicts instead.
|
||||
|
||||
## Soundness sweep
|
||||
|
||||
The audit bypassed block skipping and explicitly evaluated every density sample in each candidate's padded block, asserting that none contradicted the verdict. It covered three SurfaceWorld seeds and one FloatingIslands audit:
|
||||
|
||||
| Recipe / seed | Blocks checked | Density samples checked | Mixed fallbacks | Unknown | Violations |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| SurfaceWorld 0 | 27,479 | 36,574,549 | 1,193 | 0 | 0 |
|
||||
| SurfaceWorld 1 | 26,069 | 34,697,839 | 1,451 | 0 | 0 |
|
||||
| SurfaceWorld 17 | 26,954 | 35,875,774 | 1,718 | 0 | 0 |
|
||||
| SurfaceWorld total | 80,502 | 107,148,162 | 4,362 | 0 | 0 |
|
||||
| FloatingIslands 0 | 9,216 | 12,266,496 | 0 | 0 | 0 |
|
||||
|
||||
A violation would be possible if the native density dispatch or `point()` stopped matching this column formula, if a new volume/edit overlay bypassed the Mixed guard, or if the padded lattice/normal halo changed without updating the proof bounds. Zero density is treated as Mixed by the audit.
|
||||
|
||||
## W12 samples and worker time
|
||||
|
||||
Sample counts compare L8's SurfaceWorld profile with the final L9 profile. Worker seconds are medians of five W12 no-hash runs against `linux7shared` and the Clang candidate. They are per-LOD sums across workers.
|
||||
|
||||
| LOD | L9 filled samples | Worker seconds baseline → L9 | Change |
|
||||
|---:|---:|---:|---:|
|
||||
| 0 | 5,316,500 | 2.398 → 2.395 | -0.1% |
|
||||
| 1 | 924,232 | 1.213 → 0.920 | -24.1% |
|
||||
| 2 | 932,824 | 0.452 → 0.456 | +0.8% |
|
||||
| 3 | 1,858,789 | 0.540 → 0.549 | +1.6% |
|
||||
| 4 | 2,085,136 | 1.535 → 1.545 | +0.6% |
|
||||
| 5 | 987,696 | 1.378 → 1.395 | +1.2% |
|
||||
| 6 | 658,464 | 2.559 → 2.564 | +0.2% |
|
||||
| 7 | 329,232 | 1.134 → 1.139 | +0.4% |
|
||||
| 8 | 1,049,427 | 2.055 → 2.082 | +1.3% |
|
||||
|
||||
LOD1 per tile is 42,875 → 2,063 samples.
|
||||
|
||||
LOD0 and LOD2–8 stayed within 1.6% of baseline worker time. LOD1's 4.064 worker-seconds in the L8 profile did not reproduce in the specified `linux7shared` binary's worker diagnostics: the five-run median there was 1.213 seconds. The exact-binary interleaved timing below is the comparable whole-view measure.
|
||||
|
||||
## W12 interleaved stream time
|
||||
|
||||
Baseline is `Standalone/bin/linux/VoxelForgeCoreHeadless-linux7shared`, SHA256 `fbbd468e351c8a0e00c903a7988a3765158e9400ed6a0111898da4d7f5ae6c85`. Each pair used the same seed, camera, 2,553 tiles and W12 settings.
|
||||
|
||||
| Pair | Baseline | Clang `-ffp-contract=off` | Delta |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | 1.079 s | 1.066 s | -1.2% |
|
||||
| 2 | 1.109 s | 1.083 s | -2.3% |
|
||||
| 3 | 1.145 s | 1.100 s | -3.9% |
|
||||
| 4 | 1.125 s | 1.099 s | -2.3% |
|
||||
| 5 | 1.126 s | 1.113 s | -1.2% |
|
||||
| Median | 1.125 s | 1.099 s | -2.3% |
|
||||
|
||||
| Pair | Baseline | GCC 14 default | Delta |
|
||||
|---:|---:|---:|---:|
|
||||
| 1 | 1.073 s | 1.100 s | +2.5% |
|
||||
| 2 | 1.091 s | 1.136 s | +4.1% |
|
||||
| 3 | 1.104 s | 1.129 s | +2.3% |
|
||||
| 4 | 1.123 s | 1.140 s | +1.5% |
|
||||
| 5 | 1.123 s | 1.150 s | +2.4% |
|
||||
| Median | 1.104 s | 1.136 s | +2.9% |
|
||||
|
||||
The baseline ELF contains GCC 14 and Clang 19 objects; the candidate GCC binary is GCC-only. As a compiler-matched control, five W12 pairs against the GCC-only `linux7gcc` baseline measured 1.154 s → 1.116 s (-3.3%). Thus the specified-binary GCC comparison is slower, while the matched GCC control is faster; the mixed-toolchain delta cannot be attributed to this change. The Clang candidate is faster than the specified binary.
|
||||
|
||||
FloatingIslands seed 0 hashes match at W1/W12 for both compilers. Five W12 no-hash pairs against `linux7shared` gave Clang 10.836 → 11.005 s (+1.6%, pair range -0.5% to +3.0%). GCC was 11.223 → 12.618 s (+12.4%), while five compiler-matched GCC pairs against `linux7gcc` were 12.498 → 12.529 s (+0.25%, pair range -0.5% to +1.3%). The default island path has no measurable same-compiler slowdown; the large `linux7shared` GCC delta is the same toolchain mismatch.
|
||||
|
||||
## Hashes
|
||||
|
||||
Every SurfaceWorld hash below matched the baseline at W1/W8/W12 for both compilers; W12 was repeated five times per compiler. FloatingIslands seed 0 matched at W1/W12 for both compilers. Per-LOD tuples are density / mesh / payload / topology.
|
||||
|
||||
SurfaceWorld full detail: density `0x0ee0579bd15cbbe4`, mesh `0x32e5d4964d610fee`, payload `0x1932b96ee4b1d910`, topology `0x69743c6bad3447dc`.
|
||||
|
||||
SurfaceWorld whole view: density `0xc10697a8f48380c7`, mesh `0xe75c9119a1498cd5`, payload `0x49a9a8d1835a6c8c`, topology `0xedcb23288b5e8724`.
|
||||
|
||||
SurfaceWorld exact queries (9 probes): density/unclamped `0xe4d4a8eb95b35933`, detail `0xefcba20c0c70c415`, collision `0xe604823a249029bf`.
|
||||
|
||||
| LOD | Density | Mesh | Payload | Topology |
|
||||
|---:|---|---|---|---|
|
||||
| 0 | `0x769f2a706d585a72` | `0x33eeeb39fa443816` | `0x645621a1d968983f` | `0xc2457ed905036c9d` |
|
||||
| 1 | `0x69cc5a5c0a2c60af` | `0xb48335f5410c57d5` | `0xd15c5480162ab8ba` | `0xfeca1bd025823604` |
|
||||
| 2 | `0x85deed5683a77202` | `0x53469895987aeea2` | `0xe55f8ae7e1532033` | `0xd5feb12a0e85c09b` |
|
||||
| 3 | `0x6518caefb3be7e7f` | `0xec513c90b853a614` | `0xf62fa1137ff17f82` | `0xccf1dcf681965816` |
|
||||
| 4 | `0x62d4be96021992f4` | `0xf1e4c6efcc2615a9` | `0x9dc5f9c88be02f33` | `0xfe6f55125a07c3bf` |
|
||||
| 5 | `0x5e4a8c38b8cea6fc` | `0x08ee56998aaf21e5` | `0x541eea8558f26cc8` | `0x6539374a8b3336d8` |
|
||||
| 6 | `0xe036e95b9006945b` | `0x624a33f941a99b02` | `0x81b73e14f68bdf86` | `0x4de4c63ce1f80a56` |
|
||||
| 7 | `0x8ee358af56a30d8d` | `0x46ab3c6184048b7c` | `0x0a6164b7e68e80c5` | `0x0c26b85bb03bc831` |
|
||||
| 8 | `0xc3458e1cbdb9c471` | `0x46c03d0994d75192` | `0x8f89fc5bde12d230` | `0xd16dc0cf62031898` |
|
||||
|
||||
FloatingIslands full detail: density `0x6d842d2878b08ef6`, mesh `0x4b868a3184f08035`, payload `0x2cedf2236788752d`, topology `0xeac17d5e0cb5044a`.
|
||||
|
||||
FloatingIslands whole view: density `0x5be4d8cde79b2a76`, mesh `0x96f75041f6861b3a`, payload `0x3700a516cc89d50c`, topology `0x6b99948d6eab4f21`.
|
||||
|
||||
FloatingIslands exact queries (9 probes): density/unclamped `0x61daded9c303becb`, detail `0xefcba20c0c70c415`, collision `0x84d37463be9b0573`.
|
||||
|
||||
| LOD | Density | Mesh | Payload | Topology |
|
||||
|---:|---|---|---|---|
|
||||
| 0 | `0xf2524906044ef994` | `0x15b2828701a06591` | `0x4cbbf7ade54321f7` | `0x2c0382f590d69610` |
|
||||
| 1 | `0x71b4c8327ad91da7` | `0xfd2280177adb70c9` | `0xc6f695b969ca8573` | `0xb7b026366bc5b627` |
|
||||
| 2 | `0x23925de354d86f14` | `0xd4b470b6aae75cab` | `0x38c39feddc72ca0c` | `0x8496f76d94d0e578` |
|
||||
| 3 | `0x4cb68a60bc8a5103` | `0x30514ad73d41fd87` | `0x39a209d2ec7695b1` | `0xa2ffa199ba1e1215` |
|
||||
| 4 | `0x0e95c8abbcf6fb37` | `0x966b891fbe67794a` | `0xf01627522109d362` | `0xd7bfb2fea0137c0c` |
|
||||
| 5 | `0xf444ffed34a1bd20` | `0x7f3073ed3a711113` | `0xad32e4ea4a1b4d50` | `0x6fdba8d6e549193c` |
|
||||
| 6 | `0x6a6b4f75b9583ec7` | `0xdbb540e17a46b981` | `0xe2374a66e9d12b22` | `0xd384b7c0c0247982` |
|
||||
| 7 | `0x5c4e824aaa7e5f73` | `0x44857e6de8e89645` | `0xa4ac0bf1da59e406` | `0x94f425b0d77004c8` |
|
||||
| 8 | `0x8ce5f96cb24bc365` | `0xa1406a9a6dcae02b` | `0x8b5e182d38fe352b` | `0x098f65c1a8cde739` |
|
||||
|
||||
## Instrumentation, builds and limits
|
||||
|
||||
Removed the L8 phase/stage probes from the normal path. Kept `--profile-lod-work` for per-LOD density sample counts and added `--audit-uniform-block-proofs`; both are off by default. Normal runs do not perform the audit fill-and-check pass or sample-count aggregation. The optional LOD0 MC (1.348 worker-s) and shape compile (215 ms) were counted in L8 and not changed.
|
||||
|
||||
No async task or epoch path changed. The L3 FP-contract preamble/self-test, L4 `FLazy`, L7 shared cache, ARCHITECTURE §8.10 two-pass MC/SSE noise/clipmap-streaming invariants, and generator density output remain unchanged.
|
||||
|
||||
Clang 19 (`-ffp-contract=off`) and GCC 14 default builds used `-j12`; SHA256s are `0cd3f0ef993ced2c615418c6e2b37b83f57eff6d024d7d1568c30ea51490384a` and `23fe40de558b1b6931469cc5e976a0e4f2ed5fb6c362bc746dcc64a38fa40e08`. Peak build RSS was 901,940 KiB; highest measured runtime RSS was 623,928 KiB. Runs used CPUs 0–11, a 6 GiB address-space limit, normal priority, and the unchanged `powersave` governor. No run exceeded 30 minutes or remained stuck for 15 minutes.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- The first build wrapper failed because `/usr/bin/time` is absent; direct builds succeeded. One RSS sampling wrapper hung on zombie handling and was interrupted; later measurements used a corrected monitor.
|
||||
- Initial proof-audit commands exited 2 because the positional data file and required stream modes were missing. Corrected commands completed. The first broad global interval returned Mixed for all 28,672 blocks and was replaced by the exact lattice-column proof.
|
||||
- An early all-step audit found two legacy step-8 uniform candidates whose exact filled-grid signs disagreed; the existing post-fill validator cleared them before skip. The new audit is scoped to the native step-2 proof; step 8 is unchanged.
|
||||
- The first Clang retry failed to compile because `SampleGridPoint` was outside its lambda scope; it was corrected to call `DensityAtLattice` directly and rebuilt.
|
||||
- The first hash comparison differed because a uniform-tile early return bypassed debug density capture. Step-2 hash runs now refill omitted samples after exercising sparse MC. A separate FloatingIslands mismatch came from changing `>= 0` to `> 0`; the legacy `>= 0` comparison was restored and both compiler hashes then matched.
|
||||
|
||||
**Owner note:** LOD1 now fills 95% fewer samples with zero proof violations, and all SurfaceWorld and FloatingIslands hashes match. The GCC W12 comparison against `linux7shared` is toolchain-confounded; GCC-to-GCC controls are flat or faster, while the Clang candidate is faster than the requested baseline.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- **Re-run by me, 5 interleaved W12 pairs, L1 view.** Without `--stream-hash` (game-like):
|
||||
whole view L7 1.379/1.485/1.430/1.439/1.449 s vs L9 1.390/1.409/1.401/1.423/1.429 s (median
|
||||
1.439 → 1.409 s, −2.1%); stream wall 1.140 → 1.112 s (−2.5%). With hashing on, the difference is
|
||||
inside noise (1.748 vs 1.744 s), because hashing adds ~0.3 s. The whole-view hash is identical in
|
||||
all runs.
|
||||
- **The L8 lever was inflated by L8's own instrumentation.** L8's profile build reported LOD1 at
|
||||
4.06 worker-s; the plain `linux7shared` binary spends 1.21. Per-LOD shares from instrumented builds
|
||||
must be re-checked on a plain build before they're ranked.
|
||||
- The "baseline ELF contains GCC and Clang objects" remark is expected: crt/libstdc++ objects always
|
||||
come from GCC. The compiler-matched controls are the right comparison, as Luna did.
|
||||
- Proof read: for native SurfaceWorld step-2 tiles the verdict uses exact column extrema over the
|
||||
padded block (cells plus the −1..+9 normal halo) for density `min(z−ground, cap−z)`. Features, edits,
|
||||
mixed strata and Unknown verdicts all fill. The 80,502-block audit found 0 violations.
|
||||
@@ -0,0 +1,499 @@
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <png.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FVertex
|
||||
{
|
||||
float Position[3];
|
||||
float Normal[3];
|
||||
float Height;
|
||||
float WorldPosition[3];
|
||||
float TileData[4];
|
||||
};
|
||||
|
||||
struct FScene
|
||||
{
|
||||
std::uint32_t VoxelSizeMicrons=0;
|
||||
std::array<double,3> CameraMeters{};
|
||||
double YawDegrees=0.0,PitchDegrees=0.0;
|
||||
float MinimumHeight=std::numeric_limits<float>::infinity();
|
||||
float MaximumHeight=-std::numeric_limits<float>::infinity();
|
||||
std::vector<FVertex> Vertices;
|
||||
std::vector<std::uint32_t> Indices;
|
||||
std::uint64_t Triangles=0,SkirtTriangles=0;
|
||||
std::uint32_t TileCount=0;
|
||||
};
|
||||
|
||||
template<typename T> bool Read(std::ifstream& File,T& Value)
|
||||
{
|
||||
File.read(reinterpret_cast<char*>(&Value),sizeof(Value));
|
||||
return static_cast<bool>(File);
|
||||
}
|
||||
|
||||
bool LoadScene(const std::filesystem::path& Path,FScene& Scene,std::string& Error)
|
||||
{
|
||||
std::ifstream File(Path,std::ios::binary);
|
||||
if(!File){Error="cannot open mesh scene";return false;}
|
||||
char Magic[8]{};
|
||||
File.read(Magic,sizeof(Magic));
|
||||
if(!File||std::memcmp(Magic,"VFMSH001",8)!=0)
|
||||
{Error="mesh scene magic is invalid";return false;}
|
||||
std::uint32_t Version=0,ExpectedTiles=0;
|
||||
if(!Read(File,Version)||!Read(File,Scene.VoxelSizeMicrons)||!Read(File,ExpectedTiles)
|
||||
||Version!=1||Scene.VoxelSizeMicrons==0||ExpectedTiles>1000000)
|
||||
{Error="mesh scene header is malformed";return false;}
|
||||
for(double& Value:Scene.CameraMeters)if(!Read(File,Value))
|
||||
{Error="mesh scene camera is truncated";return false;}
|
||||
if(!Read(File,Scene.YawDegrees)||!Read(File,Scene.PitchDegrees))
|
||||
{Error="mesh scene camera orientation is truncated";return false;}
|
||||
Scene.TileCount=ExpectedTiles;
|
||||
std::uint64_t TotalVertices=0,TotalIndices=0;
|
||||
for(std::uint32_t Tile=0;Tile<ExpectedTiles;++Tile)
|
||||
{
|
||||
std::int32_t Key[4]{};
|
||||
std::uint32_t SurfaceVertices=0,SkirtCount=0,VertexCount=0,IndexCount=0,MaskCount=0;
|
||||
for(auto& Value:Key)if(!Read(File,Value)){Error="tile key is truncated";return false;}
|
||||
if(!Read(File,SurfaceVertices)||!Read(File,SkirtCount)||!Read(File,VertexCount)
|
||||
||!Read(File,IndexCount)||!Read(File,MaskCount)
|
||||
||VertexCount>10000000||IndexCount>60000000||MaskCount>20000000
|
||||
||IndexCount%3!=0||(MaskCount!=0&&MaskCount!=IndexCount/3)
|
||||
||Key[3]<0||Key[3]>8)
|
||||
{Error="tile mesh header is malformed";return false;}
|
||||
if(TotalVertices+VertexCount>std::numeric_limits<std::uint32_t>::max()
|
||||
||TotalIndices+IndexCount>std::numeric_limits<std::size_t>::max())
|
||||
{Error="mesh scene exceeds supported index space";return false;}
|
||||
const std::size_t Base=Scene.Vertices.size();
|
||||
Scene.Vertices.resize(Base+VertexCount);
|
||||
Scene.Indices.reserve(Scene.Indices.size()+IndexCount);
|
||||
for(std::size_t I=Base;I<Base+VertexCount;++I)
|
||||
for(float& Value:Scene.Vertices[I].Position)if(!Read(File,Value))
|
||||
{Error="tile positions are truncated";return false;}
|
||||
for(std::size_t I=Base;I<Base+VertexCount;++I)
|
||||
{
|
||||
for(float& Value:Scene.Vertices[I].Normal)if(!Read(File,Value))
|
||||
{Error="tile normals are truncated";return false;}
|
||||
Scene.Vertices[I].Height=Scene.Vertices[I].Position[2];
|
||||
Scene.MinimumHeight=std::min(Scene.MinimumHeight,Scene.Vertices[I].Height);
|
||||
Scene.MaximumHeight=std::max(Scene.MaximumHeight,Scene.Vertices[I].Height);
|
||||
}
|
||||
const double TileVoxels=static_cast<double>(32u<<Key[3]);
|
||||
const float TileExtentMeters=static_cast<float>(TileVoxels
|
||||
*static_cast<double>(Scene.VoxelSizeMicrons)/1000000.0);
|
||||
const float TileOriginX=static_cast<float>(static_cast<double>(Key[0])
|
||||
*TileExtentMeters);
|
||||
const float TileOriginY=static_cast<float>(static_cast<double>(Key[1])
|
||||
*TileExtentMeters);
|
||||
for(std::size_t I=Base;I<Base+VertexCount;++I)
|
||||
{
|
||||
FVertex& Vertex=Scene.Vertices[I];
|
||||
std::copy(std::begin(Vertex.Position),std::end(Vertex.Position),
|
||||
std::begin(Vertex.WorldPosition));
|
||||
Vertex.TileData[0]=TileOriginX;
|
||||
Vertex.TileData[1]=TileOriginY;
|
||||
Vertex.TileData[2]=TileExtentMeters;
|
||||
Vertex.TileData[3]=static_cast<float>(Key[3]);
|
||||
}
|
||||
const std::size_t IndexOffset=Scene.Indices.size();
|
||||
Scene.Indices.resize(IndexOffset+IndexCount);
|
||||
if(IndexCount)File.read(reinterpret_cast<char*>(Scene.Indices.data()+IndexOffset),
|
||||
static_cast<std::streamsize>(sizeof(std::uint32_t)*IndexCount));
|
||||
std::vector<std::uint8_t> Masks(MaskCount);
|
||||
if(MaskCount)File.read(reinterpret_cast<char*>(Masks.data()),MaskCount);
|
||||
if(!File){Error="tile index or skirt data is truncated";return false;}
|
||||
for(std::size_t Index=IndexOffset;Index<Scene.Indices.size();++Index)
|
||||
{
|
||||
if(Scene.Indices[Index]>=VertexCount)
|
||||
{Error="tile index points outside its vertex array";return false;}
|
||||
Scene.Indices[Index]+=static_cast<std::uint32_t>(Base);
|
||||
}
|
||||
Scene.Triangles+=IndexCount/3;
|
||||
Scene.SkirtTriangles+=SkirtCount;
|
||||
TotalVertices+=VertexCount;TotalIndices+=IndexCount;
|
||||
}
|
||||
if(File.peek()!=std::char_traits<char>::eof())
|
||||
{Error="mesh scene has trailing bytes";return false;}
|
||||
if(Scene.Indices.empty()){Error="mesh scene contains no triangles";return false;}
|
||||
return true;
|
||||
}
|
||||
|
||||
EGLDisplay MakeDisplay()
|
||||
{
|
||||
using TGetPlatformDisplay=EGLDisplay(*)(EGLenum,void*,const EGLint*);
|
||||
auto GetPlatformDisplay=reinterpret_cast<TGetPlatformDisplay>(
|
||||
eglGetProcAddress("eglGetPlatformDisplayEXT"));
|
||||
#ifdef EGL_PLATFORM_SURFACELESS_MESA
|
||||
if(GetPlatformDisplay)
|
||||
{
|
||||
const EGLDisplay Display=GetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
|
||||
EGL_DEFAULT_DISPLAY,nullptr);
|
||||
if(Display!=EGL_NO_DISPLAY)return Display;
|
||||
}
|
||||
#endif
|
||||
return eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
}
|
||||
|
||||
GLuint CompileShader(GLenum Type,const char* Source)
|
||||
{
|
||||
const GLuint Shader=glCreateShader(Type);
|
||||
glShaderSource(Shader,1,&Source,nullptr);
|
||||
glCompileShader(Shader);
|
||||
GLint Success=GL_FALSE;
|
||||
glGetShaderiv(Shader,GL_COMPILE_STATUS,&Success);
|
||||
if(Success==GL_FALSE)
|
||||
{
|
||||
GLint Length=0;glGetShaderiv(Shader,GL_INFO_LOG_LENGTH,&Length);
|
||||
std::string Log(static_cast<std::size_t>(std::max(Length,1)),0);
|
||||
glGetShaderInfoLog(Shader,Length,nullptr,Log.data());
|
||||
std::fprintf(stderr,"[OffscreenGL] shader compile failed: %s\n",Log.c_str());
|
||||
glDeleteShader(Shader);return 0;
|
||||
}
|
||||
return Shader;
|
||||
}
|
||||
|
||||
GLuint MakeProgram()
|
||||
{
|
||||
static constexpr char VertexSource[]=R"GLSL(#version 300 es
|
||||
precision highp float;
|
||||
layout(location=0) in vec3 inCameraPosition;
|
||||
layout(location=1) in vec3 inNormal;
|
||||
layout(location=2) in float inHeight;
|
||||
layout(location=3) in vec3 inWorldPosition;
|
||||
layout(location=4) in vec4 inTileData;
|
||||
out vec3 vNormal;
|
||||
out float vHeight;
|
||||
out vec3 vWorldPosition;
|
||||
out vec4 vTileData;
|
||||
uniform float uTanHalfHorizontal;
|
||||
uniform float uTanHalfVertical;
|
||||
uniform float uNear;
|
||||
uniform float uFar;
|
||||
void main()
|
||||
{
|
||||
float z=max(inCameraPosition.z,0.0001);
|
||||
float clipZ=((uFar+uNear)/(uFar-uNear))*z-(2.0*uFar*uNear)/(uFar-uNear);
|
||||
gl_Position=vec4(inCameraPosition.x/uTanHalfHorizontal,
|
||||
inCameraPosition.y/uTanHalfVertical,clipZ,z);
|
||||
vNormal=normalize(inNormal);
|
||||
vHeight=inHeight;
|
||||
vWorldPosition=inWorldPosition;
|
||||
vTileData=inTileData;
|
||||
})GLSL";
|
||||
static constexpr char FragmentSource[]=R"GLSL(#version 300 es
|
||||
precision highp float;
|
||||
in vec3 vNormal;
|
||||
in float vHeight;
|
||||
in vec3 vWorldPosition;
|
||||
in vec4 vTileData;
|
||||
out vec4 outColor;
|
||||
uniform int uReadableMode;
|
||||
uniform int uTileEdges;
|
||||
uniform float uHeightCenter;
|
||||
uniform float uHeightRange;
|
||||
uniform float uTileEdgeWidth;
|
||||
uniform vec3 uLightDirection;
|
||||
void main()
|
||||
{
|
||||
vec3 n=normalize(vNormal);
|
||||
vec3 color;
|
||||
if(uReadableMode==0)
|
||||
{
|
||||
vec3 normalColor=abs(n)*0.78+vec3(0.12);
|
||||
float band=mod(floor((vHeight+10000.0)/80.0),2.0);
|
||||
vec3 bandColor=mix(vec3(0.24,0.31,0.37),vec3(0.39,0.42,0.35),band);
|
||||
color=mix(bandColor,normalColor,0.68);
|
||||
}
|
||||
else
|
||||
{
|
||||
float heightT=clamp(0.5+(vHeight-uHeightCenter)/max(uHeightRange,0.001),0.0,1.0);
|
||||
vec3 heightTint=mix(vec3(0.80,0.91,1.0),vec3(1.0,0.91,0.77),heightT);
|
||||
float diffuse=max(dot(n,normalize(uLightDirection)),0.0);
|
||||
float illumination=0.62+0.38*diffuse;
|
||||
color=vec3(0.64,0.62,0.56)*heightTint*illumination;
|
||||
}
|
||||
if(uTileEdges!=0&&vTileData.z>0.0)
|
||||
{
|
||||
vec2 local=mod(vWorldPosition.xy-vTileData.xy,vTileData.z);
|
||||
vec2 edge=min(local,vec2(vTileData.z)-local);
|
||||
float distanceToTileEdge=min(edge.x,edge.y);
|
||||
float antialias=max(fwidth(distanceToTileEdge),0.005);
|
||||
float line=1.0-smoothstep(uTileEdgeWidth-antialias,
|
||||
uTileEdgeWidth+antialias,distanceToTileEdge);
|
||||
vec3 edgeColor=mix(vec3(0.16,0.22,0.25),vec3(0.30,0.23,0.14),
|
||||
clamp(vTileData.w/8.0,0.0,1.0));
|
||||
color=mix(color,edgeColor,0.32*line);
|
||||
}
|
||||
outColor=vec4(color,1.0);
|
||||
})GLSL";
|
||||
const GLuint VS=CompileShader(GL_VERTEX_SHADER,VertexSource);
|
||||
const GLuint FS=CompileShader(GL_FRAGMENT_SHADER,FragmentSource);
|
||||
if(!VS||!FS)return 0;
|
||||
const GLuint Program=glCreateProgram();
|
||||
glAttachShader(Program,VS);glAttachShader(Program,FS);glLinkProgram(Program);
|
||||
glDeleteShader(VS);glDeleteShader(FS);
|
||||
GLint Success=GL_FALSE;glGetProgramiv(Program,GL_LINK_STATUS,&Success);
|
||||
if(Success==GL_FALSE)
|
||||
{
|
||||
GLint Length=0;glGetProgramiv(Program,GL_INFO_LOG_LENGTH,&Length);
|
||||
std::string Log(static_cast<std::size_t>(std::max(Length,1)),0);
|
||||
glGetProgramInfoLog(Program,Length,nullptr,Log.data());
|
||||
std::fprintf(stderr,"[OffscreenGL] program link failed: %s\n",Log.c_str());
|
||||
glDeleteProgram(Program);return 0;
|
||||
}
|
||||
return Program;
|
||||
}
|
||||
|
||||
bool WritePng(const std::filesystem::path& Path,int Width,int Height,
|
||||
const std::vector<std::uint8_t>& Pixels)
|
||||
{
|
||||
std::error_code Error;
|
||||
if(!Path.parent_path().empty())std::filesystem::create_directories(Path.parent_path(),Error);
|
||||
if(Error)return false;
|
||||
FILE* File=std::fopen(Path.c_str(),"wb");
|
||||
if(!File)return false;
|
||||
png_structp Png=png_create_write_struct(PNG_LIBPNG_VER_STRING,nullptr,nullptr,nullptr);
|
||||
png_infop Info=Png?png_create_info_struct(Png):nullptr;
|
||||
if(!Png||!Info){if(Png)png_destroy_write_struct(&Png,nullptr);std::fclose(File);return false;}
|
||||
if(setjmp(png_jmpbuf(Png)))
|
||||
{png_destroy_write_struct(&Png,&Info);std::fclose(File);return false;}
|
||||
png_init_io(Png,File);
|
||||
png_set_IHDR(Png,Info,static_cast<png_uint_32>(Width),static_cast<png_uint_32>(Height),
|
||||
8,PNG_COLOR_TYPE_RGBA,PNG_INTERLACE_NONE,PNG_COMPRESSION_TYPE_DEFAULT,PNG_FILTER_TYPE_DEFAULT);
|
||||
png_write_info(Png,Info);
|
||||
const std::size_t Stride=static_cast<std::size_t>(Width)*4;
|
||||
for(int Y=Height-1;Y>=0;--Y)
|
||||
png_write_row(Png,const_cast<png_bytep>(Pixels.data()+static_cast<std::size_t>(Y)*Stride));
|
||||
png_write_end(Png,nullptr);
|
||||
png_destroy_write_struct(&Png,&Info);
|
||||
const bool Good=std::fclose(File)==0;
|
||||
return Good;
|
||||
}
|
||||
|
||||
bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height,
|
||||
double HorizontalFovDegrees,bool ReadableMode,bool TileEdges,
|
||||
double TileEdgeWidthMeters)
|
||||
{
|
||||
EGLDisplay Display=MakeDisplay();
|
||||
EGLint Major=0,Minor=0;
|
||||
if(Display==EGL_NO_DISPLAY||eglInitialize(Display,&Major,&Minor)!=EGL_TRUE)
|
||||
{std::fprintf(stderr,"[OffscreenEGL] eglInitialize failed (0x%x)\n",eglGetError());return false;}
|
||||
if(eglBindAPI(EGL_OPENGL_ES_API)!=EGL_TRUE)
|
||||
{std::fprintf(stderr,"[OffscreenEGL] eglBindAPI failed (0x%x)\n",eglGetError());eglTerminate(Display);return false;}
|
||||
const EGLint ConfigAttributes[]={EGL_SURFACE_TYPE,EGL_PBUFFER_BIT,
|
||||
EGL_RENDERABLE_TYPE,EGL_OPENGL_ES3_BIT,EGL_RED_SIZE,8,EGL_GREEN_SIZE,8,
|
||||
EGL_BLUE_SIZE,8,EGL_ALPHA_SIZE,8,EGL_DEPTH_SIZE,24,EGL_NONE};
|
||||
EGLConfig Config{};EGLint ConfigCount=0;
|
||||
if(eglChooseConfig(Display,ConfigAttributes,&Config,1,&ConfigCount)!=EGL_TRUE||ConfigCount==0)
|
||||
{std::fprintf(stderr,"[OffscreenEGL] no GLES3 pbuffer config (0x%x)\n",eglGetError());eglTerminate(Display);return false;}
|
||||
const EGLint SurfaceAttributes[]={EGL_WIDTH,Width,EGL_HEIGHT,Height,EGL_NONE};
|
||||
EGLSurface Surface=eglCreatePbufferSurface(Display,Config,SurfaceAttributes);
|
||||
const EGLint ContextAttributes[]={EGL_CONTEXT_CLIENT_VERSION,3,EGL_NONE};
|
||||
EGLContext Context=eglCreateContext(Display,Config,EGL_NO_CONTEXT,ContextAttributes);
|
||||
if(Surface==EGL_NO_SURFACE||Context==EGL_NO_CONTEXT
|
||||
||eglMakeCurrent(Display,Surface,Surface,Context)!=EGL_TRUE)
|
||||
{std::fprintf(stderr,"[OffscreenEGL] context/surface creation failed (0x%x)\n",eglGetError());
|
||||
if(Context!=EGL_NO_CONTEXT)eglDestroyContext(Display,Context);
|
||||
if(Surface!=EGL_NO_SURFACE)eglDestroySurface(Display,Surface);
|
||||
eglTerminate(Display);return false;}
|
||||
|
||||
const GLubyte* Renderer=glGetString(GL_RENDERER);
|
||||
const GLubyte* GlVersion=glGetString(GL_VERSION);
|
||||
std::printf("[OffscreenEGL] renderer=%s version=%s pbuffer=%dx%d\n",
|
||||
Renderer?reinterpret_cast<const char*>(Renderer):"unknown",
|
||||
GlVersion?reinterpret_cast<const char*>(GlVersion):"unknown",Width,Height);
|
||||
GLuint ColorTexture=0,DepthBuffer=0,Framebuffer=0;
|
||||
glGenTextures(1,&ColorTexture);glBindTexture(GL_TEXTURE_2D,ColorTexture);
|
||||
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,Width,Height,0,GL_RGBA,GL_UNSIGNED_BYTE,nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
|
||||
glGenRenderbuffers(1,&DepthBuffer);glBindRenderbuffer(GL_RENDERBUFFER,DepthBuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH_COMPONENT24,Width,Height);
|
||||
glGenFramebuffers(1,&Framebuffer);glBindFramebuffer(GL_FRAMEBUFFER,Framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,ColorTexture,0);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_RENDERBUFFER,DepthBuffer);
|
||||
if(glCheckFramebufferStatus(GL_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE)
|
||||
{std::fprintf(stderr,"[OffscreenGL] framebuffer is incomplete\n");return false;}
|
||||
|
||||
constexpr double Pi=3.14159265358979323846;
|
||||
const double Yaw=Scene.YawDegrees*Pi/180.0,Pitch=Scene.PitchDegrees*Pi/180.0;
|
||||
const std::array<double,3> Forward={std::sin(Yaw)*std::cos(Pitch),
|
||||
std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)};
|
||||
const std::array<double,3> Right={std::cos(Yaw),-std::sin(Yaw),0.0};
|
||||
const std::array<double,3> 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]};
|
||||
const double TanHalfHorizontal=std::tan(HorizontalFovDegrees*Pi/360.0);
|
||||
const double Aspect=static_cast<double>(Width)/Height;
|
||||
const double TanHalfVertical=TanHalfHorizontal/Aspect;
|
||||
const double Near=0.1,Far=20000.0;
|
||||
for(FVertex& Vertex:Scene.Vertices)
|
||||
{
|
||||
const double DX=Vertex.Position[0]-Scene.CameraMeters[0];
|
||||
const double DY=Vertex.Position[1]-Scene.CameraMeters[1];
|
||||
const double DZ=Vertex.Position[2]-Scene.CameraMeters[2];
|
||||
const float CameraX=static_cast<float>(DX*Right[0]+DY*Right[1]+DZ*Right[2]);
|
||||
const float CameraY=static_cast<float>(DX*Up[0]+DY*Up[1]+DZ*Up[2]);
|
||||
const float CameraZ=static_cast<float>(DX*Forward[0]+DY*Forward[1]+DZ*Forward[2]);
|
||||
Vertex.Position[0]=CameraX;Vertex.Position[1]=CameraY;Vertex.Position[2]=CameraZ;
|
||||
}
|
||||
const GLuint Program=MakeProgram();
|
||||
if(!Program)return false;
|
||||
GLuint Vao=0,Vbo=0,Ebo=0;
|
||||
glGenVertexArrays(1,&Vao);glBindVertexArray(Vao);
|
||||
glGenBuffers(1,&Vbo);glBindBuffer(GL_ARRAY_BUFFER,Vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER,static_cast<GLsizeiptr>(Scene.Vertices.size()*sizeof(FVertex)),
|
||||
Scene.Vertices.data(),GL_STATIC_DRAW);
|
||||
glGenBuffers(1,&Ebo);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,Ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER,static_cast<GLsizeiptr>(Scene.Indices.size()*sizeof(std::uint32_t)),
|
||||
Scene.Indices.data(),GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||
reinterpret_cast<void*>(offsetof(FVertex,Position)));
|
||||
glEnableVertexAttribArray(1);glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||
reinterpret_cast<void*>(offsetof(FVertex,Normal)));
|
||||
glEnableVertexAttribArray(2);glVertexAttribPointer(2,1,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||
reinterpret_cast<void*>(offsetof(FVertex,Height)));
|
||||
glEnableVertexAttribArray(3);glVertexAttribPointer(3,3,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||
reinterpret_cast<void*>(offsetof(FVertex,WorldPosition)));
|
||||
glEnableVertexAttribArray(4);glVertexAttribPointer(4,4,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||
reinterpret_cast<void*>(offsetof(FVertex,TileData)));
|
||||
glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);glClearColor(0.035f,0.055f,0.075f,1.0f);
|
||||
glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
||||
glUseProgram(Program);
|
||||
glUniform1f(glGetUniformLocation(Program,"uTanHalfHorizontal"),static_cast<float>(TanHalfHorizontal));
|
||||
glUniform1f(glGetUniformLocation(Program,"uTanHalfVertical"),static_cast<float>(TanHalfVertical));
|
||||
glUniform1f(glGetUniformLocation(Program,"uNear"),static_cast<float>(Near));
|
||||
glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast<float>(Far));
|
||||
glUniform1i(glGetUniformLocation(Program,"uReadableMode"),ReadableMode?1:0);
|
||||
glUniform1i(glGetUniformLocation(Program,"uTileEdges"),TileEdges?1:0);
|
||||
glUniform1f(glGetUniformLocation(Program,"uHeightCenter"),
|
||||
static_cast<float>(Scene.CameraMeters[2]));
|
||||
glUniform1f(glGetUniformLocation(Program,"uHeightRange"),64.0f);
|
||||
glUniform1f(glGetUniformLocation(Program,"uTileEdgeWidth"),
|
||||
static_cast<float>(TileEdgeWidthMeters));
|
||||
glUniform3f(glGetUniformLocation(Program,"uLightDirection"),-0.42f,-0.58f,0.70f);
|
||||
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr);
|
||||
glFinish();
|
||||
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
|
||||
glReadPixels(0,0,Width,Height,GL_RGBA,GL_UNSIGNED_BYTE,Pixels.data());
|
||||
const GLenum GlError=glGetError();
|
||||
const bool PngGood=GlError==GL_NO_ERROR&&WritePng(Output,Width,Height,Pixels);
|
||||
if(!PngGood)std::fprintf(stderr,"[OffscreenGL] render or PNG write failed (GL=0x%x)\n",GlError);
|
||||
else
|
||||
{
|
||||
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<unsigned long long>(Scene.Triangles),Scene.TileCount,
|
||||
static_cast<unsigned long long>(Scene.SkirtTriangles),
|
||||
ReadableMode?"readable":"normal",TileEdges?"on":"off",TileEdgeWidthMeters,
|
||||
static_cast<unsigned long long>(SizeError?0:Bytes),Output.string().c_str());
|
||||
}
|
||||
glDeleteProgram(Program);glDeleteBuffers(1,&Ebo);glDeleteBuffers(1,&Vbo);
|
||||
glDeleteVertexArrays(1,&Vao);glDeleteFramebuffers(1,&Framebuffer);
|
||||
glDeleteRenderbuffers(1,&DepthBuffer);glDeleteTextures(1,&ColorTexture);
|
||||
eglMakeCurrent(Display,EGL_NO_SURFACE,EGL_NO_SURFACE,EGL_NO_CONTEXT);
|
||||
eglDestroyContext(Display,Context);eglDestroySurface(Display,Surface);eglTerminate(Display);
|
||||
return PngGood;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc,char** argv)
|
||||
{
|
||||
if(argc<3)
|
||||
{
|
||||
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--tile-edges=on|off] [--tile-edge-width-m=N]\n",argv[0]);
|
||||
return 2;
|
||||
}
|
||||
int Width=2560,Height=1440;double Fov=90.0;
|
||||
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
||||
bool HasCamera=false;std::array<double,3> Camera{};
|
||||
bool ReadableMode=true,TileEdges=false;double TileEdgeWidthMeters=0.15;
|
||||
for(int I=3;I<argc;++I)
|
||||
{
|
||||
const std::string_view Arg(argv[I]);
|
||||
if(Arg.starts_with("--size="))
|
||||
{
|
||||
const std::string Value(Arg.substr(7));
|
||||
if(std::sscanf(Value.c_str(),"%dx%d",&Width,&Height)!=2||Width<=0||Height<=0)
|
||||
{std::fprintf(stderr,"Invalid image size\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--fov-deg="))
|
||||
{
|
||||
char* End=nullptr;const std::string Value(Arg.substr(10));Fov=std::strtod(Value.c_str(),&End);
|
||||
if(End==Value.c_str()||*End!='\0'||Fov<=1.0||Fov>=179.0)
|
||||
{std::fprintf(stderr,"Invalid horizontal FOV\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--camera-m="))
|
||||
{
|
||||
const std::string Value(Arg.substr(11));
|
||||
if(std::sscanf(Value.c_str(),"%lf,%lf,%lf",&Camera[0],&Camera[1],&Camera[2])!=3
|
||||
||!std::isfinite(Camera[0])||!std::isfinite(Camera[1])||!std::isfinite(Camera[2]))
|
||||
{std::fprintf(stderr,"Invalid camera position\n");return 2;}
|
||||
HasCamera=true;
|
||||
}
|
||||
else if(Arg.starts_with("--yaw-deg=")||Arg.starts_with("--pitch-deg="))
|
||||
{
|
||||
const bool IsYaw=Arg.starts_with("--yaw-deg=");
|
||||
const std::size_t Prefix=IsYaw?10:12;
|
||||
const std::string Value(Arg.substr(Prefix));char* End=nullptr;
|
||||
const double Parsed=std::strtod(Value.c_str(),&End);
|
||||
if(End==Value.c_str()||*End!='\0'||!std::isfinite(Parsed))
|
||||
{std::fprintf(stderr,"Invalid camera angle\n");return 2;}
|
||||
if(IsYaw){Yaw=Parsed;HasYaw=true;}else{Pitch=Parsed;HasPitch=true;}
|
||||
}
|
||||
else if(Arg.starts_with("--shade="))
|
||||
{
|
||||
const std::string_view Mode=Arg.substr(8);
|
||||
if(Mode=="readable")ReadableMode=true;
|
||||
else if(Mode=="normal")ReadableMode=false;
|
||||
else {std::fprintf(stderr,"Invalid shading mode\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--tile-edges="))
|
||||
{
|
||||
const std::string_view Mode=Arg.substr(13);
|
||||
if(Mode=="on")TileEdges=true;
|
||||
else if(Mode=="off")TileEdges=false;
|
||||
else {std::fprintf(stderr,"Invalid tile-edge mode\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--tile-edge-width-m="))
|
||||
{
|
||||
char* End=nullptr;const std::string Value(Arg.substr(20));
|
||||
TileEdgeWidthMeters=std::strtod(Value.c_str(),&End);
|
||||
if(End==Value.c_str()||*End!='\0'||!std::isfinite(TileEdgeWidthMeters)
|
||||
||TileEdgeWidthMeters<=0.0||TileEdgeWidthMeters>5.0)
|
||||
{std::fprintf(stderr,"Invalid tile-edge width\n");return 2;}
|
||||
}
|
||||
else {std::fprintf(stderr,"Unknown option: %.*s\n",static_cast<int>(Arg.size()),Arg.data());return 2;}
|
||||
}
|
||||
FScene Scene;std::string Error;
|
||||
if(!LoadScene(argv[1],Scene,Error))
|
||||
{std::fprintf(stderr,"[OffscreenCapture] FAIL: %s\n",Error.c_str());return 2;}
|
||||
if(HasCamera)Scene.CameraMeters=Camera;
|
||||
if(HasYaw)Scene.YawDegrees=Yaw;
|
||||
if(HasPitch)Scene.PitchDegrees=Pitch;
|
||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f shade=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
||||
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
||||
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
||||
ReadableMode?"readable":"normal",TileEdges?"on":"off",Scene.Vertices.size(),
|
||||
static_cast<unsigned long long>(Scene.Triangles));
|
||||
return Render(Scene,argv[2],Width,Height,Fov,ReadableMode,TileEdges,
|
||||
TileEdgeWidthMeters)?0:3;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,721 @@
|
||||
#pragma once
|
||||
|
||||
#include "StreamingWorld.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace StandaloneScreenSpaceGate
|
||||
{
|
||||
inline constexpr double OnePixelRadians=1.57079632679489661923/2560.0;
|
||||
|
||||
struct FErrorRow
|
||||
{
|
||||
int Lod=0;
|
||||
std::uint64_t Samples=0,Missing=0,Floating=0;
|
||||
double P50=0.0,P99=0.0,Maximum=0.0;
|
||||
double VerticalP50=0.0,VerticalP99=0.0,VerticalMaximum=0.0;
|
||||
double MaximumNormalDistanceMeters=0.0,MaximumX=0.0,MaximumY=0.0;
|
||||
double MaximumReferenceZ=0.0,MaximumDisplayedZ=0.0;
|
||||
double MaximumSlopeDegrees=0.0;
|
||||
std::array<std::uint64_t,4> SlopeSamples{};
|
||||
std::array<double,4> SlopeP99{},SlopeMaximum{};
|
||||
bool Valid=false;
|
||||
};
|
||||
|
||||
struct FPopRow
|
||||
{
|
||||
int CoarseLod=0;
|
||||
std::uint64_t Samples=0,Cracks=0,Floating=0;
|
||||
double P50=0.0,P99=0.0,Maximum=0.0;
|
||||
double VerticalP50=0.0,VerticalP99=0.0,VerticalMaximum=0.0;
|
||||
double MaximumNormalDistanceMeters=0.0,MaximumX=0.0,MaximumY=0.0;
|
||||
double MaximumReferenceZ=0.0,MaximumDisplayedZ=0.0;
|
||||
double MaximumSlopeDegrees=0.0;
|
||||
std::array<std::uint64_t,4> SlopeSamples{};
|
||||
std::array<double,4> SlopeP99{},SlopeMaximum{};
|
||||
bool Valid=false;
|
||||
};
|
||||
|
||||
struct FSurfaceRoot
|
||||
{
|
||||
double ZMeters=0.0;
|
||||
double SlopeX=0.0;
|
||||
double SlopeY=0.0;
|
||||
};
|
||||
|
||||
struct FViewBasis
|
||||
{
|
||||
std::array<double,3> Right{};
|
||||
std::array<double,3> Up{};
|
||||
};
|
||||
|
||||
struct FErrorAccumulator
|
||||
{
|
||||
std::vector<double> Pixels,VerticalPixels;
|
||||
std::array<std::vector<double>,4> BySlope;
|
||||
double Maximum=-1.0,MaximumVertical=0.0,MaximumNormalDistanceMeters=0.0;
|
||||
double MaximumX=0.0,MaximumY=0.0,MaximumSlopeDegrees=0.0;
|
||||
double MaximumReferenceZ=0.0,MaximumDisplayedZ=0.0;
|
||||
std::array<std::uint64_t,4> SlopeSamples{};
|
||||
};
|
||||
|
||||
inline double Percentile(std::vector<double>& Values,double P);
|
||||
|
||||
inline FViewBasis MakeViewBasis(double YawDegrees,double PitchDegrees)
|
||||
{
|
||||
constexpr double Pi=3.14159265358979323846;
|
||||
const double Yaw=YawDegrees*Pi/180.0,Pitch=PitchDegrees*Pi/180.0;
|
||||
const std::array<double,3> Forward={std::sin(Yaw)*std::cos(Pitch),
|
||||
std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)};
|
||||
FViewBasis Basis;
|
||||
Basis.Right={std::cos(Yaw),-std::sin(Yaw),0.0};
|
||||
Basis.Up={Basis.Right[1]*Forward[2]-Basis.Right[2]*Forward[1],
|
||||
Basis.Right[2]*Forward[0]-Basis.Right[0]*Forward[2],
|
||||
Basis.Right[0]*Forward[1]-Basis.Right[1]*Forward[0]};
|
||||
return Basis;
|
||||
}
|
||||
|
||||
inline std::size_t SlopeBin(double Degrees)
|
||||
{
|
||||
if(Degrees<30.0)return 0;
|
||||
if(Degrees<45.0)return 1;
|
||||
if(Degrees<60.0)return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
inline void AddError(FErrorAccumulator& Accumulator,double DeltaZMeters,
|
||||
const FSurfaceRoot& Root,double PixelBudgetMeters,const FViewBasis& View,
|
||||
double X,double Y)
|
||||
{
|
||||
constexpr double Pi=3.14159265358979323846;
|
||||
const double GradientSquared=Root.SlopeX*Root.SlopeX+Root.SlopeY*Root.SlopeY;
|
||||
const double NormalLength=std::sqrt(1.0+GradientSquared);
|
||||
const double NormalX=-Root.SlopeX/NormalLength;
|
||||
const double NormalY=-Root.SlopeY/NormalLength;
|
||||
const double NormalZ=1.0/NormalLength;
|
||||
const double NormalDistance=std::abs(DeltaZMeters)/NormalLength;
|
||||
const double ProjectedNormal=std::hypot(
|
||||
NormalX*View.Right[0]+NormalY*View.Right[1]+NormalZ*View.Right[2],
|
||||
NormalX*View.Up[0]+NormalY*View.Up[1]+NormalZ*View.Up[2]);
|
||||
const double ProjectedVertical=std::hypot(View.Right[2],View.Up[2]);
|
||||
const double NormalPixels=NormalDistance*ProjectedNormal/PixelBudgetMeters;
|
||||
const double VerticalPixels=std::abs(DeltaZMeters)*ProjectedVertical/PixelBudgetMeters;
|
||||
const double SlopeDegrees=std::atan(std::sqrt(GradientSquared))*180.0/Pi;
|
||||
Accumulator.Pixels.push_back(NormalPixels);
|
||||
Accumulator.VerticalPixels.push_back(VerticalPixels);
|
||||
const std::size_t Bin=SlopeBin(SlopeDegrees);
|
||||
Accumulator.BySlope[Bin].push_back(NormalPixels);
|
||||
++Accumulator.SlopeSamples[Bin];
|
||||
if(NormalPixels>Accumulator.Maximum)
|
||||
{
|
||||
Accumulator.Maximum=NormalPixels;
|
||||
Accumulator.MaximumVertical=VerticalPixels;
|
||||
Accumulator.MaximumNormalDistanceMeters=NormalDistance;
|
||||
Accumulator.MaximumX=X;Accumulator.MaximumY=Y;
|
||||
Accumulator.MaximumSlopeDegrees=SlopeDegrees;
|
||||
Accumulator.MaximumReferenceZ=Root.ZMeters;
|
||||
Accumulator.MaximumDisplayedZ=Root.ZMeters+DeltaZMeters;
|
||||
}
|
||||
}
|
||||
|
||||
inline void AddMissing(FErrorAccumulator& Accumulator)
|
||||
{
|
||||
Accumulator.Pixels.push_back(2.0);
|
||||
Accumulator.VerticalPixels.push_back(2.0);
|
||||
}
|
||||
|
||||
inline void FinalizeError(FErrorRow& Row,FErrorAccumulator& Accumulator)
|
||||
{
|
||||
Row.Samples=Accumulator.Pixels.size();
|
||||
Row.P50=Percentile(Accumulator.Pixels,0.50);
|
||||
Row.P99=Percentile(Accumulator.Pixels,0.99);
|
||||
Row.Maximum=Accumulator.Pixels.empty()?0.0
|
||||
:*std::max_element(Accumulator.Pixels.begin(),Accumulator.Pixels.end());
|
||||
Row.VerticalP50=Percentile(Accumulator.VerticalPixels,0.50);
|
||||
Row.VerticalP99=Percentile(Accumulator.VerticalPixels,0.99);
|
||||
Row.VerticalMaximum=Accumulator.VerticalPixels.empty()?0.0
|
||||
:*std::max_element(Accumulator.VerticalPixels.begin(),Accumulator.VerticalPixels.end());
|
||||
Row.MaximumNormalDistanceMeters=Accumulator.MaximumNormalDistanceMeters;
|
||||
Row.MaximumX=Accumulator.MaximumX;Row.MaximumY=Accumulator.MaximumY;
|
||||
Row.MaximumReferenceZ=Accumulator.MaximumReferenceZ;
|
||||
Row.MaximumDisplayedZ=Accumulator.MaximumDisplayedZ;
|
||||
Row.MaximumSlopeDegrees=Accumulator.MaximumSlopeDegrees;
|
||||
Row.SlopeSamples=Accumulator.SlopeSamples;
|
||||
for(std::size_t Bin=0;Bin<Accumulator.BySlope.size();++Bin)
|
||||
{
|
||||
Row.SlopeP99[Bin]=Percentile(Accumulator.BySlope[Bin],0.99);
|
||||
Row.SlopeMaximum[Bin]=Accumulator.BySlope[Bin].empty()?0.0
|
||||
:*std::max_element(Accumulator.BySlope[Bin].begin(),Accumulator.BySlope[Bin].end());
|
||||
}
|
||||
}
|
||||
|
||||
struct FHeightGrid
|
||||
{
|
||||
int Side=65;
|
||||
double MinimumX=0.0,MaximumX=0.0,MinimumY=0.0,MaximumY=0.0;
|
||||
std::vector<std::vector<double>> Hits;
|
||||
|
||||
FHeightGrid()=default;
|
||||
FHeightGrid(int InSide,double MinX,double MaxX,double MinY,double MaxY)
|
||||
:Side(InSide),MinimumX(MinX),MaximumX(MaxX),MinimumY(MinY),MaximumY(MaxY),
|
||||
Hits(static_cast<std::size_t>(InSide)*InSide)
|
||||
{}
|
||||
|
||||
double X(int Ix) const
|
||||
{return MinimumX+(MaximumX-MinimumX)*Ix/(Side-1);}
|
||||
double Y(int Iy) const
|
||||
{return MinimumY+(MaximumY-MinimumY)*Iy/(Side-1);}
|
||||
|
||||
void Add(int Ix,int Iy,double Z)
|
||||
{
|
||||
auto& Values=Hits[static_cast<std::size_t>(Iy)*Side+Ix];
|
||||
Values.push_back(Z);
|
||||
}
|
||||
|
||||
const std::vector<double>& At(int Ix,int Iy) const
|
||||
{return Hits[static_cast<std::size_t>(Iy)*Side+Ix];}
|
||||
};
|
||||
|
||||
inline double Percentile(std::vector<double>& Values,double P)
|
||||
{
|
||||
if(Values.empty())return 0.0;
|
||||
std::sort(Values.begin(),Values.end());
|
||||
const std::size_t Index=std::min(Values.size()-1,
|
||||
static_cast<std::size_t>(std::ceil(P*Values.size())-1));
|
||||
return Values[Index];
|
||||
}
|
||||
|
||||
inline double TileAabbDistanceMeters(const FVoxelTileKey& Key,
|
||||
const FVector& CameraVoxels,double VoxelMeters)
|
||||
{
|
||||
const double Extent=static_cast<double>(Key.ExtentVoxels());
|
||||
const double Min[3]={static_cast<double>(Key.OriginVoxels().X),
|
||||
static_cast<double>(Key.OriginVoxels().Y),static_cast<double>(Key.OriginVoxels().Z)};
|
||||
const double Cam[3]={CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z};
|
||||
double Squared=0.0;
|
||||
for(int Axis=0;Axis<3;++Axis)
|
||||
{
|
||||
const double Delta=Cam[Axis]<Min[Axis]?Min[Axis]-Cam[Axis]
|
||||
:Cam[Axis]>Min[Axis]+Extent?Cam[Axis]-(Min[Axis]+Extent):0.0;
|
||||
Squared+=Delta*Delta;
|
||||
}
|
||||
return std::sqrt(Squared)*VoxelMeters;
|
||||
}
|
||||
|
||||
inline bool TileHasNativeSurface(const FVoxelTileKey& Key,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& Assembly,
|
||||
double VoxelMeters)
|
||||
{
|
||||
const double Extent=static_cast<double>(Key.ExtentVoxels());
|
||||
const double X=(static_cast<double>(Key.OriginVoxels().X)+Extent*0.5)*256.0;
|
||||
const double Y=(static_cast<double>(Key.OriginVoxels().Y)+Extent*0.5)*256.0;
|
||||
const double MinZ=static_cast<double>(Key.OriginVoxels().Z);
|
||||
const double MaxZ=MinZ+Extent;
|
||||
for(const auto& Plan:Assembly.plan.strates)
|
||||
{
|
||||
VoxelForgeCore::Heightfield2D Field(Assembly.recipe,Plan);
|
||||
if(!Field.isValid())continue;
|
||||
const auto Column=Field.point(static_cast<std::int64_t>(std::llround(X)),
|
||||
static_cast<std::int64_t>(std::llround(Y)),0);
|
||||
const double Ground=static_cast<double>(Column.groundHeightSubvoxels)/256.0;
|
||||
const double Cap=static_cast<double>(Column.capHeightSubvoxels)/256.0;
|
||||
const double Margin=std::max(2.0,0.25/VoxelMeters);
|
||||
if(Ground<Cap&&((Ground>=MinZ-Margin&&Ground<=MaxZ+Margin)
|
||||
||(Cap>=MinZ-Margin&&Cap<=MaxZ+Margin)))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline FHeightGrid Rasterize(std::span<const FStandaloneCpuTile* const> Tiles,
|
||||
const FVoxelTileKey& Patch,double VoxelMeters,int Side=65)
|
||||
{
|
||||
const double Extent=static_cast<double>(Patch.ExtentVoxels())*VoxelMeters;
|
||||
const double MinX=static_cast<double>(Patch.OriginVoxels().X)*VoxelMeters;
|
||||
const double MinY=static_cast<double>(Patch.OriginVoxels().Y)*VoxelMeters;
|
||||
FHeightGrid Grid(Side,MinX,MinX+Extent,MinY,MinY+Extent);
|
||||
for(const FStandaloneCpuTile* Tile:Tiles)
|
||||
{
|
||||
if(!Tile||Tile->Indices.size()<3)continue;
|
||||
const std::size_t TriangleCount=Tile->Indices.size()/3;
|
||||
for(std::size_t Triangle=0;Triangle<TriangleCount;++Triangle)
|
||||
{
|
||||
if(Triangle<Tile->TriangleSkirtFaceMasks.size()
|
||||
&&Tile->TriangleSkirtFaceMasks[Triangle]!=0)continue;
|
||||
const std::uint32_t I0=Tile->Indices[Triangle*3];
|
||||
const std::uint32_t I1=Tile->Indices[Triangle*3+1];
|
||||
const std::uint32_t I2=Tile->Indices[Triangle*3+2];
|
||||
if(I0>=Tile->Vertices.size()||I1>=Tile->Vertices.size()||I2>=Tile->Vertices.size())continue;
|
||||
const auto& A=Tile->Vertices[I0].Position;
|
||||
const auto& B=Tile->Vertices[I1].Position;
|
||||
const auto& C=Tile->Vertices[I2].Position;
|
||||
const double Denominator=(B[1]-C[1])*(A[0]-C[0])+(C[0]-B[0])*(A[1]-C[1]);
|
||||
if(std::abs(Denominator)<1e-10)continue;
|
||||
const double MinTriX=std::min({A[0],B[0],C[0]}),MaxTriX=std::max({A[0],B[0],C[0]});
|
||||
const double MinTriY=std::min({A[1],B[1],C[1]}),MaxTriY=std::max({A[1],B[1],C[1]});
|
||||
const double StepX=Extent/(Side-1),StepY=Extent/(Side-1);
|
||||
const int Ix0=std::clamp(static_cast<int>(std::ceil((MinTriX-MinX)/StepX)),0,Side-1);
|
||||
const int Ix1=std::clamp(static_cast<int>(std::floor((MaxTriX-MinX)/StepX)),0,Side-1);
|
||||
const int Iy0=std::clamp(static_cast<int>(std::ceil((MinTriY-MinY)/StepY)),0,Side-1);
|
||||
const int Iy1=std::clamp(static_cast<int>(std::floor((MaxTriY-MinY)/StepY)),0,Side-1);
|
||||
for(int Iy=Iy0;Iy<=Iy1;++Iy)
|
||||
for(int Ix=Ix0;Ix<=Ix1;++Ix)
|
||||
{
|
||||
const double X=Grid.X(Ix),Y=Grid.Y(Iy);
|
||||
const double W0=((B[1]-C[1])*(X-C[0])+(C[0]-B[0])*(Y-C[1]))/Denominator;
|
||||
const double W1=((C[1]-A[1])*(X-C[0])+(A[0]-C[0])*(Y-C[1]))/Denominator;
|
||||
const double W2=1.0-W0-W1;
|
||||
if(W0<-1e-6||W1<-1e-6||W2<-1e-6)continue;
|
||||
Grid.Add(Ix,Iy,W0*A[2]+W1*B[2]+W2*C[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for(auto& Values:Grid.Hits)
|
||||
{
|
||||
std::sort(Values.begin(),Values.end());
|
||||
Values.erase(std::unique(Values.begin(),Values.end(),[](double A,double B)
|
||||
{return std::abs(A-B)<0.02;}),Values.end());
|
||||
}
|
||||
return Grid;
|
||||
}
|
||||
|
||||
inline std::vector<FSurfaceRoot> ExactHeightRoots(const FHeightGrid& Grid,int Ix,int Iy,
|
||||
const FVoxelTileKey& Patch,double VoxelMeters,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& Assembly)
|
||||
{
|
||||
std::vector<FSurfaceRoot> Roots;
|
||||
const double XSubvoxels=Grid.X(Ix)/VoxelMeters*256.0;
|
||||
const double YSubvoxels=Grid.Y(Iy)/VoxelMeters*256.0;
|
||||
const auto X=static_cast<std::int64_t>(std::llround(XSubvoxels));
|
||||
const auto Y=static_cast<std::int64_t>(std::llround(YSubvoxels));
|
||||
const double MinZ=static_cast<double>(Patch.OriginVoxels().Z);
|
||||
const double MaxZ=MinZ+Patch.ExtentVoxels();
|
||||
constexpr std::int64_t NormalProbeSubvoxels=256;
|
||||
for(const auto& Plan:Assembly.plan.strates)
|
||||
{
|
||||
VoxelForgeCore::Heightfield2D Field(Assembly.recipe,Plan);
|
||||
if(!Field.isValid())continue;
|
||||
const auto Column=Field.point(X,Y,0);
|
||||
if(Column.groundHeightSubvoxels>=Column.capHeightSubvoxels)continue;
|
||||
const auto XMinus=Field.point(X-NormalProbeSubvoxels,Y,0);
|
||||
const auto XPlus=Field.point(X+NormalProbeSubvoxels,Y,0);
|
||||
const auto YMinus=Field.point(X,Y-NormalProbeSubvoxels,0);
|
||||
const auto YPlus=Field.point(X,Y+NormalProbeSubvoxels,0);
|
||||
const auto Slope=[&](std::int64_t Minus,std::int64_t Plus)
|
||||
{return static_cast<double>(Plus-Minus)/(2.0*NormalProbeSubvoxels);};
|
||||
const double Ground=static_cast<double>(Column.groundHeightSubvoxels)/256.0*VoxelMeters;
|
||||
const double Cap=static_cast<double>(Column.capHeightSubvoxels)/256.0*VoxelMeters;
|
||||
const double PlanMinZ=static_cast<double>(Plan.bottomChunkZ)*32.0*VoxelMeters;
|
||||
const double PlanMaxZ=static_cast<double>(Plan.topChunkZ+1)*32.0*VoxelMeters;
|
||||
if(Ground>=MinZ*VoxelMeters-0.25*VoxelMeters
|
||||
&&Ground<=MaxZ*VoxelMeters+0.25*VoxelMeters
|
||||
&&Ground>=PlanMinZ-0.25*VoxelMeters&&Ground<=PlanMaxZ+0.25*VoxelMeters)
|
||||
Roots.push_back({Ground,Slope(XMinus.groundHeightSubvoxels,
|
||||
XPlus.groundHeightSubvoxels),Slope(YMinus.groundHeightSubvoxels,
|
||||
YPlus.groundHeightSubvoxels)});
|
||||
if(Cap>=MinZ*VoxelMeters-0.25*VoxelMeters
|
||||
&&Cap<=MaxZ*VoxelMeters+0.25*VoxelMeters
|
||||
&&Cap>=PlanMinZ-0.25*VoxelMeters&&Cap<=PlanMaxZ+0.25*VoxelMeters)
|
||||
Roots.push_back({Cap,Slope(XMinus.capHeightSubvoxels,
|
||||
XPlus.capHeightSubvoxels),Slope(YMinus.capHeightSubvoxels,
|
||||
YPlus.capHeightSubvoxels)});
|
||||
}
|
||||
std::sort(Roots.begin(),Roots.end(),[](const FSurfaceRoot& A,const FSurfaceRoot& B)
|
||||
{return A.ZMeters<B.ZMeters;});
|
||||
Roots.erase(std::unique(Roots.begin(),Roots.end(),[](const FSurfaceRoot& A,
|
||||
const FSurfaceRoot& B){return std::abs(A.ZMeters-B.ZMeters)<0.01;}),Roots.end());
|
||||
return Roots;
|
||||
}
|
||||
|
||||
inline bool MatchNearest(const std::vector<double>& Hits,const std::vector<bool>& Used,
|
||||
double Root,std::size_t& OutIndex,double& OutError)
|
||||
{
|
||||
double Best=std::numeric_limits<double>::infinity();std::size_t BestIndex=Hits.size();
|
||||
for(std::size_t I=0;I<Hits.size();++I)
|
||||
if(I>=Used.size()||!Used[I])
|
||||
{
|
||||
const double Error=std::abs(Hits[I]-Root);
|
||||
if(Error<Best){Best=Error;BestIndex=I;}
|
||||
}
|
||||
if(BestIndex==Hits.size())return false;
|
||||
OutIndex=BestIndex;OutError=Best;return true;
|
||||
}
|
||||
|
||||
inline FErrorRow CompareToExact(const FHeightGrid& MeshGrid,const FVoxelTileKey& Patch,
|
||||
double VoxelMeters,double PixelBudgetMeters,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& Assembly,const FViewBasis& View)
|
||||
{
|
||||
FErrorRow Row;Row.Lod=Patch.Level;Row.Valid=true;
|
||||
FErrorAccumulator Accumulator;
|
||||
for(int Iy=0;Iy<MeshGrid.Side;++Iy)
|
||||
for(int Ix=0;Ix<MeshGrid.Side;++Ix)
|
||||
{
|
||||
const auto Roots=ExactHeightRoots(MeshGrid,Ix,Iy,Patch,VoxelMeters,Assembly);
|
||||
if(Roots.empty()&&MeshGrid.At(Ix,Iy).empty())continue;
|
||||
std::vector<bool> Used(MeshGrid.At(Ix,Iy).size(),false);
|
||||
for(const FSurfaceRoot& Root:Roots)
|
||||
{
|
||||
std::size_t HitIndex=0;double ErrorMeters=0.0;
|
||||
if(!MatchNearest(MeshGrid.At(Ix,Iy),Used,Root.ZMeters,HitIndex,ErrorMeters))
|
||||
{
|
||||
++Row.Missing;
|
||||
AddMissing(Accumulator);
|
||||
continue;
|
||||
}
|
||||
Used[HitIndex]=true;
|
||||
AddError(Accumulator,MeshGrid.At(Ix,Iy)[HitIndex]-Root.ZMeters,
|
||||
Root,PixelBudgetMeters,View,MeshGrid.X(Ix),MeshGrid.Y(Iy));
|
||||
++Row.Samples;
|
||||
}
|
||||
// Heightfield2D declares ground and cap roots only. Other mesh intersections may
|
||||
// be valid tunnel geometry; the exact-mesh comparison checks extra pieces.
|
||||
}
|
||||
FinalizeError(Row,Accumulator);
|
||||
return Row;
|
||||
}
|
||||
|
||||
inline FErrorRow CompareToReference(const FHeightGrid& MeshGrid,
|
||||
const FHeightGrid& ReferenceGrid,const FVoxelTileKey& Patch,double VoxelMeters,
|
||||
double PixelBudgetMeters,const VoxelForgeCore::WorldAssemblyManifest& Assembly,
|
||||
const FViewBasis& View)
|
||||
{
|
||||
FErrorRow Row;Row.Lod=Patch.Level;Row.Valid=true;
|
||||
FErrorAccumulator Accumulator;
|
||||
for(int Iy=0;Iy<ReferenceGrid.Side;++Iy)
|
||||
for(int Ix=0;Ix<ReferenceGrid.Side;++Ix)
|
||||
{
|
||||
const auto& Reference=ReferenceGrid.At(Ix,Iy);
|
||||
const auto& Mesh=MeshGrid.At(Ix,Iy);
|
||||
if(Reference.empty()&&Mesh.empty())continue;
|
||||
const auto Roots=ExactHeightRoots(ReferenceGrid,Ix,Iy,Patch,VoxelMeters,Assembly);
|
||||
std::vector<bool> UsedReference(Reference.size(),false),UsedMesh(Mesh.size(),false);
|
||||
for(std::size_t ReferenceIndex=0;ReferenceIndex<Reference.size();++ReferenceIndex)
|
||||
{
|
||||
const double ReferenceZ=Reference[ReferenceIndex];
|
||||
UsedReference[ReferenceIndex]=true;
|
||||
std::size_t MeshIndex=0;double MeshError=0.0;
|
||||
if(!MatchNearest(Mesh,UsedMesh,ReferenceZ,MeshIndex,MeshError))
|
||||
{
|
||||
++Row.Missing;AddMissing(Accumulator);continue;
|
||||
}
|
||||
UsedMesh[MeshIndex]=true;
|
||||
FSurfaceRoot Root{ReferenceZ,0.0,0.0};
|
||||
if(!Roots.empty())
|
||||
{
|
||||
const auto BestRoot=std::min_element(Roots.begin(),Roots.end(),
|
||||
[ReferenceZ](const FSurfaceRoot& A,const FSurfaceRoot& B)
|
||||
{return std::abs(A.ZMeters-ReferenceZ)<std::abs(B.ZMeters-ReferenceZ);});
|
||||
if(BestRoot!=Roots.end()&&std::abs(BestRoot->ZMeters-ReferenceZ)<1.0)
|
||||
Root=*BestRoot;
|
||||
}
|
||||
AddError(Accumulator,Mesh[MeshIndex]-Reference[ReferenceIndex],
|
||||
Root,PixelBudgetMeters,View,ReferenceGrid.X(Ix),ReferenceGrid.Y(Iy));
|
||||
}
|
||||
for(std::size_t I=0;I<Mesh.size();++I)
|
||||
if(!UsedMesh[I]){++Row.Floating;AddMissing(Accumulator);}
|
||||
}
|
||||
FinalizeError(Row,Accumulator);
|
||||
return Row;
|
||||
}
|
||||
|
||||
inline FPopRow ComparePop(const FHeightGrid& Coarse,const FHeightGrid& Fine,
|
||||
const FVoxelTileKey& Patch,double VoxelMeters,double PixelBudgetMeters,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& Assembly,const FViewBasis& View)
|
||||
{
|
||||
FPopRow Row;Row.CoarseLod=Patch.Level;Row.Valid=true;
|
||||
FErrorAccumulator Accumulator;
|
||||
for(int Iy=0;Iy<Coarse.Side;++Iy)
|
||||
for(int Ix=0;Ix<Coarse.Side;++Ix)
|
||||
{
|
||||
const auto& CoarseHits=Coarse.At(Ix,Iy);
|
||||
const auto& FineHits=Fine.At(Ix,Iy);
|
||||
const auto Roots=ExactHeightRoots(Coarse,Ix,Iy,Patch,VoxelMeters,Assembly);
|
||||
if(Roots.empty()&&CoarseHits.empty()&&FineHits.empty())continue;
|
||||
std::vector<bool> CoarseUsed(CoarseHits.size(),false);
|
||||
std::vector<bool> FineUsed(FineHits.size(),false);
|
||||
for(const FSurfaceRoot& Root:Roots)
|
||||
{
|
||||
std::size_t CoarseIndex=0,FineIndex=0;
|
||||
double CoarseError=0.0,FineError=0.0;
|
||||
if(!MatchNearest(CoarseHits,CoarseUsed,Root.ZMeters,CoarseIndex,CoarseError)
|
||||
||!MatchNearest(FineHits,FineUsed,Root.ZMeters,FineIndex,FineError))
|
||||
{
|
||||
++Row.Cracks;AddMissing(Accumulator);continue;
|
||||
}
|
||||
CoarseUsed[CoarseIndex]=true;
|
||||
FineUsed[FineIndex]=true;
|
||||
AddError(Accumulator,CoarseHits[CoarseIndex]-FineHits[FineIndex],
|
||||
Root,PixelBudgetMeters,View,Coarse.X(Ix),Coarse.Y(Iy));
|
||||
++Row.Samples;
|
||||
}
|
||||
}
|
||||
FErrorRow Stats;Stats.Lod=Patch.Level;Stats.Valid=true;
|
||||
FinalizeError(Stats,Accumulator);
|
||||
Row.Samples=Stats.Samples;Row.P50=Stats.P50;Row.P99=Stats.P99;Row.Maximum=Stats.Maximum;
|
||||
Row.VerticalP50=Stats.VerticalP50;Row.VerticalP99=Stats.VerticalP99;
|
||||
Row.VerticalMaximum=Stats.VerticalMaximum;
|
||||
Row.MaximumNormalDistanceMeters=Stats.MaximumNormalDistanceMeters;
|
||||
Row.MaximumX=Stats.MaximumX;Row.MaximumY=Stats.MaximumY;
|
||||
Row.MaximumReferenceZ=Stats.MaximumReferenceZ;
|
||||
Row.MaximumDisplayedZ=Stats.MaximumDisplayedZ;
|
||||
Row.MaximumSlopeDegrees=Stats.MaximumSlopeDegrees;
|
||||
Row.SlopeSamples=Stats.SlopeSamples;Row.SlopeP99=Stats.SlopeP99;
|
||||
Row.SlopeMaximum=Stats.SlopeMaximum;
|
||||
return Row;
|
||||
}
|
||||
|
||||
inline std::array<double,9> ClosestDisplayedDistancesMeters(
|
||||
const FStandaloneOwnerWorld& World,const FVector& CameraVoxels,
|
||||
std::uint32_t VoxelSizeMicrons)
|
||||
{
|
||||
const double VoxelMeters=static_cast<double>(VoxelSizeMicrons)/1000000.0;
|
||||
std::array<double,9> Minimums{};
|
||||
Minimums.fill(std::numeric_limits<double>::infinity());
|
||||
for(int Lod=0;Lod<=8;++Lod)
|
||||
{
|
||||
const int PhaseCount=1<<Lod;
|
||||
for(int Phase=0;Phase<PhaseCount;++Phase)
|
||||
{
|
||||
FVector Probe=CameraVoxels;
|
||||
Probe.X=static_cast<float>(Phase*32.0+32.0-1.0/256.0);
|
||||
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,Probe);
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
{
|
||||
if(Tile.Level!=Lod)continue;
|
||||
Minimums[Lod]=std::min(Minimums[Lod],
|
||||
TileAabbDistanceMeters(Tile,Probe,VoxelMeters));
|
||||
}
|
||||
}
|
||||
}
|
||||
for(double& Distance:Minimums)
|
||||
if(!std::isfinite(Distance))Distance=0.0;
|
||||
return Minimums;
|
||||
}
|
||||
|
||||
inline std::array<double,9> ClipmapClosestDisplayedDistancesMeters(
|
||||
const FStandaloneOwnerWorld& World,std::uint32_t VoxelSizeMicrons)
|
||||
{
|
||||
const double VoxelMeters=static_cast<double>(VoxelSizeMicrons)/1000000.0;
|
||||
std::array<double,9> Distances{};
|
||||
for(int Lod=1;Lod<=8;++Lod)
|
||||
Distances[Lod]=static_cast<double>(World.ClipRadius)
|
||||
*static_cast<double>(CHUNK_SIZE)*(1ull<<(Lod-1))*VoxelMeters;
|
||||
return Distances;
|
||||
}
|
||||
|
||||
inline void PrintSlopeTail(const char* Kind,int Lod,const std::array<std::uint64_t,4>& Counts,
|
||||
const std::array<double,4>& P99,const std::array<double,4>& Maximum)
|
||||
{
|
||||
constexpr const char* Labels[4]={"lt30","30to45","45to60","ge60"};
|
||||
for(std::size_t Bin=0;Bin<Counts.size();++Bin)
|
||||
std::printf("[ScreenSpaceSlopeTail] kind=%s lod=%d slope_deg=%s samples=%llu p99_px=%.4f max_px=%.4f\n",
|
||||
Kind,Lod,Labels[Bin],static_cast<unsigned long long>(Counts[Bin]),P99[Bin],Maximum[Bin]);
|
||||
}
|
||||
|
||||
inline void PrintKnownFailureControl(double CellMeters,double DisplayDistanceMeters,
|
||||
const FViewBasis& View)
|
||||
{
|
||||
constexpr double Slope=100.0;
|
||||
const double LateralMeters=CellMeters/200.0;
|
||||
const double VerticalMeters=Slope*LateralMeters;
|
||||
const double NormalLength=std::sqrt(1.0+Slope*Slope);
|
||||
const double NormalDistance=VerticalMeters/NormalLength;
|
||||
const std::array<double,3> Normal={-Slope/NormalLength,0.0,1.0/NormalLength};
|
||||
const double ProjectedNormal=std::hypot(
|
||||
Normal[0]*View.Right[0]+Normal[1]*View.Right[1]+Normal[2]*View.Right[2],
|
||||
Normal[0]*View.Up[0]+Normal[1]*View.Up[1]+Normal[2]*View.Up[2]);
|
||||
const double PixelBudgetMeters=DisplayDistanceMeters*OnePixelRadians;
|
||||
const double VerticalPixels=VerticalMeters/PixelBudgetMeters;
|
||||
const double NormalPixels=NormalDistance*ProjectedNormal/PixelBudgetMeters;
|
||||
const bool Passed=VerticalPixels>4.0&&NormalPixels<1.0;
|
||||
std::printf("[ScreenSpaceKnownFailureControl] case=steep-plane-lateral-offset slope_deg=%.4f cell_m=%.6f lateral_m=%.6f cell_fraction=%.6f vertical_error_m=%.6f vertical_height_px=%.4f closest_normal_m=%.6f projected_normal_px=%.4f status=%s\n",
|
||||
std::atan(Slope)*180.0/3.14159265358979323846,CellMeters,LateralMeters,
|
||||
LateralMeters/CellMeters,VerticalMeters,VerticalPixels,NormalDistance,NormalPixels,
|
||||
Passed?"EXPECTED_VERTICAL_FALSE_POSITIVE":"CONTROL_FAILED");
|
||||
}
|
||||
|
||||
inline bool Run(const std::array<std::optional<FStandaloneCpuTile>,9>& SampleTiles,
|
||||
FStandaloneStreamingGenerator& Streamer,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& Assembly,
|
||||
const std::array<double,9>& Distances,std::uint32_t VoxelSizeMicrons,int MaxLod,
|
||||
const std::array<std::int64_t,9>& PixelBudgetsSubvoxels,
|
||||
double CameraYawDegrees,double CameraPitchDegrees)
|
||||
{
|
||||
const double VoxelMeters=static_cast<double>(VoxelSizeMicrons)/1000000.0;
|
||||
const FViewBasis View=MakeViewBasis(CameraYawDegrees,CameraPitchDegrees);
|
||||
std::array<double,9> Budgets{};
|
||||
std::array<FErrorRow,9> ErrorRows{};
|
||||
std::array<FPopRow,9> PopRows{};
|
||||
bool GatePass=true;
|
||||
std::printf("[ScreenSpaceGate] angular_pixel=%.9f rad (horizontal_fov=90deg,width=2560) lod>=3_limit=1.0px sampling=65x65 skirts=excluded metric=local_surface_normal_projected field_reference=direct-Heightfield2D-roots normal_probe_vox=1 view_yaw_deg=%.3f view_pitch_deg=%.3f\n",
|
||||
OnePixelRadians,CameraYawDegrees,CameraPitchDegrees);
|
||||
for(int Lod=0;Lod<=MaxLod&&Lod<9;++Lod)
|
||||
{
|
||||
Budgets[Lod]=Distances[Lod]*OnePixelRadians;
|
||||
std::printf("[ScreenSpaceDistance] lod=%d closest_m=%.3f pixel_budget_m=%.6f pixel_budget_cm=%.3f pixel_budget_subvoxels=%lld rule=%s\n",
|
||||
Lod,Distances[Lod],Budgets[Lod],Budgets[Lod]*100.0,
|
||||
static_cast<long long>(PixelBudgetsSubvoxels[Lod]),Lod<3?"exact-2cm":"1px");
|
||||
}
|
||||
const double LOD3CellMeters=static_cast<double>(CHUNK_SIZE*(1<<3))*VoxelMeters/64.0;
|
||||
PrintKnownFailureControl(LOD3CellMeters,Distances[3],View);
|
||||
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true,0);
|
||||
for(int Lod=3;Lod<=MaxLod&&Lod<9;++Lod)
|
||||
{
|
||||
if(!SampleTiles[Lod]||SampleTiles[Lod]->Vertices.empty())
|
||||
{
|
||||
std::printf("[ScreenSpaceGateLOD] lod=%d status=NO_SURFACE_TILE\n",Lod);
|
||||
GatePass=false;continue;
|
||||
}
|
||||
const FStandaloneCpuTile& CoarseTile=*SampleTiles[Lod];
|
||||
const FStandaloneCpuTile* CoarsePtr=&CoarseTile;
|
||||
const std::span<const FStandaloneCpuTile* const> CoarseSpan(&CoarsePtr,1);
|
||||
const FHeightGrid CoarseGrid=Rasterize(CoarseSpan,CoarseTile.Key,VoxelMeters);
|
||||
const FStandaloneCpuTile ExactTile=Streamer.GenerateDiagnostic(
|
||||
CoarseTile.Key,0,1,true);
|
||||
const FStandaloneCpuTile* ExactPointer=&ExactTile;
|
||||
const std::span<const FStandaloneCpuTile* const> ExactSpan(&ExactPointer,1);
|
||||
const FHeightGrid ExactGrid=Rasterize(ExactSpan,ExactTile.Key,VoxelMeters);
|
||||
ErrorRows[Lod]=CompareToReference(CoarseGrid,ExactGrid,CoarseTile.Key,VoxelMeters,
|
||||
Budgets[Lod],Assembly,View);
|
||||
const FErrorRow& Error=ErrorRows[Lod];
|
||||
const bool LodPass=Error.Valid&&Error.Missing==0&&Error.Floating==0
|
||||
&&Error.Samples>=16&&Error.Maximum<=1.0;
|
||||
GatePass=GatePass&&LodPass;
|
||||
std::printf("[ScreenSpaceGateLOD] lod=%d key=(%d,%d,%d) samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xy_m=(%.3f,%.3f) max_slope_deg=%.2f missing=%llu floating=%llu status=%s\n",
|
||||
Lod,CoarseTile.Key.Coord.X,CoarseTile.Key.Coord.Y,CoarseTile.Key.Coord.Z,
|
||||
static_cast<unsigned long long>(Error.Samples),Error.P50,Error.P99,Error.Maximum,
|
||||
Error.VerticalP50,Error.VerticalP99,Error.VerticalMaximum,
|
||||
Error.MaximumNormalDistanceMeters,Error.MaximumX,Error.MaximumY,
|
||||
Error.MaximumSlopeDegrees,
|
||||
static_cast<unsigned long long>(Error.Missing),static_cast<unsigned long long>(Error.Floating),
|
||||
LodPass?"PASS":"REJECT");
|
||||
PrintSlopeTail("same_lod",Lod,Error.SlopeSamples,Error.SlopeP99,Error.SlopeMaximum);
|
||||
|
||||
const FErrorRow FieldError=CompareToExact(CoarseGrid,CoarseTile.Key,VoxelMeters,
|
||||
Budgets[Lod],Assembly,View);
|
||||
const bool FieldPass=FieldError.Valid&&FieldError.Missing==0&&FieldError.Floating==0
|
||||
&&FieldError.Samples>=16&&FieldError.Maximum<=1.0;
|
||||
GatePass=GatePass&&FieldPass;
|
||||
std::printf("[ScreenSpaceGateFieldLOD] lod=%d samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xyz_m=(%.3f,%.3f,%.3f) max_slope_deg=%.2f missing=%llu floating=%llu status=%s\n",
|
||||
Lod,static_cast<unsigned long long>(FieldError.Samples),FieldError.P50,
|
||||
FieldError.P99,FieldError.Maximum,FieldError.VerticalP50,FieldError.VerticalP99,
|
||||
FieldError.VerticalMaximum,FieldError.MaximumNormalDistanceMeters,
|
||||
FieldError.MaximumX,FieldError.MaximumY,FieldError.MaximumDisplayedZ,
|
||||
FieldError.MaximumSlopeDegrees,
|
||||
static_cast<unsigned long long>(FieldError.Missing),
|
||||
static_cast<unsigned long long>(FieldError.Floating),FieldPass?"PASS":"REJECT");
|
||||
PrintSlopeTail("field",Lod,FieldError.SlopeSamples,FieldError.SlopeP99,
|
||||
FieldError.SlopeMaximum);
|
||||
|
||||
bool FaultControlsPass=true;
|
||||
const FHeightGrid MissingGrid(ExactGrid.Side,ExactGrid.MinimumX,ExactGrid.MaximumX,
|
||||
ExactGrid.MinimumY,ExactGrid.MaximumY);
|
||||
const FErrorRow MissingError=CompareToReference(MissingGrid,ExactGrid,
|
||||
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
||||
const bool MissingRejected=MissingError.Missing>0&&MissingError.Maximum>1.0;
|
||||
FaultControlsPass=FaultControlsPass&&MissingRejected;
|
||||
std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-displayed-surface missing=%llu status=%s\n",
|
||||
Lod,static_cast<unsigned long long>(MissingError.Missing),
|
||||
MissingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
||||
|
||||
FStandaloneCpuTile FloatingTile=CoarseTile;
|
||||
const std::uint32_t FloatingVertexBase=static_cast<std::uint32_t>(FloatingTile.Vertices.size());
|
||||
for(FStandaloneCpuVertex Vertex:CoarseTile.Vertices)
|
||||
{
|
||||
Vertex.Position[2]+=2.0f;
|
||||
FloatingTile.Vertices.push_back(Vertex);
|
||||
}
|
||||
for(const std::uint32_t Index:CoarseTile.Indices)
|
||||
FloatingTile.Indices.push_back(FloatingVertexBase+Index);
|
||||
FloatingTile.TriangleSkirtFaceMasks.insert(FloatingTile.TriangleSkirtFaceMasks.end(),
|
||||
CoarseTile.TriangleSkirtFaceMasks.begin(),CoarseTile.TriangleSkirtFaceMasks.end());
|
||||
const FStandaloneCpuTile* FloatingPointer=&FloatingTile;
|
||||
const std::span<const FStandaloneCpuTile* const> FloatingSpan(&FloatingPointer,1);
|
||||
const FHeightGrid FloatingGrid=Rasterize(FloatingSpan,CoarseTile.Key,VoxelMeters);
|
||||
const FErrorRow FloatingError=CompareToReference(FloatingGrid,ExactGrid,
|
||||
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
||||
const bool FloatingRejected=FloatingError.Floating>0||FloatingError.Maximum>1.0;
|
||||
FaultControlsPass=FaultControlsPass&&FloatingRejected;
|
||||
std::printf("[ScreenSpaceFaultInjection] lod=%d case=2m-floating-surface floating=%llu max_px=%.4f status=%s\n",
|
||||
Lod,static_cast<unsigned long long>(FloatingError.Floating),FloatingError.Maximum,
|
||||
FloatingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
||||
|
||||
std::vector<FStandaloneCpuTile> FineTiles;
|
||||
FineTiles.reserve(8);
|
||||
for(int Dz=0;Dz<=1;++Dz)
|
||||
for(int Dy=0;Dy<=1;++Dy)
|
||||
for(int Dx=0;Dx<=1;++Dx)
|
||||
{
|
||||
const FVoxelTileKey FineKey(FIntVector(2*CoarseTile.Key.Coord.X+Dx,
|
||||
2*CoarseTile.Key.Coord.Y+Dy,2*CoarseTile.Key.Coord.Z+Dz),Lod-1);
|
||||
FStandaloneCpuTile FineTile=Streamer.GenerateDiagnostic(FineKey);
|
||||
if(!FineTile.Vertices.empty())FineTiles.push_back(std::move(FineTile));
|
||||
}
|
||||
std::vector<const FStandaloneCpuTile*> FinePointers;
|
||||
FinePointers.reserve(FineTiles.size());
|
||||
for(const auto& Tile:FineTiles)FinePointers.push_back(&Tile);
|
||||
const auto FineSpan=std::span<const FStandaloneCpuTile* const>(
|
||||
FinePointers.data(),FinePointers.size());
|
||||
const FHeightGrid FineGrid=Rasterize(FineSpan,CoarseTile.Key,VoxelMeters);
|
||||
PopRows[Lod]=ComparePop(CoarseGrid,FineGrid,CoarseTile.Key,VoxelMeters,
|
||||
Budgets[Lod],Assembly,View);
|
||||
const FPopRow& Pop=PopRows[Lod];
|
||||
const bool PopPass=Pop.Valid&&Pop.Cracks==0&&Pop.Floating==0&&Pop.Maximum<=1.0;
|
||||
GatePass=GatePass&&PopPass;
|
||||
std::printf("[ScreenSpaceSwap] lod=%d_to_%d samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xyz_m=(%.3f,%.3f,%.3f) max_slope_deg=%.2f cracks=%llu floating=%llu status=%s\n",
|
||||
Lod,Lod-1,static_cast<unsigned long long>(Pop.Samples),
|
||||
Pop.P50,Pop.P99,Pop.Maximum,Pop.VerticalP50,Pop.VerticalP99,Pop.VerticalMaximum,
|
||||
Pop.MaximumNormalDistanceMeters,Pop.MaximumX,Pop.MaximumY,
|
||||
Pop.MaximumDisplayedZ,
|
||||
Pop.MaximumSlopeDegrees,static_cast<unsigned long long>(Pop.Cracks),
|
||||
static_cast<unsigned long long>(Pop.Floating),
|
||||
PopPass?"PASS":"REJECT");
|
||||
PrintSlopeTail("swap",Lod,Pop.SlopeSamples,Pop.SlopeP99,Pop.SlopeMaximum);
|
||||
|
||||
const FHeightGrid MissingFineGrid(CoarseGrid.Side,CoarseGrid.MinimumX,
|
||||
CoarseGrid.MaximumX,CoarseGrid.MinimumY,CoarseGrid.MaximumY);
|
||||
const FPopRow MissingFineError=ComparePop(CoarseGrid,MissingFineGrid,
|
||||
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
||||
const bool CrackRejected=MissingFineError.Cracks>0&&MissingFineError.Maximum>1.0;
|
||||
FaultControlsPass=FaultControlsPass&&CrackRejected;
|
||||
GatePass=GatePass&&FaultControlsPass;
|
||||
std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-fine-ring-surface cracks=%llu max_px=%.4f status=%s\n",
|
||||
Lod,static_cast<unsigned long long>(MissingFineError.Cracks),
|
||||
MissingFineError.Maximum,CrackRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
||||
|
||||
bool NegativeControlsPass=true;
|
||||
const auto CheckNegative=[&](const char* Name,std::uint32_t Drop,std::uint32_t Stride)
|
||||
{
|
||||
FStandaloneCpuTile Bad=Streamer.GenerateDiagnostic(
|
||||
CoarseTile.Key,Drop,Stride,true);
|
||||
const FStandaloneCpuTile* BadPointer=&Bad;
|
||||
const std::span<const FStandaloneCpuTile* const> BadSpan(&BadPointer,1);
|
||||
const FHeightGrid BadGrid=Rasterize(BadSpan,Bad.Key,VoxelMeters);
|
||||
const FErrorRow BadError=CompareToReference(BadGrid,ExactGrid,
|
||||
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
||||
const bool Rejected=BadError.Missing>0||BadError.Floating>0
|
||||
||BadError.Samples<16||BadError.Maximum>1.0;
|
||||
NegativeControlsPass=NegativeControlsPass&&Rejected;
|
||||
std::printf("[ScreenSpaceNegative] lod=%d case=%s p50_px=%.4f p99_px=%.4f max_px=%.4f missing=%llu floating=%llu status=%s\n",
|
||||
Lod,Name,BadError.P50,BadError.P99,BadError.Maximum,
|
||||
static_cast<unsigned long long>(BadError.Missing),
|
||||
static_cast<unsigned long long>(BadError.Floating),
|
||||
Rejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
||||
};
|
||||
CheckNegative("s13-global-octave-drop",1,1);
|
||||
CheckNegative("stride-2-column-interpolation",0,2);
|
||||
GatePass=GatePass&&NegativeControlsPass;
|
||||
}
|
||||
std::printf("[ScreenSpaceGateResult] status=%s\n",GatePass?"PASS":"REJECT");
|
||||
return GatePass;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
@@ -35,8 +36,17 @@ namespace
|
||||
bool StreamPipeline=false;
|
||||
bool StreamHash=false;
|
||||
bool ProfileColumnOverlap=false;
|
||||
bool UseSharedColumnCache=true;
|
||||
bool ForceGenerate=false;
|
||||
bool MeshCoverage=false;
|
||||
bool ScreenSpaceGate=false;
|
||||
bool AuditUniformBlockProofs=false;
|
||||
bool ProfileLodWork=false;
|
||||
bool AuditBasePageEquivalence=false;
|
||||
bool AuditRouteParity=false;
|
||||
bool RegionCrossing=false;
|
||||
bool AuditTileShapeReadiness=false;
|
||||
int DiagnosticSlowShapeCompileMs=0;
|
||||
bool HasSeedOverride=false;
|
||||
int SeedOverride=0;
|
||||
bool HasRenderDistanceOverride=false;
|
||||
@@ -46,8 +56,14 @@ namespace
|
||||
int TlsProbeWorkers=0;
|
||||
std::string TlsProbeTouch="none";
|
||||
bool HasCamera=false;
|
||||
bool HasCameraStep=false;
|
||||
bool AutoSurfaceCamera=false;
|
||||
FVector CameraMeters=FVector::ZeroVector;
|
||||
FVector CameraStepMeters=FVector::ZeroVector;
|
||||
float CameraYawDegrees=0.0f;
|
||||
float CameraPitchDegrees=0.0f;
|
||||
std::filesystem::path MeshExportPath;
|
||||
std::filesystem::path WorldAssemblyManifestOutputPath;
|
||||
};
|
||||
|
||||
bool ParseInteger(std::string_view text,int& output)
|
||||
@@ -61,13 +77,24 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseCamera(std::string_view text,FVector& meters)
|
||||
bool ParseCamera(std::string_view text,FVector& meters,float& yawDegrees,float& pitchDegrees)
|
||||
{
|
||||
const std::string values(text);
|
||||
float x=0.0f,y=0.0f,z=0.0f,yaw=0.0f,pitch=0.0f;
|
||||
if(std::sscanf(values.c_str(),"%f,%f,%f,%f,%f",&x,&y,&z,&yaw,&pitch)!=5)
|
||||
return false;
|
||||
meters={x,y,z};
|
||||
yawDegrees=yaw;
|
||||
pitchDegrees=pitch;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseCameraStep(std::string_view text,FVector& meters)
|
||||
{
|
||||
const std::string values(text);
|
||||
float x=0.0f,y=0.0f,z=0.0f;
|
||||
if(std::sscanf(values.c_str(),"%f,%f,%f",&x,&y,&z)!=3)return false;
|
||||
meters={x,y,z};
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -85,10 +112,22 @@ namespace
|
||||
std::printf("Usage: %s [owner_world.json] --world-assembler-prototype --stream-benchmark --stream-pipeline [options]\n"
|
||||
" --workers=N --clip-radius=N --max-clip-level=N --max-level=N\n"
|
||||
" --camera=X,Y,Z,YAW,PITCH (position in metres) --stream-hash\n"
|
||||
" --camera-step=X,Y,Z (optional second view step in metres)\n"
|
||||
" --region-crossing (compile entering Core regions at the camera step)\n"
|
||||
" --column-cache=shared|thread-local (default: shared)\n"
|
||||
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
|
||||
" --world-assembler-manifest-output=FILE (write the resolved plan for diagnostics)\n"
|
||||
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
||||
" --seed=N --recipe=surface-world|floating-islands\n"
|
||||
" --auto-surface-camera (place the 27-tile cube on this seed's surface)\n"
|
||||
" --mesh-coverage (report non-empty tiles and indexed triangles)\n"
|
||||
" --screen-space-gate (measure display error and historical negative controls)\n"
|
||||
" --audit-uniform-block-proofs (fill and check every interval-certified block)\n"
|
||||
" --profile-lod-work (report per-LOD density sample counts)\n"
|
||||
" --audit-base-page-equivalence (hash one tile through both shape routes)\n"
|
||||
" --audit-route-parity (compare page, native-only, exact-query and mesh routes)\n"
|
||||
" --audit-tile-shape-readiness (run a deliberately mis-marked tile against the native-only guard)\n"
|
||||
" --diagnostic-slow-shape-compile-ms=N (diagnostic overlap control; default 0)\n"
|
||||
" --render-distance-chunks=N (0 disables the extra far ring)\n"
|
||||
" --tls-probe=N (Linux worker TLS/stack measurement; N=0..12)\n"
|
||||
" --tls-probe-touch=none|surface-world|floating-islands (first-use probe)\n"
|
||||
@@ -103,6 +142,13 @@ namespace
|
||||
else if(arg=="--profile-column-overlap")options.ProfileColumnOverlap=true;
|
||||
else if(arg=="--force-generate")options.ForceGenerate=true;
|
||||
else if(arg=="--mesh-coverage")options.MeshCoverage=true;
|
||||
else if(arg=="--screen-space-gate")options.ScreenSpaceGate=true;
|
||||
else if(arg=="--audit-uniform-block-proofs")options.AuditUniformBlockProofs=true;
|
||||
else if(arg=="--profile-lod-work")options.ProfileLodWork=true;
|
||||
else if(arg=="--audit-base-page-equivalence")options.AuditBasePageEquivalence=true;
|
||||
else if(arg=="--audit-route-parity")options.AuditRouteParity=true;
|
||||
else if(arg=="--region-crossing")options.RegionCrossing=true;
|
||||
else if(arg=="--audit-tile-shape-readiness")options.AuditTileShapeReadiness=true;
|
||||
else if(arg=="--auto-surface-camera")options.AutoSurfaceCamera=true;
|
||||
else if(arg.starts_with("--recipe="))
|
||||
{
|
||||
@@ -135,10 +181,38 @@ namespace
|
||||
}
|
||||
else if(arg.starts_with("--camera="))
|
||||
{
|
||||
if(!ParseCamera(arg.substr(9),options.CameraMeters))
|
||||
if(!ParseCamera(arg.substr(9),options.CameraMeters,
|
||||
options.CameraYawDegrees,options.CameraPitchDegrees))
|
||||
{std::fprintf(stderr,"Invalid --camera value: %.*s\n",static_cast<int>(arg.size()),arg.data());return false;}
|
||||
options.HasCamera=true;
|
||||
}
|
||||
else if(arg.starts_with("--camera-step="))
|
||||
{
|
||||
if(!ParseCameraStep(arg.substr(std::string_view("--camera-step=").size()),
|
||||
options.CameraStepMeters))
|
||||
{std::fprintf(stderr,"Invalid --camera-step value: %.*s\n",static_cast<int>(arg.size()),arg.data());return false;}
|
||||
options.HasCameraStep=true;
|
||||
}
|
||||
else if(arg.starts_with("--column-cache="))
|
||||
{
|
||||
const std::string_view mode=arg.substr(std::string_view("--column-cache=").size());
|
||||
if(mode=="shared")options.UseSharedColumnCache=true;
|
||||
else if(mode=="thread-local")options.UseSharedColumnCache=false;
|
||||
else {std::fprintf(stderr,"Invalid --column-cache value: %.*s\n",static_cast<int>(arg.size()),arg.data());return false;}
|
||||
}
|
||||
else if(arg.starts_with("--mesh-export="))
|
||||
{
|
||||
options.MeshExportPath=std::filesystem::path(arg.substr(14));
|
||||
if(options.MeshExportPath.empty())
|
||||
{std::fprintf(stderr,"--mesh-export needs a file path\n");return false;}
|
||||
}
|
||||
else if(arg.starts_with("--world-assembler-manifest-output="))
|
||||
{
|
||||
options.WorldAssemblyManifestOutputPath=std::filesystem::path(
|
||||
arg.substr(std::string_view("--world-assembler-manifest-output=").size()));
|
||||
if(options.WorldAssemblyManifestOutputPath.empty())
|
||||
{std::fprintf(stderr,"--world-assembler-manifest-output needs a file path\n");return false;}
|
||||
}
|
||||
else if(arg.starts_with("--workers="))
|
||||
{if(!readInt("--workers=",options.WorkerCount))return false;}
|
||||
else if(arg.starts_with("--clip-radius="))
|
||||
@@ -149,6 +223,8 @@ namespace
|
||||
{if(!readInt("--max-level=",options.MaxLevel))return false;}
|
||||
else if(arg.starts_with("--far-horizon-lod8-step="))
|
||||
{if(!readInt("--far-horizon-lod8-step=",options.FarHorizonLOD8InteriorStepVoxels))return false;}
|
||||
else if(arg.starts_with("--diagnostic-slow-shape-compile-ms="))
|
||||
{if(!readInt("--diagnostic-slow-shape-compile-ms=",options.DiagnosticSlowShapeCompileMs))return false;}
|
||||
else if(arg.starts_with("--process-affinity=")||arg.starts_with("--process-priority=")
|
||||
||arg.starts_with("--frame-limit="))
|
||||
{
|
||||
@@ -163,8 +239,11 @@ namespace
|
||||
options.ClipRadius=std::clamp(options.ClipRadius,1,64);
|
||||
options.MaxClipLevel=std::clamp(options.MaxClipLevel,0,8);
|
||||
options.FarHorizonLOD8InteriorStepVoxels=std::max(0,options.FarHorizonLOD8InteriorStepVoxels);
|
||||
options.DiagnosticSlowShapeCompileMs=std::clamp(options.DiagnosticSlowShapeCompileMs,0,30000);
|
||||
if(!options.TlsProbeRequested&&options.TlsProbeTouch!="none")
|
||||
{std::fprintf(stderr,"--tls-probe-touch requires --tls-probe=N\n");return false;}
|
||||
if(options.RegionCrossing&&!options.HasCameraStep)
|
||||
{std::fprintf(stderr,"--region-crossing requires --camera-step=X,Y,Z\n");return false;}
|
||||
if(options.TlsProbeRequested)return true;
|
||||
if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline)
|
||||
{
|
||||
@@ -183,10 +262,12 @@ int main(int argc,char** argv)
|
||||
if(!ParseOptions(argc,argv,options))return 2;
|
||||
if(options.TlsProbeRequested)
|
||||
return StandaloneTlsProbe::Run(options.TlsProbeWorkers,options.TlsProbeTouch);
|
||||
std::printf("[HeadlessConfig] data=%s workers=%d clip_radius=%d max_clip_level=%d max_level=%d hash=%s column_overlap=%s force_generate=%s recipe=%s seed_override=%s render_distance_override=%s\n",
|
||||
std::printf("[HeadlessConfig] data=%s workers=%d clip_radius=%d max_clip_level=%d max_level=%d hash=%s column_overlap=%s force_generate=%s recipe=%s shared_column_cache=%s camera_step_m=(%.3f,%.3f,%.3f) seed_override=%s render_distance_override=%s\n",
|
||||
options.DataPath.string().c_str(),options.WorkerCount,options.ClipRadius,options.MaxClipLevel,
|
||||
options.MaxLevel,options.StreamHash?"on":"off",options.ProfileColumnOverlap?"on":"off",
|
||||
options.ForceGenerate?"on":"off",options.FloatingIslandsRecipe?"floating-islands":"surface-world",
|
||||
options.UseSharedColumnCache?"on":"off",options.CameraStepMeters.X,options.CameraStepMeters.Y,
|
||||
options.CameraStepMeters.Z,
|
||||
options.HasSeedOverride?"yes":"no",options.HasRenderDistanceOverride?"yes":"no");
|
||||
std::printf("[ColdTimeline] stage=owner_data_load begin_ms=%.3f\n",
|
||||
std::chrono::duration<double,std::milli>(Clock::now()-coldStart).count());
|
||||
@@ -242,6 +323,22 @@ int main(int argc,char** argv)
|
||||
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||
*ownerWorld,ownerJsonRoot,worldAssembly,error))
|
||||
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
||||
if(!options.WorldAssemblyManifestOutputPath.empty())
|
||||
{
|
||||
std::error_code directoryError;
|
||||
const auto parent=options.WorldAssemblyManifestOutputPath.parent_path();
|
||||
if(!parent.empty())std::filesystem::create_directories(parent,directoryError);
|
||||
if(directoryError)
|
||||
{std::fprintf(stderr,"Manifest output directory failed: %s\n",directoryError.message().c_str());return 2;}
|
||||
std::ofstream manifestFile(options.WorldAssemblyManifestOutputPath,std::ios::binary|std::ios::trunc);
|
||||
if(!manifestFile)
|
||||
{std::fprintf(stderr,"Cannot write manifest output: %s\n",options.WorldAssemblyManifestOutputPath.string().c_str());return 2;}
|
||||
manifestFile<<worldAssembly.ToJson()<<'\n';
|
||||
if(!manifestFile)
|
||||
{std::fprintf(stderr,"Manifest output write failed: %s\n",options.WorldAssemblyManifestOutputPath.string().c_str());return 2;}
|
||||
std::printf("[WorldAssemblyManifest] path=%s status=PASS\n",
|
||||
options.WorldAssemblyManifestOutputPath.string().c_str());
|
||||
}
|
||||
if(options.AutoSurfaceCamera)
|
||||
{
|
||||
constexpr std::int64_t UnitsPerVoxel=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
@@ -293,9 +390,17 @@ int main(int argc,char** argv)
|
||||
{std::fprintf(stderr,"Shape interval build failed: %s\n",error.c_str());return 2;}
|
||||
|
||||
FStandaloneCoreScheduler scheduler;
|
||||
const FVector cameraStepVoxels=options.HasCameraStep
|
||||
?FVector(options.CameraStepMeters.X*4.0f,options.CameraStepMeters.Y*4.0f,
|
||||
options.CameraStepMeters.Z*4.0f):FVector::ZeroVector;
|
||||
return StandaloneStreamBenchmark::RunPipelinedStreamingBenchmark(ownerWorld,snapshot,
|
||||
cameraVoxels,options.WorkerCount,options.MaxLevel,VoxelForgeCore::DetailVariant::coherent,
|
||||
scheduler,options.StreamHash,false,false,false,false,coldStart,
|
||||
options.ProfileColumnOverlap,options.FarHorizonLOD8InteriorStepVoxels,
|
||||
options.MeshCoverage);
|
||||
options.MeshCoverage,options.MeshExportPath,options.CameraYawDegrees,
|
||||
options.CameraPitchDegrees,options.ScreenSpaceGate,options.UseSharedColumnCache,
|
||||
cameraStepVoxels,options.AuditUniformBlockProofs,options.ProfileLodWork,
|
||||
options.RegionCrossing,options.AuditTileShapeReadiness,
|
||||
options.DiagnosticSlowShapeCompileMs,options.AuditBasePageEquivalence,
|
||||
options.AuditRouteParity);
|
||||
}
|
||||
@@ -60,6 +60,7 @@ struct FStandaloneCpuTile
|
||||
bool bHasPayloadHash=false;
|
||||
uint64 TopologyHash=14695981039346656037ull;
|
||||
bool bHasTopologyHash=false;
|
||||
bool bGenerationFailed=false;
|
||||
uint64 MeshPayloadAllocatedBytes=0;
|
||||
VoxelForgeCore::TileGenerationStats CoreDiagnostics{};
|
||||
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
|
||||
@@ -428,12 +429,14 @@ public:
|
||||
bool bInSkipColors=false,
|
||||
VoxelForgeCore::World* InCoreWorld=nullptr,
|
||||
bool bInCaptureColumnCoordinates=false,
|
||||
int32 InLod8HorizonInteriorStepVoxels=0)
|
||||
int32 InLod8HorizonInteriorStepVoxels=0,
|
||||
bool bInSharedColumnCache=true)
|
||||
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,16)),bHashDensity(bInHashDensity),
|
||||
MeshCache(InMeshCache),MaxLevelFilter(InMaxLevelFilter),
|
||||
CoreWorld(InCoreWorld),bSkipUvAndColors(bInSkipUvAndColors),bSkipMaterialGroups(bInSkipMaterialGroups),
|
||||
bSkipUV(bInSkipUV),bSkipColors(bInSkipColors),
|
||||
bCaptureColumnCoordinates(bInCaptureColumnCoordinates),
|
||||
bSharedColumnCache(bInSharedColumnCache),
|
||||
Lod8HorizonInteriorStepVoxels(std::max(0,InLod8HorizonInteriorStepVoxels))
|
||||
{
|
||||
VoxelGenLOD::InitializeConsoleSwitches();
|
||||
@@ -646,6 +649,18 @@ public:
|
||||
int32 GetWorkerCount()const{return WorkerCount;}
|
||||
uint64 GetCacheHits()const{return CacheHitCount.load(std::memory_order_relaxed);}
|
||||
|
||||
void SetMesherStageProfilingEnabled(bool Enabled)
|
||||
{bProfileMesherStages=Enabled;}
|
||||
void SetUniformBlockProofAuditEnabled(bool Enabled)
|
||||
{bAuditUniformBlockProofs=Enabled;}
|
||||
FStandaloneCpuTile GenerateDiagnostic(const FVoxelTileKey& Key,
|
||||
std::uint32_t GlobalOctaveDrop=0,std::uint32_t CellStride=1,
|
||||
bool ForceGenerate=false)
|
||||
{
|
||||
FStandaloneTileJob Job;Job.Key=Key;Job.StartedAt=FClock::now();Job.QueuedAt=Job.StartedAt;
|
||||
return Generate(Job,GlobalOctaveDrop,CellStride,ForceGenerate);
|
||||
}
|
||||
|
||||
private:
|
||||
FStandaloneStreamingUpdate UpdateDesiredSet(FStandaloneDesiredTileSet NewDesired,
|
||||
const std::vector<FVoxelTileKey>& ResidentKeys,uint64 CrossingIndex)
|
||||
@@ -761,12 +776,14 @@ private:
|
||||
Wake.notify_all();
|
||||
return Stats;
|
||||
}
|
||||
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job)
|
||||
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job,
|
||||
std::uint32_t DiagnosticGlobalOctaveDrop=0,std::uint32_t DiagnosticCellStride=1,
|
||||
bool ForceGenerate=false)
|
||||
{
|
||||
const auto StartTime=Job.StartedAt;
|
||||
FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing;
|
||||
Out.QueuedAt=Job.QueuedAt;Out.StartedAt=Job.StartedAt;
|
||||
if(MeshCache)
|
||||
if(MeshCache&&!ForceGenerate)
|
||||
{
|
||||
if(const FStandaloneCpuTile* Cached=MeshCache->Find(Job.Key))
|
||||
{
|
||||
@@ -804,13 +821,21 @@ private:
|
||||
Options.skipUvAndColors=bSkipUvAndColors;
|
||||
Options.skipMaterialGroups=bSkipMaterialGroups;Options.skipUv=bSkipUV;Options.skipColors=bSkipColors;
|
||||
Options.captureColumnCoordinates=bCaptureColumnCoordinates;
|
||||
Options.useSharedColumnCache=bSharedColumnCache;
|
||||
Options.lod8HorizonInteriorStepVoxels=Lod8HorizonInteriorStepVoxels;
|
||||
Options.profileMesherStages=bProfileMesherStages&&!ForceGenerate;
|
||||
Options.auditUniformBlockProofs=bAuditUniformBlockProofs&&!ForceGenerate;
|
||||
Options.diagnosticGlobalOctaveDrop=DiagnosticGlobalOctaveDrop;
|
||||
Options.diagnosticCellStride=std::max<std::uint32_t>(1,DiagnosticCellStride);
|
||||
Options.diagnosticColumnInterpolationStride=
|
||||
std::max<std::uint32_t>(1,DiagnosticCellStride);
|
||||
std::string Error;
|
||||
auto Result=CoreWorld->generateTile(
|
||||
{{Job.Key.Coord.X,Job.Key.Coord.Y,Job.Key.Coord.Z},Job.Key.Level},Options,Error);
|
||||
if(!Result)
|
||||
{
|
||||
std::fprintf(stderr,"[VoxelForgeCore] tile generation failed: %s\n",Error.c_str());
|
||||
Out.bGenerationFailed=true;
|
||||
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
||||
Out.CompletedAt=FClock::now();return Out;
|
||||
}
|
||||
@@ -976,7 +1001,10 @@ private:
|
||||
bool bSkipUV=false;
|
||||
bool bSkipColors=false;
|
||||
bool bCaptureColumnCoordinates=false;
|
||||
bool bSharedColumnCache=true;
|
||||
int32 Lod8HorizonInteriorStepVoxels=0;
|
||||
bool bProfileMesherStages=false;
|
||||
bool bAuditUniformBlockProofs=false;
|
||||
std::atomic<uint64> CacheHitCount{0};
|
||||
mutable std::mutex Mutex;
|
||||
std::condition_variable Wake;
|
||||
|
||||
@@ -121,7 +121,9 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
FVoxelSharedDensityGrid* OutPreEditDensityGrid,
|
||||
const FVoxelSharedDensityGrid* InSharedDensityGrid,
|
||||
std::vector<float>* OutDebugDensitySamples,
|
||||
FVoxelMarchingCubesStageTimings* OutStageTimings)
|
||||
FVoxelMarchingCubesStageTimings* OutStageTimings,
|
||||
FVoxelUniformBlockProofAuditStats* OutBlockProofAudit,
|
||||
bool bForceFullDensityCapture)
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
|
||||
const uint64 StageMesherStartCycles=OutStageTimings?FPlatformTime::Cycles64():0;
|
||||
@@ -130,6 +132,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
if (OutPreEditDensityGrid) { OutPreEditDensityGrid->Reset(); }
|
||||
if (OutDensitySampleCount) { *OutDensitySampleCount = 0; }
|
||||
if (OutStageTimings) { *OutStageTimings = FVoxelMarchingCubesStageTimings{}; }
|
||||
if (OutBlockProofAudit) { *OutBlockProofAudit = FVoxelUniformBlockProofAuditStats{}; }
|
||||
if (OutDebugDensitySamples) { OutDebugDensitySamples->clear(); }
|
||||
if (!Generator) return MeshData;
|
||||
const bool bEmitUV = !bStandaloneSkipUvAndColors && !bStandaloneSkipUV;
|
||||
@@ -550,22 +553,19 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
|
||||
MesherSetupTimer.End();
|
||||
|
||||
// Optional 8x8x8-cell block proof. The tile classifier has already returned Mixed, but
|
||||
// that is a conservative answer over the whole tile. A tile can still be uniformly solid
|
||||
// (or uniformly air) when its smaller blocks are each provable. Do this before allocating
|
||||
// or filling the density grid: the grid is only needed when at least one block can contain
|
||||
// a surface. A whole-tile early return is allowed only when every relevant block has the
|
||||
// SAME verdict; adjacent uniform blocks with opposite signs can contain a surface between
|
||||
// them. This is a proof-only optimization: ClassifyTile is conservative and no samples or
|
||||
// resolution are removed from a tile that reaches the mesher.
|
||||
// Optional 8x8x8-cell block proof. Mixed/Unknown blocks always fall back to exact density
|
||||
// sampling. The Core SurfaceWorld column-range proof at Step 2 can also omit evaluations
|
||||
// outside active blocks and their one-sample normal halo.
|
||||
TArray<uint8> BlockSkip;
|
||||
int32 BlocksPerAxis = 0;
|
||||
// LOD2's 16-cell tile is the remaining false-Mixed empty case in the streamed game path.
|
||||
// Its step-8 lattice can use the same proof-only 8-cell block partition as LOD0. Keep the
|
||||
// gate off for steps 2/16/32: their meshing work is already smaller than repeating a full
|
||||
// tile classifier per block, and this preserves the established coarse-LOD latency profile.
|
||||
// Step 2 is enabled only for a tile wholly inside one native SurfaceWorld stratum. Steps
|
||||
// 16/32 keep the historical gate off because their smaller grids do not amortize per-block queries.
|
||||
const bool bNativeSurfaceTileProof = Step == 2 && ShapeWorldContext != nullptr
|
||||
&& ShapeWorldContext->IsNativeSurfaceBox(OriginVoxels,Step,CellsPerAxis);
|
||||
const bool bUseProofBlocks = bUseBlockEarlyOut && !OutCaptureGrid
|
||||
&& (Step == 1 || Step == 8);
|
||||
&& (Step == 1 || Step == 8 || (Step == 2
|
||||
&& (bNativeSurfaceTileProof || OutBlockProofAudit != nullptr)));
|
||||
bool bCanSkipProofBlockGrid = bNativeSurfaceTileProof;
|
||||
if (bUseProofBlocks)
|
||||
{
|
||||
constexpr int32 BlockCells = 8;
|
||||
@@ -613,18 +613,27 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
VoxelDensityProfile::RecordClassifierCall(bSampleClassifier);
|
||||
}
|
||||
EVoxelTileClass Verdict;
|
||||
bool bBlockUnknown = false;
|
||||
bool bBlockNative = false;
|
||||
if(ShapeWorldContext&&Step<=2)
|
||||
{
|
||||
const auto ShapeVerdict=ShapeWorldContext->ClassifyBox(
|
||||
BlockOrigin,Step,BlockCells);
|
||||
Verdict=ShapeVerdict==EVoxelTileClass::AllSolid
|
||||
const auto ShapeVerdict=ShapeWorldContext->ClassifyBoxDetailed(
|
||||
BlockOrigin,Step,BlockCells,Step==2);
|
||||
Verdict=ShapeVerdict.Verdict==EVoxelTileClass::AllSolid
|
||||
?EVoxelTileClass::AllSolid
|
||||
:ShapeVerdict==EVoxelTileClass::AllAir
|
||||
:ShapeVerdict.Verdict==EVoxelTileClass::AllAir
|
||||
?EVoxelTileClass::AllAir:EVoxelTileClass::Mixed;
|
||||
bBlockUnknown=ShapeVerdict.bUnknown;
|
||||
bBlockNative=ShapeVerdict.bNativeSurface;
|
||||
if(Step==2&&!ShapeVerdict.bNativeSurface)
|
||||
bCanSkipProofBlockGrid=false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(Step==2)bCanSkipProofBlockGrid=false;
|
||||
Verdict=Generator->ClassifyTile(BlockOrigin,Step,BlockCells,
|
||||
bSampleClassifier ? &ClassifierStats : nullptr);
|
||||
}
|
||||
if (bSampleClassifier)
|
||||
{
|
||||
VoxelDensityProfile::AddSampledMeasurement(
|
||||
@@ -699,20 +708,70 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
ClassifierStats.RoomTailCycles);
|
||||
}
|
||||
}
|
||||
if (Verdict == EVoxelTileClass::AllSolid
|
||||
|| Verdict == EVoxelTileClass::AllAir)
|
||||
{
|
||||
if(OutBlockProofAudit&&Step==2&&bBlockNative)
|
||||
{
|
||||
++OutBlockProofAudit->CheckedBlocks;
|
||||
const int32 ProofMinX=BlockX*BlockCells-1;
|
||||
const int32 ProofMinY=BlockY*BlockCells-1;
|
||||
const int32 ProofMinZ=BlockZ*BlockCells-1;
|
||||
const int32 ProofMaxX=BlockX*BlockCells+BlockCells+1;
|
||||
const int32 ProofMaxY=BlockY*BlockCells+BlockCells+1;
|
||||
const int32 ProofMaxZ=BlockZ*BlockCells+BlockCells+1;
|
||||
bool bUniform=true;
|
||||
for(int32 Z=ProofMinZ;Z<=ProofMaxZ&&bUniform;++Z)
|
||||
for(int32 Y=ProofMinY;Y<=ProofMaxY&&bUniform;++Y)
|
||||
for(int32 X=ProofMinX;X<=ProofMaxX;++X)
|
||||
{
|
||||
++OutBlockProofAudit->CheckedSamples;
|
||||
const float Density=ShapeWorldContext
|
||||
?ShapeWorldContext->DensityAtLattice(
|
||||
OriginVoxels.X+X*Step,
|
||||
OriginVoxels.Y+Y*Step,
|
||||
OriginVoxels.Z+Z*Step)
|
||||
:Generator->GetDensityAtLattice(
|
||||
OriginVoxels.X+X*Step,
|
||||
OriginVoxels.Y+Y*Step,
|
||||
OriginVoxels.Z+Z*Step);
|
||||
if(!VoxelMath::IsFinite(Density)||Density==0.0f
|
||||
||(Verdict==EVoxelTileClass::AllSolid
|
||||
?Density>=0.0f:Density<=0.0f))
|
||||
{
|
||||
std::printf("[UniformBlockProofViolation] tile_origin=(%d,%d,%d) step=%d cells=%d block=(%d,%d,%d) sample=(%d,%d,%d) expected=%s density=%.9g native=%d unknown=%d\n",
|
||||
OriginVoxels.X,OriginVoxels.Y,OriginVoxels.Z,Step,
|
||||
CellsPerAxis,BlockX,BlockY,BlockZ,
|
||||
OriginVoxels.X+X*Step,OriginVoxels.Y+Y*Step,
|
||||
OriginVoxels.Z+Z*Step,
|
||||
Verdict==EVoxelTileClass::AllSolid?"solid":"air",
|
||||
Density,bBlockNative?1:0,bBlockUnknown?1:0);
|
||||
bUniform=false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!bUniform)
|
||||
{
|
||||
++OutBlockProofAudit->Violations;
|
||||
Verdict=EVoxelTileClass::Mixed;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Verdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
if(OutBlockProofAudit&&Step==2)
|
||||
{
|
||||
++OutBlockProofAudit->FallbackBlocks;
|
||||
if(bBlockUnknown)++OutBlockProofAudit->UnknownBlocks;
|
||||
}
|
||||
MixedBlockIndices.Add(
|
||||
((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX);
|
||||
}
|
||||
if (Verdict == EVoxelTileClass::AllSolid
|
||||
|| Verdict == EVoxelTileClass::AllAir)
|
||||
{
|
||||
// Keep the classifier result as a candidate until the exact density grid
|
||||
// used by this mesher has been filled. ClassifyTile is conservative by
|
||||
// contract, but its proof and the mesher share worker-local caches; a
|
||||
// cache replacement must never turn that contract into a cell skip by
|
||||
// itself. The post-grid sign check below is the final proof consumed by
|
||||
// the cell loop and clears any candidate whose actual MC vertices disagree.
|
||||
// In the ordinary path, candidates are checked against the filled grid.
|
||||
// The Step-2 supremum interval path is itself the proof used to omit fills.
|
||||
BlockSkip[((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX] =
|
||||
Verdict == EVoxelTileClass::AllSolid ? 1 : 2;
|
||||
if (!bHaveUniformBlock)
|
||||
@@ -806,7 +865,9 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
// ClassifyTile path already relies on this same non-Mixed proof contract. Return before
|
||||
// allocating the density grid; no samples are skipped for a tile that can contain
|
||||
// geometry, and the final geometry remains byte-identical because this path emits none.
|
||||
if (bHaveUniformBlock && bAllRelevantBlocksSame && bAllRelevantBlocksUniform)
|
||||
if (bHaveUniformBlock && bAllRelevantBlocksSame && bAllRelevantBlocksUniform
|
||||
&& !(Step == 2 && OutDebugDensitySamples != nullptr)
|
||||
&& !(bForceFullDensityCapture && OutDebugDensitySamples != nullptr))
|
||||
{
|
||||
return MeshData;
|
||||
}
|
||||
@@ -841,6 +902,43 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
&& SharedDeltaX + GridDim < ReuseGrid->Dim
|
||||
&& SharedDeltaY + GridDim < ReuseGrid->Dim
|
||||
&& SharedDeltaZ + GridDim < ReuseGrid->Dim;
|
||||
const bool bSkipProofBlockGrid = bUseProofBlocks && bCanSkipProofBlockGrid
|
||||
&& OutBlockProofAudit == nullptr && OutPreEditDensityGrid == nullptr
|
||||
&& !bUseSharedDensityGrid;
|
||||
static thread_local TArray<uint8> RequiredGridSamples;
|
||||
if(bSkipProofBlockGrid)
|
||||
{
|
||||
RequiredGridSamples.SetNumUninitialized(MDim*MDim*MDim);
|
||||
FMemory::Memzero(RequiredGridSamples.GetData(),
|
||||
static_cast<SIZE_T>(RequiredGridSamples.Num())*sizeof(uint8));
|
||||
constexpr int32 BlockCells=8;
|
||||
for(int32 BlockZ=0;BlockZ<BlocksPerAxis;++BlockZ)
|
||||
{
|
||||
const int32 BlockMinZ=BlockZ*BlockCells;
|
||||
const int32 BlockMaxZ=FMath::Min(CellsPerAxis-1,BlockMinZ+BlockCells-1);
|
||||
if(BlockMaxZ<CzLo||BlockMinZ>CzHi)continue;
|
||||
for(int32 BlockY=0;BlockY<BlocksPerAxis;++BlockY)
|
||||
for(int32 BlockX=0;BlockX<BlocksPerAxis;++BlockX)
|
||||
{
|
||||
const int32 BlockIndex=((BlockZ*BlocksPerAxis)+BlockY)*BlocksPerAxis+BlockX;
|
||||
if(BlockSkip[BlockIndex]!=0)continue;
|
||||
const int32 CellMinX=BlockX*BlockCells;
|
||||
const int32 CellMinY=BlockY*BlockCells;
|
||||
const int32 CellMaxX=FMath::Min(CellsPerAxis-1,CellMinX+BlockCells-1);
|
||||
const int32 CellMaxY=FMath::Min(CellsPerAxis-1,CellMinY+BlockCells-1);
|
||||
const int32 SampleMinX=FMath::Max(-1,CellMinX-1);
|
||||
const int32 SampleMaxX=FMath::Min(GridDim,CellMaxX+2);
|
||||
const int32 SampleMinY=FMath::Max(-1,CellMinY-1);
|
||||
const int32 SampleMaxY=FMath::Min(GridDim,CellMaxY+2);
|
||||
const int32 SampleMinZ=FMath::Max(GzLo,FMath::Max(CzLo,BlockMinZ)-1);
|
||||
const int32 SampleMaxZ=FMath::Min(GzHi,FMath::Min(CzHi,BlockMaxZ)+2);
|
||||
for(int32 Z=SampleMinZ;Z<=SampleMaxZ;++Z)
|
||||
for(int32 Y=SampleMinY;Y<=SampleMaxY;++Y)
|
||||
for(int32 X=SampleMinX;X<=SampleMaxX;++X)
|
||||
RequiredGridSamples[((Z+1)*MDim+(Y+1))*MDim+(X+1)]=1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool bCapturePreEditGrid = OutPreEditDensityGrid != nullptr
|
||||
&& !bUseSharedDensityGrid
|
||||
&& Step == 1 && CellsPerAxis == CHUNK_SIZE
|
||||
@@ -1046,6 +1144,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
auto SampleGridPoint = [&](int32 gx, int32 gy, int32 gz)
|
||||
{
|
||||
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
|
||||
if(bSkipProofBlockGrid&&RequiredGridSamples[LocalIndex]==0)return;
|
||||
if (bUseSharedDensityGrid)
|
||||
{
|
||||
const int32 SharedX = SharedDeltaX + gx;
|
||||
@@ -1092,9 +1191,9 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
const FStandaloneShapeWorld* ActiveShapeWorld=ShapeWorldContext;
|
||||
const bool bUseShapeWorldZBatch=ActiveShapeWorld!=nullptr
|
||||
&&ActiveShapeWorld->IsAnalyticOnly()
|
||||
&&!bUseSharedDensityGrid&&!bCapturePreEditGrid;
|
||||
&&!bUseSharedDensityGrid&&!bCapturePreEditGrid&&!bSkipProofBlockGrid;
|
||||
const bool bUseShapeWorldXBatch=ActiveShapeWorld!=nullptr
|
||||
&&!bUseSharedDensityGrid&&!bCapturePreEditGrid;
|
||||
&&!bUseSharedDensityGrid&&!bCapturePreEditGrid&&!bSkipProofBlockGrid;
|
||||
if(bUseShapeWorldZBatch&&!bUseVerticalCacheTraversal)
|
||||
{
|
||||
for(const std::vector<int32>& YGroup:YGroups)
|
||||
@@ -1242,6 +1341,34 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
}
|
||||
if (OutDebugDensitySamples)
|
||||
{
|
||||
// Hash/debug runs need the complete reference density lattice. Let MC exercise the
|
||||
// sparse block path, then evaluate omitted samples before exporting that lattice.
|
||||
if(bSkipProofBlockGrid)
|
||||
{
|
||||
for(int32 gz=GzLo;gz<=GzHi;++gz)
|
||||
for(int32 gy=-1;gy<=GridDim;++gy)
|
||||
for(int32 gx=-1;gx<=GridDim;++gx)
|
||||
{
|
||||
const int32 LocalIndex=((gz+1)*MDim+(gy+1))*MDim+(gx+1);
|
||||
if(RequiredGridSamples[LocalIndex]!=0)continue;
|
||||
RequiredGridSamples[LocalIndex]=1;
|
||||
const int32 ReachBlockX=FMath::Clamp(gx,0,CellsPerAxis-1)
|
||||
/VoxelGenLOD::TileReachBlockCells;
|
||||
const int32 ReachBlockY=FMath::Clamp(gy,0,CellsPerAxis-1)
|
||||
/VoxelGenLOD::TileReachBlockCells;
|
||||
const int32 ReachBlockZ=FMath::Clamp(gz,0,CellsPerAxis-1)
|
||||
/VoxelGenLOD::TileReachBlockCells;
|
||||
VoxelGenLOD::TileReachBlockIndex=
|
||||
((ReachBlockZ*ReachBlocksPerAxis)+ReachBlockY)*ReachBlocksPerAxis+ReachBlockX;
|
||||
const int32 SampleX=OriginVoxels.X+gx*Step;
|
||||
const int32 SampleY=OriginVoxels.Y+gy*Step;
|
||||
const int32 SampleZ=OriginVoxels.Z+gz*Step;
|
||||
DensityGrid[LocalIndex]=ShapeWorldContext
|
||||
?ShapeWorldContext->DensityAtLattice(SampleX,SampleY,SampleZ)
|
||||
:Generator->GetDensityAtLattice(SampleX,SampleY,SampleZ);
|
||||
if(OutDensitySampleCount)++(*OutDensitySampleCount);
|
||||
}
|
||||
}
|
||||
const size_t SampleCount = static_cast<size_t>(GzHi - GzLo + 1)
|
||||
* static_cast<size_t>(MDim) * static_cast<size_t>(MDim);
|
||||
OutDebugDensitySamples->reserve(SampleCount);
|
||||
@@ -1259,7 +1386,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
FPlatformTime::Cycles64()-StageDensityGridStartCycles)
|
||||
*FPlatformTime::GetSecondsPerCycle64()*1000.0;
|
||||
|
||||
if (bUseProofBlocks)
|
||||
if (bUseProofBlocks&&(!bSkipProofBlockGrid||OutDebugDensitySamples!=nullptr))
|
||||
{
|
||||
// Finalize every block candidate against the exact density samples that the marching-
|
||||
// cubes loop will consume. This is intentionally redundant with ClassifyTile: it is a
|
||||
@@ -1768,6 +1895,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
const int32 GroundSurfaceTriangleCount=GroundTris.Num()/3;
|
||||
const int32 CapSurfaceTriangleCount=CapTris.Num()/3;
|
||||
TArray<uint8> GroundSkirtFaceMasks,CapSkirtFaceMasks;
|
||||
const uint64 SkirtStageStartCycles=OutStageTimings?FPlatformTime::Cycles64():0;
|
||||
if (bGenerateSkirts && (GroundTris.Num() + CapTris.Num()) > 0)
|
||||
{
|
||||
const float ExtentCm = (float)(CellsPerAxis * Step) * GeometryVoxelSize;
|
||||
@@ -2007,6 +2135,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
EmitSkirts(GroundTris,GroundSkirtFaceMasks);
|
||||
EmitSkirts(CapTris,CapSkirtFaceMasks);
|
||||
}
|
||||
if(OutStageTimings)
|
||||
OutStageTimings->SkirtMilliseconds=static_cast<double>(
|
||||
FPlatformTime::Cycles64()-SkirtStageStartCycles)
|
||||
*FPlatformTime::GetSecondsPerCycle64()*1000.0;
|
||||
|
||||
// F17 — concatène sol PUIS sky-cap : un run d'indices contigu par polygroup (voir
|
||||
// FVoxelMeshData::NumCeilingTriangles). Mêmes triangles qu'avant, seul l'ordre change.
|
||||
@@ -2059,6 +2191,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
AuditInterpolationCycles,AuditInterpolationSamples,SurfaceCellCalls);
|
||||
OutStageTimings->VertexDedupStreamMilliseconds=ScaleSampledCycles(
|
||||
AuditDedupStreamCycles,AuditDedupStreamSamples,SurfaceCellCalls);
|
||||
OutStageTimings->SkirtMilliseconds=std::max(0.0,
|
||||
OutStageTimings->SkirtMilliseconds);
|
||||
const double TotalMilliseconds=static_cast<double>(
|
||||
FPlatformTime::Cycles64()-StageMesherStartCycles)*CycleToMs;
|
||||
OutStageTimings->OtherMilliseconds=FMath::Max(0.0,TotalMilliseconds
|
||||
@@ -2066,7 +2200,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
-OutStageTimings->CellClassificationMilliseconds
|
||||
-OutStageTimings->GradientNormalsMilliseconds
|
||||
-OutStageTimings->EdgeInterpolationMilliseconds
|
||||
-OutStageTimings->VertexDedupStreamMilliseconds);
|
||||
-OutStageTimings->VertexDedupStreamMilliseconds
|
||||
-OutStageTimings->SkirtMilliseconds);
|
||||
}
|
||||
|
||||
return MeshData;
|
||||
|
||||
@@ -50,12 +50,24 @@ struct FVoxelMarchingCubesStageTimings
|
||||
double GradientNormalsMilliseconds = 0.0;
|
||||
double EdgeInterpolationMilliseconds = 0.0;
|
||||
double VertexDedupStreamMilliseconds = 0.0;
|
||||
double SkirtMilliseconds = 0.0;
|
||||
double OtherMilliseconds = 0.0;
|
||||
uint64 GradientFallbackDensityCalls = 0;
|
||||
uint64 EdgeMidpointDensityCalls = 0;
|
||||
uint64 GradientMemoHits = 0;
|
||||
};
|
||||
|
||||
// Opt-in soundness audit for density blocks which the Core mesher would skip.
|
||||
// Audit de justesse facultatif pour les blocs de densite que Core sauterait.
|
||||
struct FVoxelUniformBlockProofAuditStats
|
||||
{
|
||||
uint64 CheckedBlocks = 0;
|
||||
uint64 CheckedSamples = 0;
|
||||
uint64 FallbackBlocks = 0;
|
||||
uint64 UnknownBlocks = 0;
|
||||
uint64 Violations = 0;
|
||||
};
|
||||
|
||||
// Stable plan data copied onto one worker-owned mesher for hybrid LOD0 generation.
|
||||
// Donnees stables du plan copiees dans un mailleur de worker pour le mode hybride LOD0.
|
||||
struct FVoxelHybridSharpFeatureBox
|
||||
@@ -107,7 +119,9 @@ public:
|
||||
FVoxelSharedDensityGrid* OutPreEditDensityGrid = nullptr,
|
||||
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr,
|
||||
std::vector<float>* OutDebugDensitySamples = nullptr,
|
||||
FVoxelMarchingCubesStageTimings* OutStageTimings = nullptr);
|
||||
FVoxelMarchingCubesStageTimings* OutStageTimings = nullptr,
|
||||
FVoxelUniformBlockProofAuditStats* OutBlockProofAudit = nullptr,
|
||||
bool bForceFullDensityCapture = false);
|
||||
|
||||
//=========================================================================
|
||||
// SERVICES (injectés par AVoxelWorld)
|
||||
|
||||