af5f2103b3
The bisect settled it. The difference survives every stage removal down to
"corridors + carve ONLY", which is character-for-character transcribed code, so
it is not in anything the decomposition added.
Cause, read out of the engine rather than assumed (VCToolChain.cs):
case FPSemanticsMode.Default: // Default is imprecise FP semantics.
case FPSemanticsMode.Imprecise: Arguments.Add("/fp:fast"); break;
with UBT's own doc: "the compiler is allowed to transform math expressions in
ways that might result in differently rounded results". Identical source in two
translation units may reassociate differently, worth ~1 ULP. It shows up on
exactly the ~2% of samples inside the SDF blend shell, where Blend - Sdf
catastrophically cancels; outside it Carve is exactly 0 or 1 and both agree.
So MazeEquivalence now grades what it can actually assert:
- hard fail : any isosurface crossing (geometry moves)
- info : differences at ULP scale (the unavoidable floor)
- warn : anything larger, which IS port drift, and runs the bisect
A test that warns on every port would get ignored by the port that matters.
Recorded in OPSTACK-PLAN 2.6, and as AUDIT C9 for the part that outlives this
refactor: ARCHITECTURE 9.1's "every peer regenerates identically" holds only
between bit-identical binaries under /fp:fast. Fine for one build on one
platform; a real desync source for a Linux server plus Windows clients both
regenerating authoritative geometry. The FPSemantics::Precise knob exists but
must not be turned speculatively -- it blocks the vectorisation T2.a was chasing,
on the hot loop, for an unmeasured cost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
731 lines
41 KiB
Markdown
731 lines
41 KiB
Markdown
# VoxelForge — Independent Audit, 2026-07-26
|
||
|
||
*Scope: every file under `Source/VoxelForge/` (~16,400 lines, 30 files) + all five design docs, read in
|
||
full. Axes requested: **correctness & threading**, **perf & algorithm choice**, **architecture &
|
||
maintainability**. Bar: **fitness for purpose for VoxelM** — not marketplace-grade generality.*
|
||
|
||
> ⚠️ This file is currently **untracked** — the strict-whitelist `.gitignore` only keeps `Source/`,
|
||
> `CODEMAP.md` and the `.uplugin`. See P1.
|
||
|
||
---
|
||
|
||
## 0. Verdict
|
||
|
||
**This is good work.** Well above the median for a solo UE plugin: the perf reasoning is real and
|
||
measured (Insights traces cited with numbers), the invariants are named and defended, the caches are
|
||
box-validated rather than key-validated for the right reasons, and almost every expensive path is
|
||
budgeted. The clipmap + trivial-tile-reject + worker-built streams architecture is the correct shape
|
||
for this game. Nothing in here reads as cargo-cult.
|
||
|
||
**And it is not in a state where you can answer "does it work?"** Three separate things have quietly
|
||
gone wrong at the process layer, and they compound:
|
||
|
||
1. **~3 weeks of work sits in one commit called `tmp`** (3,903 insertions / 1,029 deletions across 20
|
||
files). No bisect, no per-feature revert, no "when did seed X stop matching".
|
||
2. **The design record isn't in version control.** `ARCHITECTURE.md`, `fable-idea.md`,
|
||
`REVIEW_FINDINGS.md`, `CLAUDE.md` are all excluded by `.gitignore`. Only `CODEMAP.md` survives.
|
||
3. ~~A stack of "CODE-COMPLETE, PENDING BUILD"...~~ **CORRECTED 2026-07-26 (Jahni):** everything has
|
||
been built and verified working. The *documents* are stale, not the code — `fable-idea.md` and
|
||
`ARCHITECTURE.md` still carry "PENDING BUILD" markers dated 07-04/-06/-08 that were resolved and
|
||
never ticked. That's a doc-hygiene item, not a project-state risk. The uncommitted tree is
|
||
deliberate: the current 3D work is experimental (see §7).
|
||
|
||
For a codebase whose central promise is **determinism**, having no per-change history is the sharpest
|
||
internal contradiction in the project. That's the headline.
|
||
|
||
The code findings below are secondary to that, but there are real ones — including one latent bug
|
||
that will silently flatten your terrain the day you pass a large seed.
|
||
|
||
---
|
||
|
||
## 1. Correctness & threading
|
||
|
||
### C1 — Large seeds destroy the noise field ⚠️ **highest-severity latent bug**
|
||
|
||
`VoxelGenerator.cpp` uses `const float SeedF = (float)Seed;` and then adds `SeedF * k` (k up to
|
||
**97.7**) directly into Perlin coordinates — ~40 call sites, e.g.:
|
||
|
||
```cpp
|
||
// VoxelGenerator.cpp:1390 (Scallop)
|
||
CellularNoise3D(FVector(WorldX * SF + SeedF * 83.1f, ..., EffectiveZ * SF + SeedF * 97.7f));
|
||
```
|
||
|
||
Float has a 24-bit mantissa. At magnitude `V` the ULP is `V · 2⁻²³`:
|
||
|
||
| `Seed` | `SeedF · 97.7` | ULP there | Effect on a coordinate stepping by ~0.02/voxel |
|
||
|---|---|---|---|
|
||
| 1,000 | 9.8e4 | 0.012 | fine |
|
||
| 100,000 | 9.8e6 | 1.2 | noise lattice-snaps; detail gone |
|
||
| 10,000,000 | 9.8e8 | **117** | the coordinate term is *completely* absorbed → that noise field becomes **constant** |
|
||
|
||
`ChangeSeed(int32)` is `BlueprintCallable`. The moment game code calls it with anything like
|
||
`FMath::Rand()` (up to 2³¹), whole noise terms collapse to a constant and terrain goes flat or
|
||
degenerate. It works today only because your seeds are small.
|
||
|
||
`fable-idea.md` **flagged this exact issue** in T2.a ("seed offsets like `SeedF * 7.3f` can reach 1e8+
|
||
where float precision is ~64 units — hash the seed into a bounded offset range (e.g. [0, 16k])") and it
|
||
was never done.
|
||
|
||
**Fix (one line, one world re-tune):**
|
||
```cpp
|
||
const float SeedF = (float)(VoxelHash::Mix((uint32)Seed) & 0x3FFF); // bounded [0, 16383]
|
||
```
|
||
Applied consistently at all `SeedF` definitions (`:682`, `:1756`, `:2066`, `:2208`, `:2275`, `:2687`).
|
||
**Verify:** set `Seed = 1234567` in the data asset and look at the terrain. If it's flat, this is live.
|
||
|
||
---
|
||
|
||
### C2 — Per-chunk parameter caches have no layout key (stale after live-edit) ⚠️ **real, reproducible**
|
||
|
||
`VoxelGenerator.cpp:503-524`:
|
||
|
||
```cpp
|
||
thread_local FIntVector CP_Chunk(INT32_MAX, INT32_MAX, INT32_MAX);
|
||
thread_local ECaveGeneratorType CP_GenType;
|
||
thread_local FStrateGenerationParams CP_Tunnel; // + CP_Slab, CP_Maze, CP_Surface, CP_Vert,
|
||
// CP_Float, CP_Dist, CP_BiomeCtx, ...
|
||
if (ChunkCoord != CP_Chunk) { /* refetch everything */ }
|
||
```
|
||
|
||
The key is **`ChunkCoord` alone** — no seed, no layout version. After `RebuildStrates()` or an
|
||
`OnObjectModifiedInEditor` live edit, `StrateManager::Initialize` rebuilds the layout and bumps
|
||
`PassagesVersion`, but a pooled worker thread whose `CP_Chunk` still equals the chunk it's now asked to
|
||
regenerate **skips the refetch and generates with the old params**. Since `RegenerateAllChunks` reloads
|
||
the *same* tile coords, often on the *same* workers, this is likely rather than exotic.
|
||
|
||
Same pattern, same gap:
|
||
- `OC_Chunk` — the `GetSurfaceHeightAt` oracle cache (`:2420`)
|
||
- `BM_Chunk` — `GetBiomeMaterialAt` (`:3045`)
|
||
- `FChunkBiomeCache::Contains` — keyed on `(box, ChunkZ, Seed)` but **not** the biome context, so
|
||
editing a strate's `Biomes[]` leaves stale cells
|
||
|
||
Note the asymmetry: the SDF cache (`CachedSeed`) and the surface-column cache (`GSurfColCache`, keyed on
|
||
Seed) **are** seed-guarded, and the strate-index memo **is** version-guarded (`SI_Version`). So
|
||
`ChangeSeed` mostly survives; **live-edit is where this bites**. Symptom: "I tweaked the strate asset,
|
||
regenerated, and one patch kept the old shape."
|
||
|
||
**Fix:** the getter already exists and is already used elsewhere —
|
||
```cpp
|
||
const uint32 LV = StrateManager->GetLayoutVersion();
|
||
if (ChunkCoord != CP_Chunk || LV != CP_Version) { CP_Version = LV; /* refetch */ }
|
||
```
|
||
Three sites. Cheap, and it closes a whole class of "why didn't my edit apply".
|
||
|
||
---
|
||
|
||
### C3 — `FMath::PerlinNoise2D` survived the SIMD migration, on the per-voxel hot path
|
||
|
||
`VoxelCaveMorphology.cpp:799-800`, inside `EvaluateSDFCached`'s **per-room, per-voxel** loop:
|
||
|
||
```cpp
|
||
float N = FMath::PerlinNoise2D(FVector2D(Pos.X * RF + SF, Pos.Y * RF + SF * 1.7f)) * 0.65f
|
||
+ FMath::PerlinNoise2D(FVector2D(Pos.X * RF * 2.3f + SF * 3.1f, ...)) * 0.35f;
|
||
```
|
||
|
||
Two **double-precision, permutation-table** UE Perlin calls per voxel per in-range room. The entire T2.a
|
||
pass existed to remove exactly this from the density path, and `ARCHITECTURE §8.10` states outright:
|
||
*"Don't reintroduce `FMath::PerlinNoise3D` on the density path."* The 2D sibling slipped through the net.
|
||
|
||
Gated by `FloorReliefStrength > 0` (default 0), so it's **dormant today** — but the moment you author a
|
||
room floor with relief (the tooltip recommends 5-10 for "clear hills and basins — interesting to walk
|
||
across"), every TunnelNetwork voxel near a room pays up to ~6 double-precision Perlin evals. That is
|
||
plausibly larger than everything T2.a saved.
|
||
|
||
**Fix:** `VoxelNoise::Perlin3D(x, y, 0.0f)` (or add a `VoxelNoise::Perlin2D`). One-time visual re-tune of
|
||
the floor relief; nothing else moves.
|
||
|
||
---
|
||
|
||
### C4 — Streaming stops if the pawn is at exactly the origin, or before a pawn exists
|
||
|
||
`VoxelWorld.cpp:453`:
|
||
```cpp
|
||
FVector PlayerLastPos = GetPlayerPosition(); // returns ZeroVector when no PC/pawn
|
||
if ((PlayerLastPos != FVector::ZeroVector)) { UpdateChunksAroundPosition(...); ... }
|
||
```
|
||
|
||
Two problems with using a value as a validity sentinel:
|
||
- **(0,0,0) is a real, designed location** — it's the (0,0) descent spine, the landing column in every
|
||
strate. A pawn resting exactly there freezes chunk streaming, atmosphere, decorations and the density
|
||
volume for that frame.
|
||
- **No pawn ⇒ permanent silent stall.** Early `BeginPlay`, a cinematic without a possessed pawn, or a
|
||
future headless/dedicated path (§9 explicitly plans for this) gets a world that never generates and
|
||
logs nothing.
|
||
|
||
**Fix:** `bool bHasPlayer` out-param on `GetPlayerPosition`, or return `TOptional<FVector>`.
|
||
|
||
---
|
||
|
||
### C5 — Shutdown spin-waits time out and then destroy objects workers are still reading
|
||
|
||
`AVoxelWorld::EndPlay` (`:319-330`) waits 3 s on `ActiveTaskCount`, logs a warning, then **falls through
|
||
to teardown** while a worker may still be inside `GenerateTileResult` holding `Generator`/`Mesher`/
|
||
`StrateManager`. `UVoxelContentManager::NotifyShutdown` does the same (3 s, then `break`), and its task
|
||
lambda captures raw `this` (a UObject) — so after the timeout, `bShuttingDown.load()` and
|
||
`DecoResults.Enqueue()` touch freed memory.
|
||
|
||
`UVoxelDensityVolume` gets this right (`FillThread->Kill(true)` — unbounded, blocks until `Run()`
|
||
returns). The other two should either be unbounded too, or hold a shared-ownership handle the workers
|
||
keep alive.
|
||
|
||
Low probability. High confusion cost when it does fire — it presents as a random PIE-exit crash.
|
||
|
||
---
|
||
|
||
### C6 — The diff layer is an unbounded append-only list of brush strokes
|
||
|
||
`UVoxelDiffLayer` stores each `FVoxelModification` in **every chunk its AABB overlaps**, and
|
||
`EvaluateMods` iterates **all** of a chunk's mods per voxel:
|
||
|
||
```cpp
|
||
for (const FVoxelModification& Mod : Mods) { /* sphere/box/capsule falloff */ }
|
||
```
|
||
|
||
- `MaxModifications` defaults to **0 = unlimited**.
|
||
- No compaction, no rasterization to a sparse voxel grid, no spatial subdivision within a chunk.
|
||
- No serialization (`fable-idea` F4 save/load is unbuilt), so the entire player footprint dies with the
|
||
session.
|
||
|
||
Cost of re-meshing a chunk grows **linearly in the number of strokes ever made in it**, forever. For a
|
||
game whose stated core verb is digging, this is the most significant architectural gap in the plugin.
|
||
It's invisible today because nobody has dug 500 times in one chunk.
|
||
|
||
The threading is correct (`FRWLock` + the version-stamped snapshot cache is a genuinely nice design —
|
||
~27 lock ops per tile instead of ~86k). It's the *data structure* that won't scale, not the concurrency.
|
||
|
||
**When it matters:** the moment mining becomes a real loop. The natural answer is a per-chunk sparse
|
||
`uint8`/`int8` delta grid that strokes rasterize into (O(1) per voxel, bounded memory, trivially
|
||
serializable, and it's the same representation MP §9.5's "compacted diff snapshot" wants).
|
||
|
||
---
|
||
|
||
### C7 — The density volume runs by default for a feature that's switched off
|
||
|
||
`bEnableDensityVolume = true` (`VoxelSettings.h:268`). What that buys, per frame while streaming:
|
||
|
||
- a dedicated OS thread (`FVoxelDensityFillRunnable`),
|
||
- a `CHUNK_SIZE³` quantize + 32 KB queue payload per capture-eligible level-0 tile,
|
||
- a `TMap<FIntVector, TArray<uint8>>` capture cache,
|
||
- and the big one — `UploadDirtyTextures()` copies **the whole `Res³` array** and issues a full
|
||
`RHIUpdateTexture3D` per dirty level. `BlitCaptureToWindow` sets `bGPUDirty = true` on *every* tile
|
||
ingest, so level 0 is dirty essentially every frame a tile lands.
|
||
|
||
At the default `Res = 128` that's **2 MB CPU copy + 2 MB GPU upload per level per frame** during
|
||
streaming; at 192 it's ~7 MB. The code itself flags it: *"Sub-box upload (with toroidal wrap-splitting)
|
||
is a later optimisation."*
|
||
|
||
**CONFIRMED 2026-07-26 (Jahni): the lighting system is deprecated for now.** So this is not
|
||
speculative — the volume, the fill thread, the capture path and the per-frame texture upload are all
|
||
running to feed a consumer that no longer exists.
|
||
|
||
**Action:** set `bEnableDensityVolume = false`. Straight cost deletion, no behavioural change. If the
|
||
volume is revived later, the sub-box upload (with toroidal wrap-splitting) must land first — a full
|
||
`Res³` re-upload per frame was never meant to be the steady state.
|
||
|
||
Also worth deleting while it's cold rather than leaving it half-live: `UpdateTerrainMaterialParams`
|
||
still recomputes and pushes `TVP0..TVP9` onto every terrain MID each Tick, and `ApplyMeshToTile` still
|
||
routes every material through `GetOrCreateTerrainMID` when the volume is enabled. With `bEnable...` off,
|
||
both self-disable — but the code paths stay as a trap for whoever reads them next.
|
||
|
||
---
|
||
|
||
### C8 — Documented invariant that isn't checked: `OriginRoomRadius` vs the COLLECT margin
|
||
|
||
`BuildChunkCache` guarantees window-invariance by collecting rooms within
|
||
`CollectMargin = 2·MaxTunnelLength + MaxInfluence`. The origin room is special-cased: it's also added when
|
||
`RoomReachesSearchBox(...)` succeeds even if `(0,0)` is *outside* the collect box. In that case
|
||
`NearestNeighbor[OriginIdx]` and the `OriginDowngraded` ranking are computed over a **different candidate
|
||
set** than a chunk near `(0,0)` computes → different tunnel decisions → a discontinuity ring at roughly
|
||
`CollectMargin` from origin.
|
||
|
||
**Cannot fire at defaults** (`OriginRoomRadius = 20`, `MaxTunnelLength = 200` ⇒ margin ≈ 434). It becomes
|
||
reachable only with a very large origin room *and* short tunnels. Flagging it because the tooltip invites
|
||
"30+ → massive starting cavern" and there's nothing stopping someone typing 500.
|
||
|
||
**Fix:** an `ensureMsgf(OriginRoomRadius < CollectMargin)` in `BuildChunkCache`, plus a line in
|
||
`ARCHITECTURE §8.4`.
|
||
|
||
---
|
||
|
||
### C9 — The build uses `/fp:fast`, and the MP plan assumes bit-reproducible terrain ⚠️ **added 2026-07-27, measured not assumed**
|
||
|
||
Found while chasing a 1-ULP difference between `GetMazeDensity` and its operator-stack port. The
|
||
difference survived a full bisect down to code that is character-for-character identical, which
|
||
pointed at the toolchain rather than the port. It is:
|
||
|
||
```csharp
|
||
// UnrealBuildTool/Platform/Windows/VCToolChain.cs
|
||
case FPSemanticsMode.Default: // Default is imprecise FP semantics.
|
||
case FPSemanticsMode.Imprecise: Arguments.Add("/fp:fast"); break;
|
||
```
|
||
|
||
and UBT's own doc for that mode: *"FP math isn't IEEE-754 compliant: the compiler is allowed to
|
||
transform math expressions in ways that might result in differently rounded results from what
|
||
IEEE-754 requires."* The plugin sets no `FPSemantics` override, so it gets the default.
|
||
|
||
**Two consequences, one benign and one not:**
|
||
|
||
**Benign — refactors cannot be bit-identical.** The same expression compiled into two translation
|
||
units may reassociate differently, worth ~1 ULP. So "the port reproduces the original exactly" is
|
||
not an achievable bar for the op-stack work, and `OPSTACK-PLAN §2.6`'s bar (recognisably the same
|
||
*place*, judged on a screenshot) was the right call for reasons beyond the ones it gave. The
|
||
acceptance criterion is now encoded in `VoxelForge.OpStack.MazeEquivalence`: **hard-fail on any
|
||
isosurface crossing, tolerate ULP-scale deltas, warn on anything larger.**
|
||
|
||
**Not benign — `ARCHITECTURE §9.1`'s multiplayer model rests on this.** The plan is "replicate the
|
||
seed + layout + diff, never the geometry; every peer regenerates identically." That guarantee is
|
||
only as strong as the floating-point reproducibility of the density path, and under `/fp:fast` it
|
||
holds **only between bit-identical binaries**. Same build, same platform: fine — `DensityPurity`
|
||
proves the field is pure across threads and query order. But a different compiler version, a
|
||
different optimisation level (Debug vs Shipping), or a different platform's toolchain may produce a
|
||
subtly different world from the same seed.
|
||
|
||
The docs already reject GPU density partly because *"cross-GPU float determinism is fatal for
|
||
replicate-the-seed"*. The same argument applies to the CPU across build variants, and had not been
|
||
stated.
|
||
|
||
**How much it matters depends on a design question that is Jahni's, not the audit's:**
|
||
|
||
- **If listen-server only, one binary per platform, and clients never regenerate authoritative
|
||
geometry** — this is a non-issue. Ship as is.
|
||
- **If a dedicated server on Linux and Windows clients both regenerate terrain and compare** — this
|
||
is a real desync source, and it will present as rare, unreproducible, geometry-only divergence:
|
||
approximately the worst bug class to diagnose.
|
||
|
||
**The knob, if it turns out to matter:** `ModuleRules.FPSemantics = FPSemanticsMode.Precise` in
|
||
`VoxelForge.Build.cs` restores IEEE semantics for this module only. **Do not do this speculatively** —
|
||
the density path is the plugin's hot loop, `/fp:precise` blocks exactly the vectorisation and
|
||
contraction that T2.a's SIMD noise work was chasing, and the cost is unmeasured. It is a decision to
|
||
take with a profile in hand and a confirmed cross-platform requirement, not a tidy-up.
|
||
|
||
---
|
||
|
||
### Threading — what's *right*, for the record
|
||
|
||
Worth stating plainly, because it's the part that's easy to get wrong and this doesn't:
|
||
|
||
- `ProcessQueue` is correctly `Mpsc`, with the failure mode documented at the declaration.
|
||
- The `FTaskGuard` RAII decrement covers every early-exit path in `LoadTile`.
|
||
- Epoch carrying is consistent across all four async paths (tile gen, deco march, volume fill, band re-gen).
|
||
- `DiffLayer`'s `FRWLock` + lock-free `bHasAnyMods` fast-reject + per-(chunk,version) snapshot is a genuinely
|
||
good piece of engineering, and the comment records the AV it fixed.
|
||
- `BuildCellSpawns` is correctly `static` — no implicit `this` on the worker.
|
||
- `VoxelGenLOD::OctaveBias` is `TGuardValue`-scoped per tile, so deco snapping and volume fills can't
|
||
inherit a stale bias.
|
||
|
||
---
|
||
|
||
## 2. Performance & algorithm choice
|
||
|
||
### Is Marching Cubes still the right call in 2026? — **Yes, for this game.**
|
||
|
||
- **Transvoxel** ([transvoxel.org](https://transvoxel.org/)) remains the canonical answer to LOD seams,
|
||
and `fable-idea` already schedules it correctly ("whenever LOD cracks become the loudest remaining
|
||
flaw"). Your skirts are a legitimate substitute: they cost extra verts and some overdraw on boundary
|
||
contour edges, but they're ~80 lines instead of a transition-cell rewrite, and they work for *any*
|
||
level delta rather than only ±1.
|
||
- **Surface Nets / Dual Contouring** would cut triangle count meaningfully and are faster per chunk, but
|
||
naive Surface Nets can't hold a sharp 90° feature and DC brings QEF solving + self-intersection
|
||
handling. You'd trade a rewrite for a benefit you aren't currently bottlenecked on. Correct to skip.
|
||
- **GPU meshing** — `fable-idea` rejects it for readback latency, CPU-side collision, and cross-GPU float
|
||
determinism. All three are still true, and the last one is fatal for a game whose MP model is
|
||
"replicate the seed, regenerate identically on every peer". Right call, right reasons.
|
||
|
||
**The verdict is: your mesher isn't the problem.** T1.a–f and T2.a–e genuinely took per-tile generation
|
||
into diminishing returns. The remaining cost lives elsewhere (C3, C7) and in the scaling cliffs below.
|
||
|
||
### Scaling cliffs nobody has hit yet
|
||
|
||
**`BuildChunkCache` is O(N²) in rooms, and N grows quadratically with the tunnel/spacing ratio.**
|
||
`PickNeighbor` is O(N) per room and the connection loop is O(N²), over the COLLECT box which spans
|
||
`2·MaxTunnelLength + MaxInfluence`. So room count `N ≈ ((2L + I)/S)²` and the graph build is
|
||
**O((L/S)⁴)**:
|
||
|
||
| `MaxTunnelLength` | `RoomSpacing` | ≈ rooms in COLLECT | pair iterations |
|
||
|---|---|---|---|
|
||
| 200 (default) | 80 (default) | ~40 | ~800 |
|
||
| 300 | 60 | ~140 | ~10,000 |
|
||
| 400 | 40 | ~500 | ~125,000 |
|
||
|
||
Per cache rebuild, per worker. Amortised over 32,768 voxels it's survivable at defaults; at row 3 it
|
||
isn't. Worth a comment on `MaxTunnelLength` and a soft clamp.
|
||
|
||
**`AnyPassageNearBox`** walks every passage per tile with no spatial index. Fine at 10-20 passages; grows
|
||
linearly with `TotalStrates × Connections`.
|
||
|
||
**`FindSlotIndexForChunkZ`** is a linear scan called extremely often — per lattice-Z in `ClassifyTile`,
|
||
per archetype param getter, on every `GetDensityAt` chunk change. The comment acknowledges it ("with
|
||
~10-20 strates this is fine"). It is. But it's the classic death-by-a-thousand-cuts profile entry.
|
||
|
||
**`GetDominantBiomeAt`** is fully uncached: it builds an `FBiomeContext` (heap-allocating a `TArray`) and
|
||
runs the warped Voronoi + up to 2 `ClassifyBiomeAtSite` (4 fBM evals). `AtmosphereManager::UpdateForPlayer`
|
||
calls it **every Tick**, unconditionally. Small, but it's a per-frame game-thread allocation for a value
|
||
that changes maybe once a minute.
|
||
|
||
### The external development you should know about: **RealtimeMeshComponent 6.0**
|
||
|
||
You're on **RMC 5.3.2**. [RMC 6.0.0 shipped June 2026](https://triaxis.games/realtime-mesh/) with:
|
||
|
||
- **Runtime Nanite** — "real cluster groups, full LOD DAG, per-page streaming. No cooked bake, no
|
||
`UStaticMesh` round-trip."
|
||
- **Lumen card generation** + **distance field support** (Pro tier, $49 one-time).
|
||
- UE 5.2–5.7 support (you're on 5.7).
|
||
|
||
This matters more to VoxelForge than to most plugins, because it potentially subsumes **three separate
|
||
hand-built systems**:
|
||
|
||
| Your system | What RMC 6.0 offers instead |
|
||
|---|---|
|
||
| Clipmap levels + `CoarseTileCells` + `LODOctaveDrop` + skirts | Nanite's own LOD DAG, no seams by construction |
|
||
| F18 sheet ring (`GenerateSheetMesh`, XY hole, band re-queue) | far-field falls out of Nanite streaming |
|
||
| Density volume + raymarched mini-sun shadows (currently gated off) | runtime Lumen cards + distance fields |
|
||
|
||
I am **not** saying "rewrite on Nanite" — runtime Nanite on procedurally-remeshed geometry is new, and
|
||
digging (constant re-meshing) is the adversarial case for cluster building. But this is the single
|
||
biggest change in the landscape since your architecture was designed, and it argues strongly for a
|
||
**timeboxed spike before building any more LOD machinery**. Measure: build time per tile, re-mesh cost
|
||
on carve, and whether skirts/sheets become unnecessary.
|
||
|
||
(For reference on the alternative path: [Voxel Plugin 2.0](https://docs.voxelplugin.com/) now targets
|
||
5.6/5.7 and its [VoxelCore](https://github.com/VoxelPlugin/VoxelCore) module is open-source — worth
|
||
knowing exists, not worth switching to. Your strate/archetype model is the whole game; theirs isn't.)
|
||
|
||
---
|
||
|
||
## 3. Architecture & maintainability
|
||
|
||
### The structural risk: ~30 hand-rolled `thread_local` caches
|
||
|
||
The density field is a *pure function by convention*, and that convention is enforced by roughly thirty
|
||
hand-written memo caches, each with a hand-written invalidation key:
|
||
|
||
`GSurfColCache` · `CP_Chunk`+9 param slots · `SDFCache`+`CachedSMinX/Y`+`CachedStrate`+`CachedSeed` ·
|
||
`SI_ChunkZ`/`SI_Version` · `DiffSlots[64]` · `CH_*` chasms · `BR_*` bridges · `RG_*` ridges · `SC_*` slab
|
||
columns · `MZ_*` maze edges · `VS_*` shafts · `FI_*` islands · `OC_*` oracle · `BM_*` biome material ·
|
||
`TC_BiomeCache` · `Slots[2]` · `SL_*` passage shortlist · the mesher's `VertexMap`/`DensityGrid`/
|
||
`SurfColMemo`/`VertexClasses`/`GroundTris`/`CapTris`/`SheetCols`/`GroundIdx`/`CapIdx`.
|
||
|
||
Every one is individually justified and individually documented. Collectively they mean **the
|
||
correctness of the entire world rests on ~30 key lists each enumerating every input**, audited by hand.
|
||
Two are provably incomplete (C2). This isn't a bug count — it's an *unbounded bug surface that grows
|
||
with every feature*, and the failure mode is the worst kind: a seam or hole that appears only under a
|
||
particular thread/edit interleaving.
|
||
|
||
The existing mitigation, `ValidateDeterminism`, is the right instinct but a narrow test: one axis (X
|
||
boundary), near the player, one alignment pair, **on the game thread**. It would not have caught C2
|
||
(worker-thread caches) or C8 (origin ring).
|
||
|
||
**What would actually help**, in rough order of value-per-hour:
|
||
|
||
1. A `VF_CACHE_KEY(...)` macro that bundles `(coord, seed, layoutVersion)` so a new cache can't forget
|
||
the standard fields.
|
||
2. A **debug-only "no-cache" mode** (`#if VF_VALIDATE_PURITY`) where every memo is bypassed, plus an
|
||
automation test that samples N random points in both modes and asserts bit-equality. That converts
|
||
*all* "bit-identical, I promise" claims — of which the docs make dozens — into a machine check.
|
||
3. Extend `ValidateDeterminism` to run its samples across **multiple worker threads** in shuffled order.
|
||
|
||
### No tests, no automation
|
||
|
||
Zero test files. UE ships an automation framework you're not using. Three tests would cover most of the
|
||
risk surface:
|
||
|
||
- **Density purity**: sample 10k points, shuffle the query order, re-sample, assert bit-equality.
|
||
- **`ClassifyTile` soundness**: for random tiles, if the verdict is `AllSolid`/`AllAir`, brute-force the
|
||
lattice and assert every sample agrees. A false verdict is a *hole* — this is the highest-consequence
|
||
function in the plugin and it's validated only by reasoning.
|
||
- **`DiffLayer` under contention**: N reader threads + a writer, assert no crash and monotonic version.
|
||
|
||
### Function size (the open item that stays open)
|
||
|
||
`REVIEW_FINDINGS.md` has listed these splits as open through three passes:
|
||
|
||
| Function | Lines | Note |
|
||
|---|---|---|
|
||
| `GetDensityWithParams` | ~1,080 | listed as "~600-1000 L" — it has grown since |
|
||
| `BuildChunkCache` | ~640 | |
|
||
| `UpdateChunksAroundPosition` | ~300 | + 5 nested lambdas, 3 of them capturing by reference |
|
||
| `BuildCellSpawns` | ~350 | contains a **175-line nested lambda** (`PlaceAtCrossing`) |
|
||
| `ClassifyTile` | ~200 | |
|
||
|
||
An item that survives three review passes isn't going to get done voluntarily. Either accept it
|
||
explicitly (write "we are keeping these monolithic, here's why" in `REVIEW_FINDINGS`) or bind it to the
|
||
next feature that touches each one. Leaving it as a permanent open checkbox just makes the checklist
|
||
lie.
|
||
|
||
### Smaller structural notes
|
||
|
||
- **`FStrateGenerationParams` is a 74-field god-struct**, and blend correctness depends on remembering
|
||
to add every new field to `VF_STRATE_PARAM_FIELDS`. The X-macro was the right fix; make drift a
|
||
*compile* error by putting `static_assert(sizeof(FStrateGenerationParams) == N, "add the new field to
|
||
VF_STRATE_PARAM_FIELDS")` right next to it.
|
||
- **Chunk-quantised param blending.** `GetGenerationParams` computes the Gradient/Interleaved blend
|
||
`Alpha` from `ChunkCoord`, so within a transition band the density field has a **step at every chunk
|
||
boundary**. MC samples corners from both sides, so it won't tear, but small ledges at chunk borders in
|
||
Gradient zones are expected behaviour, not a mystery, if you ever see them.
|
||
- **Dead knobs still shipping**: `ViewDistanceXY` (only `Up`/`Down` are read), `CeilingViewMultiplier` and
|
||
`CeilingBandChunks` (the wide-ceiling system was removed by the clipmap), `DensityVolumeMaxTasks`
|
||
(deprecated in its own tooltip). Each is a lever a designer will pull and get nothing.
|
||
- **`EVoxelPassageType` vs `EVoxelPassageStyle`** — two overlapping passage-shape enums, both live.
|
||
Already open in `REVIEW_FINDINGS`; still open.
|
||
- **Leftover tutorial scaffolding** in `VoxelWorld.h/.cpp`: `// CHUNK MANAGEMENT - YOU IMPLEMENT THESE`,
|
||
`// This one is tricky with Unreal's API, so I'll give you more help:`, and a 20-line `STEPS: 1. Try to
|
||
dequeue...` block inside the now-finished `ProcessPendingChunks`. Cosmetic, but it's the first thing a
|
||
reader hits.
|
||
- **`.uplugin` metadata**: `"CreatedBy": "You"`, `"Description": "...built from scratch to learn"`.
|
||
|
||
### The documentation, honestly
|
||
|
||
The prose is genuinely excellent — better than most commercial plugins. `ARCHITECTURE §8.10` in
|
||
particular is a real asset: it records *why* each optimisation exists and what regressing it costs.
|
||
|
||
Two problems:
|
||
|
||
1. **The numbers have drifted.** `CODEMAP.md` header says *"~8,300 lines of C++ across 25 files"*;
|
||
reality is **~16,400 across 30**. §3.5's line anchors are badly stale (`LoadChunk :445` → actually
|
||
~1336; `ApplyModification :691` → ~1816). Since `CLAUDE.md` already instructs "trust symbol names over
|
||
line numbers", I'd **delete the line column entirely** — it's a maintenance tax that buys nothing and
|
||
actively misleads.
|
||
2. **The comment density is optimised for one kind of reader.** Blocks like the F17 row in
|
||
`ARCHITECTURE §8.1` (a single table cell running ~40 lines, narrating v1 → v2 → v3 with rationale and
|
||
quoted feedback) are superb context for an AI collaborator picking the file up cold, and a genuine
|
||
liability for a human trying to find the code. You've made a real trade here; it's worth making it
|
||
knowingly. A `HISTORY.md` for the "we tried X, it failed because Y" narrative would let the source
|
||
comments shrink to *what the code does now*.
|
||
|
||
---
|
||
|
||
## 4. Process — what I'd fix first
|
||
|
||
### P1 — Track the design docs. Today. ⚠️
|
||
|
||
```
|
||
.gitignore: * → !/Source/ !VoxelForge.uplugin !CODEMAP.md
|
||
```
|
||
`git ls-files` confirms **`ARCHITECTURE.md`, `fable-idea.md`, `REVIEW_FINDINGS.md`, `CLAUDE.md` and this
|
||
audit are all untracked.** `fable-idea.md` even says so in its own header — *"the strict-whitelist
|
||
`.gitignore` will ignore this file. Add `!fable-idea.md` if you want it tracked"* — and it never was.
|
||
|
||
These documents *are* the project's memory. Losing them loses more than losing the code, because the code
|
||
can be re-read and the reasoning can't. Two lines:
|
||
```
|
||
!*.md
|
||
```
|
||
|
||
### P2 — Stop batching three weeks into `tmp`
|
||
|
||
| Commit | Date | Content |
|
||
|---|---|---|
|
||
| `69fa73e` **tmp** | 2026-07-26 | **+3,903 / −1,029 across 20 files** — F17, F18, F19 anchors, F20 ph.1+2, companions, landmark merge, strate cut, sheet ring |
|
||
| `cb61c8b` potential perf regression | 2026-07-04 | |
|
||
| `6875614` Fix Decoration Placement | 2026-06-26 | |
|
||
| `e6cd852` Another pass | 2026-06-23 | |
|
||
| `db558d9` j | 2026-06-16 | |
|
||
|
||
Seven commits in seven weeks, and the message quality (`j`, `tmp`, `Another pass`) means even the
|
||
existing history isn't navigable. You don't need discipline theatre — one commit per *feature*, with the
|
||
feature's name in the subject, would be enough to bisect "which change broke seed 42".
|
||
|
||
This matters disproportionately *here* because determinism is the product. When a world stops matching,
|
||
"which commit changed the field" is the only question, and right now it's unanswerable.
|
||
|
||
### P3 — Reconcile the "pending build" backlog before adding anything
|
||
|
||
`fable-idea.md` and `ARCHITECTURE.md` currently carry, unreconciled:
|
||
|
||
- F20 **phase 1** — "CODE-COMPLETE, PENDING BUILD (2026-07-08)"
|
||
- F20 **phase 2** — "CODE-COMPLETE, PENDING BUILD (2026-07-08)"
|
||
- §8.14 biome full-param redesign — "code-complete, pending build"
|
||
- §8.15 F6 vertex palette — "C++ code-complete, pending in-editor build + the master material graph"
|
||
- `REVIEW_FINDINGS` perf pass 2 + batch 3 — "CODE-COMPLETE, pending build"
|
||
|
||
That's three weeks of unverified code stacked on unverified code, on a system where the failure modes
|
||
(seams, holes, stale caches) are *visual* and only appear in-editor. **The honest statement of project
|
||
state is: unknown for everything after 2026-07-04.**
|
||
|
||
Suggested order before any new feature:
|
||
1. Build. Fix compile errors.
|
||
2. Run `ValidateDeterminism` at several locations (including far from origin).
|
||
3. `bEnableDensityVolume = false` → trace → compare (C7).
|
||
4. Set `Seed = 1234567` → look (C1).
|
||
5. Live-edit a strate asset, regenerate, look for patches that kept old params (C2).
|
||
6. Commit, per feature, with real messages.
|
||
|
||
---
|
||
|
||
## 5. Priority list
|
||
|
||
*Re-ordered 2026-07-26 around the live 3D work.*
|
||
|
||
| # | Item | Cost | Why |
|
||
|---|---|---|---|
|
||
| 1 | **§6.1** — `ClassifyTile` guard for 3D caves | ~30 lines | **blocks the current feature**; a false `AllSolid` is an invisible, collision-less cave |
|
||
| 2 | **P1** — track the `.md`s (`!*.md`) | 2 min | irreplaceable if lost; the docs *are* the design record |
|
||
| 3 | **C7** — `bEnableDensityVolume = false` | 1 min | confirmed dead weight — lighting is deprecated |
|
||
| 4 | **§6.6** — branch the experimental 3D work | 1 min | restores a known-good world to diff against |
|
||
| 5 | **C1** — bound `SeedF` | 1 line + re-tune | silent terrain collapse on large seeds |
|
||
| 6 | **C2** — add `GetLayoutVersion()` to 3 cache keys | ~10 lines | closes "my edit didn't apply" |
|
||
| 7 | **§6.2** — keep 3D caves *placed*, not *fielded* | a design call | decides whether T1.d keeps its 44 % |
|
||
| 8 | **C4** — explicit no-player flag | ~5 lines | origin-standing / no-pawn stall |
|
||
| 9 | **C5** — unbounded joins on shutdown | ~20 lines | random PIE-exit crashes |
|
||
| 10 | Automation tests (purity, `ClassifyTile`, `DiffLayer`) | ~a day | `ClassifyTile` is now the highest-consequence function in the plugin |
|
||
| 11 | **P2** — per-feature commits | ongoing | determinism without history is a contradiction |
|
||
| 12 | **C3** — `PerlinNoise2D` off the SDF hot path | ~5 lines | dormant, but violates a stated invariant |
|
||
| 13 | **RMC 6.0 spike** (runtime Nanite / Lumen cards) | timeboxed | may subsume clipmap LOD + sheets + density volume |
|
||
| 14 | **C6** — diff-layer delta grid | a design pass | the scaling wall for the game's core verb |
|
||
| 15 | Tick the stale "PENDING BUILD" markers in the docs | 20 min | they currently misreport project state |
|
||
| 16 | Drop line numbers from CODEMAP §3; fix the header count | 30 min | the map misstates its own scale (8.3k → 16.4k) |
|
||
| 17 | Delete dead settings + tutorial scaffolding | 30 min | knobs that do nothing mislead |
|
||
|
||
---
|
||
|
||
## 6. Going 3D — what the current architecture will fight you on
|
||
|
||
*Added 2026-07-26 after Jahni confirmed the live direction: 3D density features — overhangs, caves
|
||
inside mountains, volumetric generation inside `SurfaceWorld`. This is the section that matters most
|
||
right now; everything above is maintenance by comparison.*
|
||
|
||
### 6.1 `ClassifyTile` will delete your caves. This is the one to get right first. ⚠️
|
||
|
||
T1.d proves a tile `AllSolid`/`AllAir` and **skips `GenerateMesh` entirely**. For `SurfaceWorld` the
|
||
proof is (`VoxelGenerator.cpp`, `TestColumn`):
|
||
|
||
```cpp
|
||
if (Z >= TerrainZ && Z <= CeilSurf) { bCanSolid = false; } // air side
|
||
else { bCanAir = false; } // below terrain, or in the cap
|
||
```
|
||
|
||
So **any point below `TerrainZ` is treated as provably solid.** A tile entirely inside a mountain
|
||
returns `AllSolid`, emits no mesh, and never calls the density function. Carve a cave in there and:
|
||
|
||
- the cave's walls are **never meshed** — you get an unrendered void,
|
||
- there is **no collision** (level-0 collision is per-section, and there are no sections),
|
||
- and it is invisible until you enter it from a passage, at which point you fall through nothing.
|
||
|
||
The code comment states the rule exactly: *"A false `Mixed` only costs CPU; the code must NEVER emit a
|
||
false `AllSolid`/`AllAir` (that's a hole)."* This is the same failure that got T1.d **v1 reverted on
|
||
2026-06-26** — a global analytic ceiling bound that wasn't conservative.
|
||
|
||
The existing guards are the template — each kills one hypothesis, cheaply, per tile:
|
||
|
||
| Guard | Kills | Mechanism |
|
||
|---|---|---|
|
||
| `AnyPassageNearBox` | `AllSolid` | sphere-vs-AABB over the passage list |
|
||
| origin spine | `AllSolid` | circle-vs-box in XY |
|
||
| chasm disturbances | `AllSolid` | `ChasmDensity > 0` on the strate |
|
||
| bridges / ridges | `AllAir` | density > 0 on the strate |
|
||
| `HasAnyModInChunkRange` | both | one key walk of the diff map |
|
||
| F20 overhang | both, in a band | `(TerrainZ, TerrainZ + OverhangMargin]` ⇒ `Mixed`, upward only |
|
||
|
||
**Every new 3D feature needs one of these before it ships.** For hash-placed cave systems that means a
|
||
per-tile shortlist exactly like `EvaluateModifierSDF`'s passage shortlist: collect the feature bounds
|
||
that reach this tile once, and if the list is non-empty, `bCanSolid = false`. `fable-idea`'s own F20
|
||
phase-3 GOTCHA already says this for spikes/holes — it applies verbatim to caves.
|
||
|
||
> **Test that catches it:** generate a mountain with caves, then stand outside and fly the camera through
|
||
> the mountain volume. Any cave that renders from inside a passage but shows nothing when approached
|
||
> through rock is a false `AllSolid`.
|
||
|
||
### 6.2 The T1.d economics invert as caves get more volumetric
|
||
|
||
The 2026-07-05 trace justified T1.d with: **84 % of `GenerateMesh` calls produced empty tiles**
|
||
(83,925 gens vs 13,227 meshes, ~500 s of 617 s worker CPU wasted), and shipping it cut worker CPU 44 %.
|
||
|
||
The bulk of those `AllSolid` tiles **is deep rock below `TerrainZ`** — precisely the volume "caves inside
|
||
mountains" occupies. Every tile a cave touches must now generate. So there's a direct, quantifiable
|
||
tension:
|
||
|
||
- **Bounded caves** (hash-placed systems with bounding volumes, like rooms/tunnels already are) wake only
|
||
the tiles they actually intersect. The shortlist proves the rest of the mountain still solid. T1.d keeps
|
||
most of its win.
|
||
- **A global 3D noise field** (`if (noise3D(x,y,z) > t) carve`) is unprovable *everywhere* below terrain →
|
||
every deep tile becomes `Mixed` → you hand back a large slice of that 44 %, plus you now mesh and cook
|
||
collision for tiles that are 99 % solid rock.
|
||
|
||
**Recommendation: make 3D caves placed, not fielded.** Same discipline as `FCachedRoom`/`FCachedTunnel` —
|
||
they exist at hash-determined locations with known bounds. That keeps the classifier effective, makes the
|
||
shortlist guard trivial to write, and gives you authored control over cave density per biome. It's also
|
||
the only version that stays cheap at the render-distance ring.
|
||
|
||
### 6.3 Do not put Z-dependent data in `FSurfaceColumn`
|
||
|
||
`FSurfaceColumnBox` is keyed `(XY box, StrateKey, Seed)` with **no `ChunkZ`**, held as a 6-box LRU, and
|
||
deliberately **shared down the entire vertical chunk stack** — that's the whole T1.a win (the heightfield
|
||
was being recomputed once per altitude). `ARCHITECTURE §8.10` states it: *"ZERO Z dependence... Don't
|
||
re-introduce a ChunkZ key."*
|
||
|
||
So the rule for every new 3D feature is:
|
||
|
||
- **XY-pure data** (a per-column gate, amplitude, direction, a cave-system ID) → cache on `FSurfaceColumn`.
|
||
- **Z-dependent evaluation** → do it per-voxel in `SurfaceDensityFromColumn`, reading the cached column.
|
||
|
||
Your F20 phase-2 overhang is the correct template and worth copying deliberately: `OverhangAmp` (strength ×
|
||
slope-gate) and the unit uphill `(DirX, DirY)` are resolved **once per column** in `ComputeSurfaceColumn`;
|
||
the actual warped-terrain union is per-voxel. Anything that violates this silently corrupts every chunk in
|
||
the vertical stack, and `ValidateDeterminism` — which samples along an **X** boundary — would not catch it.
|
||
|
||
### 6.4 Window invariance: pick the cheap pattern where you can
|
||
|
||
Two existing precedents, and the difference is worth being deliberate about:
|
||
|
||
- **`Maze`** — edge identity is a pure hash of `(lower node, axis)`. Two adjacent chunks *cannot* disagree.
|
||
No cache, no COLLECT region, no seam risk, zero invariant maintenance.
|
||
- **`BuildChunkCache`** — because tunnel existence depends on a *connectivity decision over a neighbourhood*,
|
||
it needs the two-region COLLECT/STORE split (§8.4), and that invariant is delicate enough to have its own
|
||
documentation section.
|
||
|
||
If your cave systems need connectivity (a cave network that must be traversable), you inherit the second
|
||
pattern and its COLLECT-margin discipline. If each cave is independently placed, use the first — a pure
|
||
`hash(cell, seed)` placement with per-voxel SDF evaluation and no cross-chunk decision at all.
|
||
|
||
**Pick the Maze pattern unless connectivity is a gameplay requirement.** The complexity delta is large and
|
||
it's all seam-risk.
|
||
|
||
### 6.5 Smaller things that will bite
|
||
|
||
- **Route new per-voxel volumetric noise through `VoxelGenLOD::Eff(N)`.** That's the T2.b contract: per-voxel
|
||
3D noise gets the LOD octave drop, XY-field noise deliberately does not. New 3D code that calls
|
||
`FractalNoise3D(..., 3)` directly silently opts out of far-tile savings.
|
||
- **LOD-gate the expensive 3D ops.** Your own note on F20 phase 2 flags it: *"KNOWN v1 LIMIT: applies at ALL
|
||
LODs (may alias far) — gate to fine tiles later."* With one op that's a wart; with several it's a far-field
|
||
cost multiplier. A `Step > N` early-out per volumetric op is a few lines and compounds.
|
||
- **F17 already handles cave roofs correctly** — the surface class is semantic, not geometric: down-facing but
|
||
*below* `TerrainZ` ⇒ ground/rock, not sky-cap. This was designed for exactly your current case and needs
|
||
nothing. Good call at the time.
|
||
- **The F18 sheet ring won't show caves** — sheets are two heightfields, so carved features don't exist there.
|
||
Already an accepted trade-off; just don't expect a cave mouth to be visible at render-distance.
|
||
- **`SurfaceDensityFromColumn` is now the hot path**, not the SDF morphology. The overhang block already
|
||
re-runs `SampleSurfaceStructuralZ` per lip voxel. Each new volumetric op that re-samples structural height
|
||
per voxel stacks on that. Budget it consciously — the per-column cache saved you ~33×, and per-voxel
|
||
structural re-evals spend it back.
|
||
|
||
### 6.6 Since it's experimental and uncommitted
|
||
|
||
You said the tree is uncommitted *because* the 3D work is experimental. That's a reasonable instinct pointed
|
||
at the wrong tool — experimental work is exactly what branches are for:
|
||
|
||
```
|
||
git switch -c 3d-density # experiment freely, commit noisily
|
||
git switch main # a known-good world is always one command away
|
||
```
|
||
|
||
The specific thing you lose right now: when a seed stops matching or a hole appears mid-experiment, there
|
||
is no known-good state to diff against, and the 3D features are the *highest*-risk changes in the project
|
||
for exactly that failure mode (§7.1). A branch costs nothing and gives you the bisect back.
|
||
|
||
---
|
||
|
||
## 7. What I'd tell someone joining this project
|
||
|
||
The generation core is strong and the perf work is honest and measured — the person who wrote this knows
|
||
what they're doing and has the traces to prove it. The weakness is **everything around verification**:
|
||
no history you can bisect, no tests, design docs outside version control, and a habit of stacking
|
||
features on top of unbuilt features. Nothing in the code list above is hard to fix; the process items
|
||
are the ones that decide whether the next three weeks are as productive as the last three.
|
||
|
||
*Sources consulted for the external comparison: [transvoxel.org](https://transvoxel.org/) ·
|
||
[triaxis.games/realtime-mesh](https://triaxis.games/realtime-mesh/) ·
|
||
[TriAxis-Games/RealtimeMeshComponent](https://github.com/TriAxis-Games/RealtimeMeshComponent) ·
|
||
[docs.voxelplugin.com](https://docs.voxelplugin.com/) ·
|
||
[VoxelPlugin/VoxelCore](https://github.com/VoxelPlugin/VoxelCore) ·
|
||
[UE 5.7 release notes](https://www.unrealengine.com/news/unreal-engine-5-7-is-now-available)*
|