Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa67bd31f5 | ||
|
|
b21a19d561 | ||
|
|
7770074503 | ||
|
|
046f2af495 | ||
|
|
303b7b9f24 | ||
|
|
1276fd11e3 | ||
|
|
ec2afe9c93 | ||
|
|
bd5e38594f | ||
|
|
87b0e5d8cd | ||
|
|
212814a75c | ||
|
|
dd9bfd3217 | ||
|
|
b90d63d0f7 | ||
|
|
e7586a1256 | ||
|
|
8440adc742 | ||
|
|
5eb9ee76ff | ||
|
|
b9c13c37c0 | ||
|
|
c0596c5f24 | ||
|
|
9d3c50ca95 | ||
|
|
d0f97bf79a | ||
|
|
6ddb834b1c | ||
|
|
d6e69420e8 | ||
|
|
111cf3d262 | ||
|
|
48fa028086 | ||
|
|
4afea7c802 | ||
|
|
53a1e69af7 | ||
|
|
4661a445be | ||
|
|
22a6b30494 | ||
|
|
a3b7d2015e | ||
|
|
6953254349 | ||
|
|
61633cb492 | ||
|
|
f67b519e75 | ||
|
|
2d0cec90e4 | ||
|
|
20c6d86f0e | ||
|
|
2c20f9eaea | ||
|
|
fcb16fd265 | ||
|
|
874c796c65 | ||
|
|
d5ce5d675d | ||
|
|
1cf25eff30 | ||
|
|
61ce0dee4b | ||
|
|
7661fe9d24 | ||
|
|
d696463c68 | ||
|
|
75210e56f3 | ||
|
|
075e58187e | ||
|
|
29ca99af7f | ||
|
|
a1dfc0422e | ||
|
|
61106be142 | ||
|
|
5aa285c3ef | ||
|
|
7d1808fdf2 | ||
|
|
7d92e148a9 | ||
|
|
20acf57678 | ||
|
|
94d50edb47 | ||
|
|
bb082f3ae9 | ||
|
|
052b6de354 | ||
|
|
38f6009200 | ||
|
|
2cb3344e1b | ||
|
|
89fd9a0021 | ||
|
|
2fea2710f7 | ||
|
|
6f2d33e0a9 | ||
|
|
666dfb811c | ||
|
|
c152708a67 | ||
|
|
f322b1c089 | ||
|
|
a96fd0b95b | ||
|
|
05b1523c54 | ||
|
|
7f64e2e0aa | ||
|
|
b6ac950073 | ||
|
|
10580650cb | ||
|
|
b0da995978 | ||
|
|
f2de8fe9c6 | ||
|
|
90421d0655 | ||
|
|
41716f00b8 | ||
|
|
b0a14262b1 | ||
|
|
0e730cdbba | ||
|
|
247682a8e0 | ||
|
|
782f195bdb | ||
|
|
b61ccff549 | ||
|
|
d4b4377d30 | ||
|
|
566d1f342a | ||
|
|
de14f0042d | ||
|
|
b3af0e589e | ||
|
|
08ceed200d | ||
|
|
3b37d5702e | ||
|
|
aeeedaa73d | ||
|
|
557f6aac30 | ||
|
|
631dd57eb4 | ||
|
|
2656d81ac9 | ||
|
|
16612acba4 | ||
|
|
d4fa89204d | ||
|
|
a40b0454b3 | ||
|
|
5602a281b9 | ||
|
|
9ffe296cd1 | ||
|
|
a3cb26b5c8 | ||
|
|
6ec4a8f856 | ||
|
|
ee5b05b357 | ||
|
|
97c0455939 | ||
|
|
7e3f926352 | ||
|
|
4f4e67c8ca | ||
|
|
8c068ee2cd | ||
|
|
5441c7da5c | ||
|
|
81b7d6d0ff | ||
|
|
3f300b912a | ||
|
|
05f62cff36 | ||
|
|
a443eb8546 | ||
|
|
8d29c09512 | ||
|
|
61b1c9c329 | ||
|
|
413983f2d3 | ||
|
|
8b87749771 | ||
|
|
35e3d149d2 | ||
|
|
bc0f9ee791 | ||
|
|
ca78352e43 | ||
|
|
d6555df5fa | ||
|
|
0a7492ffab | ||
|
|
f8a46ecf8d | ||
|
|
78641da0bb | ||
|
|
d6e021a80b | ||
|
|
43c11ccaa8 | ||
|
|
fd9f809c1c | ||
|
|
41a9265ebd | ||
|
|
3b4d6855fe | ||
|
|
41b001b652 | ||
|
|
97a1874498 | ||
|
|
6d2a068308 | ||
|
|
153798f436 | ||
|
|
3de7ce1420 | ||
|
|
1568f76ca2 | ||
|
|
bf22f0ffe0 | ||
|
|
ce6812dbeb | ||
|
|
0bcd67a016 | ||
|
|
116b57f01e | ||
|
|
0b8fbc5591 | ||
|
|
593dfac1d8 | ||
|
|
f83c4bd8c1 | ||
|
|
8117d118eb | ||
|
|
63fb71fe6a | ||
|
|
6a2d701eab | ||
|
|
20619e587d | ||
|
|
41fcef692a | ||
|
|
b6529719dd | ||
|
|
3bd0fd36df | ||
|
|
c4397a5fb7 | ||
|
|
e960b9c7e3 | ||
|
|
c43eebbf9a | ||
|
|
46f7f5f7c1 | ||
|
|
26fbf0c999 | ||
|
|
c91b63d7c2 | ||
|
|
26984d38d8 | ||
|
|
6fd8a88986 | ||
|
|
3f629f407a | ||
|
|
4753416d4a | ||
|
|
1de44af7cc | ||
|
|
b4ac9969f5 | ||
|
|
fb052716c6 | ||
|
|
de05e293aa | ||
|
|
6ee433efa5 | ||
|
|
a49725d365 | ||
|
|
85a803b873 | ||
|
|
7486c299d4 | ||
|
|
f36bc1f9b2 | ||
|
|
370c39e473 | ||
|
|
ea3f0d9a52 | ||
|
|
30253f0e89 | ||
|
|
d7af2cab97 | ||
|
|
88f3e44402 | ||
|
|
0dd45ca410 | ||
|
|
7af8d0e870 | ||
|
|
cc04c0bd4e | ||
|
|
a7b8d10de6 | ||
|
|
1c642fd182 | ||
|
|
76bc8bce97 | ||
|
|
d01af4606d | ||
|
|
01cb9d47f1 | ||
|
|
4fa3931176 | ||
|
|
0231072cc4 | ||
|
|
bf9384a1d5 | ||
|
|
8722293f6b | ||
|
|
10fd0b6e77 | ||
|
|
cda082fa10 | ||
|
|
cf9904e8b1 | ||
|
|
410788f7dd | ||
|
|
c7b832db4c | ||
|
|
8ef1a3b579 | ||
|
|
cfd81b5ce9 | ||
|
|
a668833ef5 | ||
|
|
0208e93606 | ||
|
|
1d824aaab2 | ||
|
|
ce4d8783e9 | ||
|
|
92607751b1 | ||
|
|
cf2b2add20 | ||
|
|
8879045627 | ||
|
|
39c306b524 | ||
|
|
481e7bbaee | ||
|
|
4c8d06fd8f | ||
|
|
7ece3ceafc | ||
|
|
7a972f513d | ||
|
|
6ccf3718ff | ||
|
|
c132243b30 | ||
|
|
11f0c1335b | ||
|
|
26b34765d5 | ||
|
|
5489b5e311 | ||
|
|
22186473f9 | ||
|
|
ab4bbe3680 | ||
|
|
b839edd3a3 | ||
|
|
cfad3d1c18 | ||
|
|
2de603cd77 | ||
|
|
2990009f3b | ||
|
|
8e40bdd92c | ||
|
|
c65b015f01 | ||
|
|
83a0e4d85c | ||
|
|
667da8df14 | ||
|
|
f2044135ea | ||
|
|
bb8ec51939 | ||
|
|
cf0319d480 | ||
|
|
35de59bac5 | ||
|
|
0388c7dede | ||
|
|
081cc5e459 | ||
|
|
58bdfb32de | ||
|
|
aa62971047 | ||
|
|
5ddd96664d | ||
|
|
adb34dffa9 | ||
|
|
4f8b4a9a27 | ||
|
|
2260c4d47a | ||
|
|
5156084cac | ||
|
|
8e42d6d807 | ||
|
|
e91f1c44bb | ||
|
|
ce53552543 | ||
|
|
ff8bd7a8bc | ||
|
|
383ec86c37 | ||
|
|
412a7ad219 | ||
|
|
d38d4cabef | ||
|
|
0e9df9e5a7 | ||
|
|
2f33870a84 | ||
|
|
e63f016835 | ||
|
|
ee89c4d8c2 | ||
|
|
0576e800c4 | ||
|
|
64f5d61e08 | ||
|
|
e08b49a183 | ||
|
|
d97373c5a8 | ||
|
|
55bf807f60 | ||
|
|
6b916699af | ||
|
|
bd547c35c4 | ||
|
|
413c15de24 | ||
|
|
e4e159f0cd | ||
|
|
2eaceb5984 | ||
|
|
4378385b37 | ||
|
|
110ae9d0ed | ||
|
|
cf5f83e9f0 | ||
|
|
fc43a86709 | ||
|
|
eb47efdec9 | ||
|
|
faa62d419e | ||
|
|
088b40216d | ||
|
|
dd38832bb6 | ||
|
|
42e1f0ec8c | ||
|
|
0b3ec18b43 | ||
|
|
43eaf1ef0e | ||
|
|
d717bc74ff | ||
|
|
90c40c8d5a | ||
|
|
993512f0bc | ||
|
|
f088c19f88 | ||
|
|
859a9b25c1 | ||
|
|
190ff381c0 | ||
|
|
358bd415a9 | ||
|
|
8e346a3054 | ||
|
|
7b982ad5ae | ||
|
|
4f11ce81a4 | ||
|
|
9d2324879d | ||
|
|
44938f3f98 | ||
|
|
271bbdd034 | ||
|
|
9d5633ad54 | ||
|
|
7a20eeed0d | ||
|
|
58e61cdefa | ||
|
|
5519b323c8 | ||
|
|
0bbfdeb7a7 | ||
|
|
cd31dec869 | ||
|
|
efcc8fcb65 | ||
|
|
13399529b5 | ||
|
|
6988e5ace1 | ||
|
|
8cd5face9b | ||
|
|
1ab8c0cae4 | ||
|
|
10a8bbc677 | ||
|
|
a60138b19c | ||
|
|
b781e3bab2 | ||
|
|
b7cf12af4c | ||
|
|
302cf16681 | ||
|
|
dfa1d5f5b1 | ||
|
|
eae7e68caf | ||
|
|
d8f7dabee8 |
No files matched your search
@@ -9,7 +9,26 @@
|
||||
!/Source/
|
||||
!/Source/**
|
||||
|
||||
# Portable CPU generator and D3D11 viewer.
|
||||
!/Standalone/
|
||||
!/Standalone/**
|
||||
/Standalone/Build/
|
||||
/Standalone/bin/
|
||||
|
||||
# The guarded single-entry test harness is source, not generated output.
|
||||
!/Tools/
|
||||
!/Tools/**
|
||||
/Tools/**/__pycache__/
|
||||
|
||||
# Closed / historical documents.
|
||||
!/Docs/
|
||||
!/Docs/**
|
||||
|
||||
!VoxelForge.uplugin
|
||||
|
||||
# Keep every design / doc markdown at any depth (AUDIT P1 — these were untracked)
|
||||
# Keep every design / doc markdown at any depth
|
||||
!*.md
|
||||
/Standalone/data/*.log
|
||||
/Standalone/data/*.err
|
||||
/Standalone/data/*.vfmesh
|
||||
/Standalone/Research/**/*.log
|
||||
@@ -25,11 +25,18 @@ try to run anything.
|
||||
## Hard rules (these prevent real bugs — verify against code, don't assume)
|
||||
- **Density sign: negative = solid, positive = air** (MC convention at the mesher).
|
||||
`FVoxelModification::Strength` negative = carve. *The #1 source of confusion.*
|
||||
- **Density functions take VOXEL coords, not cm.** Conversions live in `VoxelTypes.h`.
|
||||
- **Density functions take VOXEL coords, not cm.** The legacy plugin field uses 12.5 cm voxels;
|
||||
the opt-in Core field uses 25 cm voxels and its own tile keys. One legacy 32³ chunk is 4 m.
|
||||
Conversions live in `VoxelTypes.h` and the Core bridge; do not mix scales in queries, anchors, edits,
|
||||
or surface lookups.
|
||||
- **Determinism:** all randomness = hash of (coord, seed, strateIndex). No RNG state — same seed ⇒
|
||||
same world. Player edits (diff layer) are the only non-deterministic overlay.
|
||||
- **Core world invariants:** Core density keeps the same sign (negative solid, positive air), but Core
|
||||
voxel coordinates are 25 cm. Worker-count changes must preserve hashes; Core world edits are
|
||||
intentional world-version changes and must retain finite, walkable strate entrances and exits.
|
||||
- **Async safety:** worker tasks only READ Generator/Mesher, must check `bShuttingDown`, and return via
|
||||
`ProcessQueue` (must stay `EQueueMode::Mpsc`). `EndPlay` blocks on `ActiveTaskCount → 0`.
|
||||
the result queues — `ProcessQueue` and its `CriticalProcessQueue` / `EditedProcessQueue` /
|
||||
`NearProcessQueue` siblings (all must stay `EQueueMode::Mpsc`). `EndPlay` blocks on `ActiveTaskCount → 0`.
|
||||
- **Carry the `Epoch`** through any new async path (stale results are dropped on mismatch).
|
||||
- **Don't "optimize" the perf invariants** in ARCHITECTURE `§8.10` — the `thread_local` box-valid
|
||||
caches, two-pass MC loop, SSE noise, and clipmap streaming are intentional.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Codex Working Guidance
|
||||
|
||||
## Role
|
||||
|
||||
Codex is the implementation agent for this repository. Work directly on the assigned task; Claude Code is a reviewer and coordinator, not a concurrent editor of this checkout.
|
||||
|
||||
## Task contract
|
||||
|
||||
For each task, the request should state:
|
||||
|
||||
- Desired outcome and acceptance criteria.
|
||||
- Scope: relevant folders, systems, or files, and any areas that must not change.
|
||||
- Validation expected: tests, build, editor check, or a stated reason a check cannot run.
|
||||
|
||||
Use the repository's existing conventions and tools. Make the smallest coherent change that satisfies the task. Do not broaden scope without calling it out as optional work.
|
||||
|
||||
## Visibility and progress
|
||||
|
||||
Before any multi-step work or tool use, send one brief update stating the first action.
|
||||
|
||||
Send another brief update only when a major phase finishes or a discovery changes the approach. Every update must contain a concrete result, such as `Found`, `Changed`, `Confirmed`, or `Blocked`.
|
||||
|
||||
If progress is blocked for more than two minutes, stop waiting and report:
|
||||
|
||||
1. Current state and files changed.
|
||||
2. Last command or action and its result.
|
||||
3. Exact blocker.
|
||||
4. Safest next action or the smallest decision needed from the user.
|
||||
|
||||
Never wait silently for a reviewer or another agent. Continue when the next safe implementation step is clear.
|
||||
|
||||
## Implementation loop
|
||||
|
||||
1. Inspect the relevant code and local instructions before editing.
|
||||
2. State a short plan when the task has multiple meaningful parts.
|
||||
3. Implement the change, keeping unrelated changes intact.
|
||||
4. Inspect the diff for accidental edits and integration issues.
|
||||
5. Run the most relevant available validation.
|
||||
|
||||
For Unreal work, prefer targeted validation first: module/plugin build, automated test, compile check, or a minimal editor/runtime smoke test appropriate to the changed behavior. Do not claim validation passed unless it was actually run.
|
||||
|
||||
## Collaboration with Claude Code
|
||||
|
||||
- Codex owns edits and commands in this checkout while its task is active.
|
||||
- Claude reviews plans, diffs, test output, and reported blockers at explicit checkpoints.
|
||||
- Treat Claude feedback as a prioritized fix list. Address actionable items, then re-validate affected behavior.
|
||||
- Do not have both agents edit the same working tree simultaneously. For genuine parallel implementation, use separate git worktrees with non-overlapping tasks.
|
||||
|
||||
## Completion report
|
||||
|
||||
Finish with a concise report containing:
|
||||
|
||||
- What changed and why.
|
||||
- Files changed.
|
||||
- Validation run and its result; if not run, why and the next best check.
|
||||
- Any remaining risk, assumption, or follow-up.
|
||||
|
||||
## Reviewer prompt for Claude Code
|
||||
|
||||
> You are the reviewer and coordinator for this task. Do not edit the shared checkout. Review the supplied Codex plan, diff, and validation output against the acceptance criteria. Return only prioritized, actionable findings, including file and line references where possible. If there are no blocking issues, say so explicitly.
|
||||
@@ -0,0 +1,234 @@
|
||||
# AUDIT-COLD-2026-09-14: read-only examination, with the orchestrator's verification notes
|
||||
|
||||
Produced by Codex (gpt-5.6-luna, max effort, read-only sandbox) on 2026-09-14 at HEAD `7a972f5`,
|
||||
briefed with no project background. It did read the plugin-root docs, so it is not context-free,
|
||||
but its findings are code-level. Findings are verbatim below. These notes record what the
|
||||
orchestrator checked against the code afterwards.
|
||||
|
||||
## Orchestrator verification notes
|
||||
- **#7 CONFIRMED, a determinism bug introduced by the tile-cache work (`2de603c`/`cfad3d1`).**
|
||||
`VoxelGenerator.cpp:2968` moves `CP_OpStack` into the cache entry and resets `GTunnelCoreCache`.
|
||||
A later request for the same `ChunkCoord` under the other `bTileCacheContext` misses the key
|
||||
(`:2660`), but the refetch guard (`:2723-2725`) skips because chunk, owner and version are
|
||||
unchanged, so evaluation uses the moved-from (empty) stack and core cache. The hit path sets
|
||||
`CP_Chunk` to the shared key `(0,0,Z)` (`:2698`), so the spawn column `X=Y=0` reaches this state
|
||||
after an ordinary hit, not only in a narrow timing window. Coarse tiles (LOD >= 3) covering the
|
||||
origin can be evaluated wrongly depending on the order work reaches a worker.
|
||||
- **#3 and #8 are real but narrower than stated.** In multiplayer, the server and client worlds share
|
||||
seed and layout by design, so the cached values are identical. Contamination needs worlds that
|
||||
genuinely differ under the same key: editor edits that do not bump the version, or two diff
|
||||
layers with different contents at the same `ModsVersion`. Cheap to fix (add owner/layer identity).
|
||||
- **#19 is not a defect under C++20** (UE 5.7), where left-shifting negative signed values is
|
||||
defined. Overflow for extreme coordinates remains theoretical.
|
||||
- **#21 matters again** if the gap/seal AllSolid proof (in progress) calls `HasAnyModInChunkRange`
|
||||
per LOD0 tile.
|
||||
- The others (#1, #2, #4-6, #9-18, #20, #22) were not independently re-verified. Verify each before
|
||||
fixing it.
|
||||
|
||||
---
|
||||
|
||||
# Read-only audit report
|
||||
|
||||
I made no file changes and did not build or run the plugin. “VERIFIED” means the failure follows directly from the source path; “SUSPECT” means a runtime/compiler-dependent hazard that was not executed.
|
||||
|
||||
## Highest impact
|
||||
|
||||
1. **The density-volume worker races live generator mutation — P1 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelWorld.cpp` — `FScopedGenerationPause` (203–222), `AVoxelWorld::RebuildStrates` (551–582), `AVoxelWorld::ChangeSeed` (5569–5623).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelDensityVolume.cpp` — `UVoxelDensityVolume::Reset` (101–120), `ProcessOneFill` (603–637).
|
||||
|
||||
`FScopedGenerationPause` waits for ordinary generation tasks and decoration tasks, but not the dedicated density-volume fill thread. That thread reads the raw `Generator` pointer and calls `GetDensityAt` while `RebuildStrates` or `ChangeSeed` mutates the generator and strata manager.
|
||||
|
||||
A fill in progress during a PIE edit or seed change can therefore read partially rebuilt arrays or mixed old/new settings. This is an actual data race with crash/garbled-density potential. `Reset` also clears `PendingFills` but not the already queued `FillQueue`, causing stale work to continue after reset.
|
||||
|
||||
2. **Decoration tasks can use a destroyed content manager — P1 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelContentManager.cpp` — `UVoxelContentManager::BeginDestroy` (58–62), `NotifyShutdown` (65–75), `WaitForDecorationTasks` (78–85), `LaunchDecoTasks` (327–415).
|
||||
|
||||
Tasks capture raw `this` and later access `bShuttingDown` and `DecoResults`. Shutdown waits only three seconds, ignores the timeout result, and `BeginDestroy` immediately calls `Super::BeginDestroy`.
|
||||
|
||||
Any decoration task exceeding the timeout can later enqueue into the destroyed manager. The global task counter also includes tasks belonging to unrelated worlds, so one world can time out because another world is busy.
|
||||
|
||||
3. **The diff snapshot TLS cache is not keyed by its owning diff layer — P1 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelGenerator.cpp` — `UVoxelGenerator::GetDensityAt` (3069–3097).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelDiffLayer.h` — `GetModsVersion` and `ModsVersion` (241, 289).
|
||||
|
||||
`thread_local FDiffSlot DiffSlots[64]` considers only chunk coordinates and `ModsVersion`. It does not include the `UVoxelDiffLayer*`.
|
||||
|
||||
Two PIE worlds commonly have the same chunk and the same per-layer version—for example, both reach version 2 after their first edit. If a worker samples world A first, world B can hit A’s cached modification snapshot and apply the wrong edits. This produces cross-world terrain contamination dependent on worker history.
|
||||
|
||||
4. **Fixed-only strata configurations are silently disabled — P1 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelWorld.cpp` — `AVoxelWorld::BeginPlay` (1294–1305).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelStrateManager.cpp` — `UVoxelStrateManager::Initialize` (1060–1244).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelGenerator.cpp` — fallback generation in `GetDensityAt` (3030–3039).
|
||||
|
||||
`BeginPlay` creates a strata manager only when a season is assigned or `StratePool.Num() > 0`. It does not test `FixedStrates.Num()`.
|
||||
|
||||
A settings asset with valid fixed strata but no season and no pool therefore receives no strata manager. The generator takes its generic fallback path, ignoring the authored fixed definitions.
|
||||
|
||||
5. **Settings rebuilds leave dependent systems with inconsistent snapshots — P1/P2 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelWorld.cpp` — `RebuildStrates` (551–582), `PostEditChangeProperty` (885–895), `OnObjectModifiedInEditor` (898–985).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelGenerator.cpp` — `InitializeSettings` (2079–2088).
|
||||
|
||||
`RebuildStrates` reinitializes the strata manager but does not reinitialize generator settings. `PostEditChangeProperty` only regenerates chunks. Asset-change handling recognizes strata and terrain-op definitions, but not general settings or biome assets.
|
||||
|
||||
Changing seed, world radius, edge sealing, the settings object reference, or related configuration can leave the manager using new values while the generator, content manager, or density volume still use old snapshots. `ChangeSeed` has a separate correct reinitialization path, but the other rebuild paths do not. This also contradicts the `RebuildStrates` documentation in `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelWorld.h` (572–575), which promises to re-read all settings.
|
||||
|
||||
6. **Invalid strata initialization destroys the previous valid layout — P1/P2 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelStrateManager.cpp` — `UVoxelStrateManager::Initialize` (1060–1205).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelWorld.cpp` — `RebuildStrates` (562–576), `OnObjectModifiedInEditor` (975–985).
|
||||
|
||||
`Initialize` empties `StrateLayout`, `SeasonStrates`, and related state before all season/layout validation has succeeded. If an assigned season is missing, malformed, or contains invalid bounds, initialization returns false after the old layout has already been discarded.
|
||||
|
||||
`RebuildStrates` then leaves existing meshes visible while future generation queries see an empty or partial manager. The editor asset-modification path ignores the initialization return value and regenerates anyway, making a transient invalid asset capable of replacing the working world with fallback/empty terrain.
|
||||
|
||||
## Material correctness and determinism issues
|
||||
|
||||
7. **The operator-stack cache is stale when tile-cache context changes — P2 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelGenerator.cpp` — `UVoxelGenerator::GetDensityAt` (2218–2230, 2255–2261, 2320–2324, 2393–2407, 2551–2573, 2600–2672).
|
||||
|
||||
The tunnel cache key includes whether tile-cache mode is active, but the rebuild guard does not. A fine-LOD request can build and move the operator stack into the cache, leaving the worker-local `CP_OpStack` empty. A later request for the same chunk under a different tile-cache context misses the cache key but does not rebuild the stack because chunk, manager, and layout version are unchanged.
|
||||
|
||||
The empty or mismatched stack is then used for density evaluation. LOD request order can therefore change terrain results.
|
||||
|
||||
8. **Several surface and biome TLS caches omit generator/context identity — P2 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelGenerator.cpp` — `FSurfaceColumnCache::Acquire` (274–328), `GetDensityAt` (2692–2705), `GetSurfaceHeightAt` (5149–5166), `ClassifyTile` (6014–6023), `GetBiomeMaterialAt` (7019–7034).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelBiomeTypes.h` — `FChunkBiomeCache::Contains` (240–245).
|
||||
|
||||
These worker-local caches use combinations of chunk, seed, strata key, and layout version, but not the owning generator, strata manager, or biome context. Two worlds can legitimately share all those scalar keys while having different terrain or biome parameters.
|
||||
|
||||
When a pooled worker handles both worlds, the second world can receive cached surface columns, biome classification, or material data from the first. Direct asset changes that do not advance the relevant version have the same stale-cache problem.
|
||||
|
||||
9. **Native shaft/island topology caches omit parameters used to build them — P2 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelGenerator.cpp` — `GetVerticalShaftDensity` (7090–7106, 7211–7219, 7392–7413), `GetFloatingIslandDensity` (7518–7554).
|
||||
|
||||
The shaft cache key omits `LedgeSpacing` and `LedgeDepth`, although both affect connector and ledge placement. The floating-island cache key omits `BoundarySealThickness`, although it affects cached vertical spread.
|
||||
|
||||
Changing these values—or using two contexts with identical keyed values but different omitted values—reuses old topology on the same worker.
|
||||
|
||||
10. **Completed pre-edit decoration results can resurrect removed objects — P2 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelWorld.cpp` — `ApplyModification` (5319–5375).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelContentManager.cpp` — `LaunchDecoTasks` (381–404), `ProcessDecoResults` (769–807), `MergeCellResult` (815–862), `RemoveDecorationsInSphere` (1249–1260).
|
||||
|
||||
Applying a live diff removes currently applied decorations but does not invalidate queued/in-flight decoration builds or associate them with a diff version. `BuildId` only identifies decoration-region rebuilds.
|
||||
|
||||
Sequence: a task computes old grass/props, the edit removes current instances, then the old result is drained and accepted. The old decorations reappear over the edited terrain until another rebuild occurs.
|
||||
|
||||
11. **Landmark conditions are evaluated without the actual biome context — P2 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelContentManager.cpp` — `UpdateDecorations` (153–263), `UpdateLandmarks` (1263–1310), `SpawnFromProfile` (1101–1137).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelGenerator.cpp` — `EvaluateTerrainConditions` (6682–6717).
|
||||
|
||||
Normal decoration updates populate `FDecoContext::BiomeCtx`; landmark updates construct a context but never set `BiomeCtx`. A landmark profile using biome, moisture, relief, or biome-border conditions therefore evaluates with the default/invalid biome map.
|
||||
|
||||
On a non-default biome map, landmarks can spawn or fail to spawn differently from ordinary decorations.
|
||||
|
||||
12. **Decoration cleanup uses local coordinates as world coordinates — P2 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelWorld.cpp` — `ApplyModification` (5369–5375); `WorldToLocalVoxel` and `LocalVoxelToWorld` (2314–2321).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelContentManager.cpp` — `RemoveInstancesInContent` (1231–1242).
|
||||
|
||||
`Modification.Center` is a local voxel coordinate, but cleanup computes `Center * VOXEL_SIZE` and passes it to a world-space overlap query. For a translated actor, the query is centered at the wrong location.
|
||||
|
||||
The same cleanup also approximates boxes and capsules with only `Modification.Radius`, which does not cover a long capsule or a box’s full extent. Existing decorations can remain over an edit or unrelated origin decorations can be removed.
|
||||
|
||||
13. **Decoration application order is timing-dependent — P2 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelContentManager.cpp` — `LaunchDecoTasks` (327–415), `ProcessDecoResults` (769–810), `ApplyRegion` (885–952), `RebuildDesiredCells` (281–324).
|
||||
|
||||
Tasks enqueue results in completion order. Results append actor spawns and HISM transforms in that order; mesh buckets are iterated through a `TMap`. Equal-distance pending regions also have no deterministic tie-breaker.
|
||||
|
||||
The same candidate set can therefore produce different actor spawn order, NetGUID/side-effect order, HISM instance indices, and internal clustering across runs or machines.
|
||||
|
||||
14. **The passage shortlist version is not advanced when passages are cleared — P2/P3 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelStrateManager.cpp` — `GeneratePassages` (1611–1620, 2477), `EvaluateModifierSDF` (2488–2524), `VF_GetNearbyPassages` (294–338).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelStrateManager.h` — `GetLayoutVersion` and `PassagesVersion` (250, 515, 519).
|
||||
|
||||
`GeneratePassages` empties `Passages` and returns early for an empty layout without incrementing `PassagesVersion`. Existing worker TLS nearby-passage caches still match the manager lifetime, chunk, and unchanged version.
|
||||
|
||||
After a previously valid layout becomes empty, `EvaluateModifierSDF` can reuse indices into the now-empty array and dereference invalid passage data. This is an edge configuration, but it is reachable through the invalid-layout paths above.
|
||||
|
||||
## Input-contract and performance issues
|
||||
|
||||
15. **Diff modifications accept nonfinite and invalid geometry — P2/P3 — VERIFIED for malformed input**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelDiffLayer.cpp` — `CanModify` (20–44), `ApplyModification` (63–96), `EvaluateMods` (191–248).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelWorld.cpp` — `CarveAtPosition` (5301–5307).
|
||||
|
||||
There is no finite-value validation for strength, radius, centers, endpoints, or extents. A NaN strength survives `FMath::Abs`, then propagates through `TotalOffset` and density evaluation. Infinite strength produces infinite density. Negative or zero radii can consume budget while producing no meaningful spherical modification.
|
||||
|
||||
This is reachable from C++ or malformed Blueprint/runtime data and permanently poisons the diff layer until cleared.
|
||||
|
||||
16. **The public diff API contradicts itself about coordinate units — P2/P3 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelDiffLayer.h` — `FVoxelModification::Center` (64), `CapsuleEnd` (86).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelWorld.h` — `ApplyModification` documentation (334–340).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelDiffLayer.cpp` — `GetWorldBounds` (88–96).
|
||||
|
||||
The modification struct comments describe the center as world-space, while the low-level world API and implementation interpret it as voxel-space. The helper functions explicitly convert world centimeters into voxel coordinates.
|
||||
|
||||
A caller following the struct comment and supplying centimeters gets an edit displaced by a factor of `VOXEL_SIZE`.
|
||||
|
||||
17. **Diff budget accounting underestimates boxes and long capsules — P3 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelWorld.cpp` — `CarveBox`/`FillBox` (5379–5398), capsule helpers (5401 onward).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelDiffLayer.cpp` — `CanModify` (34–39).
|
||||
|
||||
The budget charges every shape as a sphere with volume based on `Modification.Radius`. Boxes use the maximum half-extent as a proxy; capsules use the tube radius even when endpoints are very far apart.
|
||||
|
||||
A capsule with radius 3 and endpoints 1000 voxels apart is charged like a tiny sphere despite affecting a long volume and many chunks. Large edits can therefore bypass the intended modification budget and cause excessive remeshing.
|
||||
|
||||
18. **Decoration radius/spacing changes are ignored while the player is stationary — P3 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelContentManager.cpp` — `UpdateDecorations` (199–256), `RebuildDesiredCells` (265–325).
|
||||
|
||||
Radius and spacing are refreshed every update, but desired-cell rebuilding occurs only when `PlayerCell != LastDecoCell`.
|
||||
|
||||
Shrinking the radius leaves distant regions loaded; expanding it does not enqueue new regions; changing spacing leaves existing regions at the old grid layout. Movement eventually repairs the state, but a stationary player sees stale decoration coverage.
|
||||
|
||||
19. **Signed left shifts of negative chunk coordinates are undefined — P3 — SUSPECT**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelWorld.cpp` — `BuildDesiredTiles` (3498–3548).
|
||||
|
||||
Streaming computes coordinates with expressions such as `T.Z << L`. Ordinary underground tiles have negative `T.Z`, so this left-shifts a negative signed integer. Large positive coordinates can also overflow.
|
||||
|
||||
The exact manifestation is compiler/optimization-dependent, so I classify this as SUSPECT rather than a fully end-to-end verified runtime failure.
|
||||
|
||||
20. **Interleaved transitions can divide by zero — P3 — VERIFIED math path**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelStrateDefinition.h` — `TransitionBlendChunks` (99–101).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelStrateManager.cpp` — `GetGenerationParams` (5673, 5728–5753, 5826).
|
||||
|
||||
The property metadata clamps the value in the editor, but runtime/cooked data can still contain zero. The interleaved transition path divides warped distance by `EffectiveBlend` without a zero guard.
|
||||
|
||||
A zero-width transition can produce Inf/NaN blend values and invalid generation parameters.
|
||||
|
||||
21. **Tile classification scans the entire modification map — P3 — VERIFIED**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelGenerator.cpp` — `ClassifyTileUncached` (5785–5790).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelDiffLayer.cpp` — `HasAnyModInChunkRange` (170–188).
|
||||
|
||||
Classification calls a routine that walks every `ChunkMods` key under the read lock instead of using the existing per-chunk grouping to narrow the query.
|
||||
|
||||
After many edits, each tile classification becomes O(number of modified chunks), increasing lock hold time and making edit-heavy worlds disproportionately expensive.
|
||||
|
||||
22. **Operator documentation is internally stale — P3 — VERIFIED documentation contradiction**
|
||||
|
||||
Location: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelDensityOp.h` (1–3, 86–92).
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelGenerator.cpp` — `UVoxelGenerator::GetDensityAt` (2393–2600).
|
||||
|
||||
The header says the density-op system is not wired into `GetDensityAt`, while later comments say all eight operators now have the stack path. The generator does in fact execute the operator stack.
|
||||
|
||||
This is not itself a runtime defect, but it is misleading maintenance documentation around a central generation path.
|
||||
|
||||
The most urgent issues are the uncoordinated density-volume thread, decoration-task lifetime, cross-world TLS diff cache, and incomplete settings/layout rebuilds.
|
||||
@@ -0,0 +1,402 @@
|
||||
# AUDIT-SOL-2026-09-11.md — read-only audit of experimental @ aa62971
|
||||
|
||||
Performed by Codex **gpt-5.6-sol**, reasoning effort xhigh, **sandbox read-only** — no file was
|
||||
created, modified or deleted. Commissioned by Jahni after the experimental branch showed two
|
||||
regressions against main: generation much slower, and the player falling through the world.
|
||||
|
||||
Verbatim report follows.
|
||||
|
||||
---
|
||||
|
||||
# Audit conclusion
|
||||
|
||||
The two regressions share a cause, but they are not the same bug:
|
||||
|
||||
1. **The remaining generation slowdown is primarily in density evaluation.** The composable operator representation is still being interpreted in the sampling hot path, with rich per-sample state, block-wide operator passes, and several corrective passes afterward. The outer tile classifier is not where the original ~8× went.
|
||||
|
||||
2. **There is no “collision-ready before player-ready” invariant.** Neither branch explicitly had one. `main` was fast enough—and had simpler spawn geometry—so the race remained hidden. `experimental` made the race long enough to become visible, especially because the new origin room’s air and supporting floor can occupy different tiles.
|
||||
|
||||
I disagree with the inference that the old behavior proves there was a correctness barrier. The observation is valid, but the code shows a timing dependency masquerading as a guarantee.
|
||||
|
||||
## 1. Density evaluation is the dominant performance regression
|
||||
|
||||
**Status: Verified architecture; strong attribution. Highest expected performance impact.**
|
||||
|
||||
A streamed LOD0 tile asks for a 35³ haloed density grid: **42,875 sample positions** ([VoxelMarchingCubesMesher.cpp:227](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:227>)).
|
||||
|
||||
At `34f06df`, that hot path was approximately:
|
||||
|
||||
- Resolve one cached chunk parameter set.
|
||||
- Enter one direct archetype switch.
|
||||
- Keep most intermediate values in local variables.
|
||||
- Skip the detailed cave suite when `bNearCaveSurface` was false—historically around 70% of deep-solid samples.
|
||||
- Apply disturbances/differences and return.
|
||||
|
||||
The relevant historical path is `34f06df:Source/VoxelForge/Private/VoxelGenerator.cpp:438-631` and `:947-1621`.
|
||||
|
||||
Current `experimental` instead:
|
||||
|
||||
- Allocates/initializes an `FVoxelOpSample` for every position, containing density, SDF, floor metadata, tunnel metadata, pointers and indices ([VoxelDensityOp.h:327](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelDensityOp.h:327>)).
|
||||
- Determines a block-wide active-operator list.
|
||||
- Traverses the sample array once per active operator.
|
||||
- Uses scalar per-sample fallback implementations for most concrete operators ([VoxelDensityOpStack.cpp:51](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:51>)).
|
||||
- Copies block scratch into the session cache and later back into output.
|
||||
- Still calls the scalar `GetDensityAt` wrapper for every grid point, so routing, coordinate setup and the post-stack structural tail remain per-sample ([VoxelGenerator.cpp:1690](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:1690>), [VoxelGenerator.cpp:2165](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2165>)).
|
||||
|
||||
Loop inversion removed much of the virtual dispatch cost, but it did not restore the old locality or short-circuiting. An operator whose interval intersects the surface anywhere in the block can be run across the whole block; the old path could decide per sample that expensive detail work was irrelevant.
|
||||
|
||||
That is the architectural mistake: **the composable authoring model was carried directly into execution instead of being compiled/lowered into a fused evaluator.** Composition is not itself the problem. Interpreting it through rich mutable sample records is.
|
||||
|
||||
The 128³ comparison strongly supports this conclusion: the editor export path owns a shared density grid and does not force the streaming outer-classifier path ([VoxelForgeExploreCommandlet.cpp:1700](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp:1700>)), yet it still takes approximately 1.04 seconds versus 0.130 seconds before the operator-stack wire-in.
|
||||
|
||||
The later features add cost, particularly the D-shaped floors, but the history indicates roughly a 17% net penalty there—not anything close to the remaining 8×.
|
||||
|
||||
## 2. The player can become active before any supporting collision exists
|
||||
|
||||
**Status: Verified. Highest correctness impact.**
|
||||
|
||||
`BeginPlay` initializes systems, but it does not synchronously request a spawn support tile, wait for a mesh, or gate pawn gravity/control ([VoxelWorld.cpp:988](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:988>)).
|
||||
|
||||
Streaming begins from `Tick`, and only when `GetPlayerPosition()` is not exactly `FVector::ZeroVector` ([VoxelWorld.cpp:1167](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:1167>), [VoxelWorld.cpp:1359](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:1359>)). Thus an exact world-origin pawn is indistinguishable from “no pawn.” Even when Z is nonzero and that specific sentinel is avoided, there is still no bootstrap barrier.
|
||||
|
||||
The supplied play trace shows:
|
||||
|
||||
- `BeginPlay` finished at about **50 ms**.
|
||||
- The first desired-tile pass did not begin until about **439 ms**.
|
||||
- The first LOD0 nonempty/mixed result was not ready until about **1.093 s**.
|
||||
- That result still represented mesh/collision submission, not collision-cook completion.
|
||||
|
||||
At ordinary gravity, one second is enough to fall roughly five metres.
|
||||
|
||||
There are three additional gaps:
|
||||
|
||||
- A tile is inserted into `LoadedTiles` before its empty/nonempty application path completes ([VoxelWorld.cpp:1532](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:1532>)).
|
||||
- Only LOD0 receives collision ([VoxelWorld.cpp:3109](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:3109>)). Coarser visible terrain can therefore be present while the corresponding collidable LOD0 tile is still queued.
|
||||
- Current architecture documentation says RMC collision is asynchronously cooked ([ARCHITECTURE.md:817](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:817>)), but `VoxelWorld` records no cook-complete state. Its `apply_s` measurement ends after submission and says nothing about when physics can actually collide.
|
||||
|
||||
I compared the equivalent paths on `main`. It also:
|
||||
|
||||
- Starts streaming from `Tick`.
|
||||
- Uses the zero-position guard.
|
||||
- Marks a tile loaded before handling its empty result.
|
||||
- Enables collision only for LOD0.
|
||||
- Has no spawn collision barrier or collision-cook acknowledgement.
|
||||
|
||||
So no explicit guarantee was removed inside `VoxelWorld`. What `experimental` broke was the **implicit timing budget** that had hidden this race.
|
||||
|
||||
The new origin-room geometry makes it worse. The first central LOD0 request in the trace is uniformly air and correctly receives no component. That is not itself an error: the player occupies air, while collision belongs to the neighboring floor/wall surface tile. With the current default origin geometry, the floor can lie near or below the lower tile boundary ([VoxelPassageGeometry.h:365](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelPassageGeometry.h:365>)). The supporting tile therefore has to arrive and finish cooking independently.
|
||||
|
||||
`CollisionOnlyTiles` does not close this gap. It controls presentation of already generated LOD0 components for non-player anchors; it does not synthesize or reserve collision underneath the local pawn. The supplied trace records zero collision-only tiles.
|
||||
|
||||
## 3. The mesher’s nested block classifier is probably self-inflicted overhead
|
||||
|
||||
**Status: Mechanism verified; net cost strongly suspected. High performance impact.**
|
||||
|
||||
The code contradicts its own guidance:
|
||||
|
||||
- The mesher header says the optional 8³ refinement should remain disabled during streaming because repeated proofs may cost more than they save ([VoxelMarchingCubesMesher.h:150](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h:150>)).
|
||||
- `VoxelWorld` unconditionally enables it ([VoxelWorld.cpp:1102](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:1102>)).
|
||||
|
||||
For every outer-Mixed tile, the mesher may run up to 64 additional `ClassifyTile` calls ([VoxelMarchingCubesMesher.cpp:254](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:254>)). On cache misses, each call builds another operator stack ([VoxelGenerator.cpp:5779](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:5779>)).
|
||||
|
||||
In `MyPlay`:
|
||||
|
||||
- 298 LOD0 tiles were outer-Mixed.
|
||||
- All 298 produced geometry.
|
||||
- Therefore every one eventually populated the full density grid.
|
||||
- The nested classifier could only avoid marching-cubes cell processing in uniform sub-blocks; it could not avoid the main density grid once any surface sub-block was found.
|
||||
|
||||
This is precisely the workload where the header warns it may lose.
|
||||
|
||||
There is also an instrumentation trap: the outer cache counters are snapped before meshing. Nested classifier calls occur inside the measured `mesh_s` interval ([VoxelWorld.cpp:2762](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:2762>)). Consequently, the reported “cache hits 0” does **not** describe classifier activity originating inside the mesher.
|
||||
|
||||
This deserves the first controlled A/B test: identical trace and seed with only `bUseBlockEarlyOut` changed, while separately counting outer and nested classifier calls.
|
||||
|
||||
## 4. The outer whole-tile classifier is probably earning its keep
|
||||
|
||||
**Status: Verified workload behavior; exact counterfactual unmeasured.**
|
||||
|
||||
I would not remove the outer classifier based on the 95 ms figure.
|
||||
|
||||
It rejected **547 of 845 LOD0 tiles**, or 64.7%, before meshing:
|
||||
|
||||
- 484 uniformly solid.
|
||||
- 63 uniformly air.
|
||||
- 298 mixed, all of which produced geometry.
|
||||
|
||||
A full 35³ grid for those rejected tiles would represent up to 23.45 million density positions. Previous history also shows that the outer classifier eliminated multi-second empty-tile fallbacks.
|
||||
|
||||
The 95 ms is not a clean CPU cost. It is elapsed wall time inside a `BackgroundNormal` worker task, so it includes preemption and contention. A second trace in the repository from the same current code has an LOD0 classifier median around **8.45 ms**, versus **94.7 ms** in the moving/rendering owner session. The real play number is important for latency, but it should not be interpreted as intrinsic classifier compute.
|
||||
|
||||
My assessment is:
|
||||
|
||||
- **Outer whole-tile classification:** valuable under the current expensive evaluator.
|
||||
- **Nested 8³ block classification:** likely negative in this workload.
|
||||
- **Long-term:** making density evaluation cheap again may change the outer classifier’s break-even point.
|
||||
|
||||
## 5. Movement does not cancel obsolete generation work
|
||||
|
||||
**Status: Verified mechanism; contribution to running falls strongly suspected.**
|
||||
|
||||
The abort predicate only notices shutdown or an explicit generation pause ([VoxelWorld.h:764](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelWorld.h:764>)). It does not abort a tile because the player has moved and the tile is no longer desired.
|
||||
|
||||
Desired work is globally distance-sorted using an artificial chunk-center position rather than the exact pawn position, with no strong collision-critical/L0 priority ([VoxelWorld.cpp:1864](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:1864>)). Up to 14 background tasks can therefore be occupied by old, coarse, or non-supporting work while the player’s new LOD0 floor waits.
|
||||
|
||||
By the steady point in the supplied session there were 1,481 loaded tiles for 899 currently desired tiles, with roughly 582 on the unload/transition side. Retaining old rendered tiles is not automatically wrong, but generation already launched for the old neighborhood is not preempted in favor of collision-critical tiles.
|
||||
|
||||
This is a credible explanation for falls while running. The trace cannot prove it because it omits tile coordinates, pawn position and collision-cook completion.
|
||||
|
||||
## 6. The verdict cache is mismatched to normal streaming
|
||||
|
||||
**Status: Implementation verified; negative startup ROI suspected. Medium impact.**
|
||||
|
||||
The cache:
|
||||
|
||||
- Is global and protected by one lock.
|
||||
- Advances a clock and linearly scans entries.
|
||||
- Holds only 512 proofs ([VoxelGenerator.cpp:1512](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:1512>)).
|
||||
- Keys include sample step, limiting reuse across LODs.
|
||||
- Does not store a useful outer proof for Mixed tiles.
|
||||
|
||||
Fresh streaming normally visits unique regions, which explains the zero outer hits in both supplied traces. Nested block classifiers can then insert many short-lived proofs, pay the lock/scan costs, and evict one another—without appearing in the trace’s cache telemetry.
|
||||
|
||||
It demonstrably helped a deliberately repeated commandlet case, but that access pattern is not representative of a moving player. I would treat it as unproven or likely negative during startup until nested calls are measured.
|
||||
|
||||
## 7. Field semantics are now implemented in several overlapping places
|
||||
|
||||
**Status: Verified duplication; cost not isolated. Medium impact and high maintenance risk.**
|
||||
|
||||
The effective field is no longer simply “the operator stack”:
|
||||
|
||||
1. The operator stack evaluates the composable field.
|
||||
2. The classifier maintains parallel interval/proof semantics.
|
||||
3. `GetDensityAt` performs a post-stack repair pipeline for origin floors, passage structure, passage floors and room floors.
|
||||
|
||||
For example, `FRoomGraphSource` evaluates the cached room graph SDF and then separately traverses tunnels again for metadata ([VoxelDensityOpStack.cpp:4234](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:4234>)). The common tail then reasserts origin and passage structure/floors ([VoxelGenerator.cpp:2244](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2244>)).
|
||||
|
||||
This is the clearest sign of overengineering: later correctness fixes are compensating for where an operation sits in the pipeline. It creates duplicated traversal, makes interval proofs harder to trust, and prevents the stack from being the single semantic source of truth.
|
||||
|
||||
Runtime also does not use the commandlet’s shared density-grid ownership, so adjacent tiles repeat halo/boundary samples. Earlier measurement put this duplication around 18%. It existed in the old architecture too, so it did not cause the original regression, but every repeated sample is now much more expensive.
|
||||
|
||||
## 8. There are two lower-confidence ways to get a permanent hole
|
||||
|
||||
**Status: Verified risk paths; not demonstrated by this trace.**
|
||||
|
||||
First, whole-tile AllAir/AllSolid verdicts are trusted without exact validation. They become loaded `bEmpty` tiles with no component. Internal block verdicts are later checked against the full grid when meshing occurs, but an outer uniform verdict has no equivalent safety net.
|
||||
|
||||
The supplied aggregate argues against a widespread problem—every LOD0 Mixed tile produced geometry—but it cannot establish whether the floor tile beneath the pawn was incorrectly declared uniform because tile coordinates were not logged.
|
||||
|
||||
Second, `ApplyMeshToTile` has no success return. If runtime-mesh initialization fails, the function returns while the tile remains recorded as loaded, so it will not naturally retry ([VoxelWorld.cpp:3064](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:3064>)). I found no trace evidence that this occurred.
|
||||
|
||||
The normal `bAborted` result path is not the likely culprit: it removes the pending record without marking the tile loaded, allowing a later desired pass to reschedule it. There is, however, an editor pause corner case where a worker can exit without enqueuing any result and leave its pending entry behind if pause acquisition times out. That is a real bug, but it does not match this epoch-zero play session.
|
||||
|
||||
## What I would prove next
|
||||
|
||||
In order:
|
||||
|
||||
1. A/B the forced nested block classifier on the exact owner trace, recording outer versus nested calls, proof outcomes, stack builds and cache probes.
|
||||
2. Add collision-specific timestamps: requested, mesh submitted, collision cook started/completed, component registered, plus tile and pawn coordinates.
|
||||
3. Force exact meshing/validation for every supposedly uniform LOD0 tile in the spawn neighborhood to rule a false classifier proof in or out.
|
||||
4. Benchmark a compiled/fused evaluator that retains the authored operator graph but restores per-sample short-circuiting and avoids spilling `FVoxelOpSample` through memory for every operator.
|
||||
5. Prioritize a small LOD0 support neighborhood and require collision-cook completion before releasing the pawn. Faster generation alone cannot make the current lifecycle correct.
|
||||
|
||||
The blunt version: **the operator abstraction leaked into the innermost loop, and streaming treats “work submitted/applied” as “safe to stand on.”** The classifier work then became a collection of compensators for the expensive evaluator, some useful and some apparently duplicative.
|
||||
|
||||
This was a read-only audit. No files were created, modified or deleted; `git status` remains clean on `experimental` at `aa62971`.
|
||||
tokens used
|
||||
782 274
|
||||
# Audit conclusion
|
||||
|
||||
The two regressions share a cause, but they are not the same bug:
|
||||
|
||||
1. **The remaining generation slowdown is primarily in density evaluation.** The composable operator representation is still being interpreted in the sampling hot path, with rich per-sample state, block-wide operator passes, and several corrective passes afterward. The outer tile classifier is not where the original ~8× went.
|
||||
|
||||
2. **There is no “collision-ready before player-ready” invariant.** Neither branch explicitly had one. `main` was fast enough—and had simpler spawn geometry—so the race remained hidden. `experimental` made the race long enough to become visible, especially because the new origin room’s air and supporting floor can occupy different tiles.
|
||||
|
||||
I disagree with the inference that the old behavior proves there was a correctness barrier. The observation is valid, but the code shows a timing dependency masquerading as a guarantee.
|
||||
|
||||
## 1. Density evaluation is the dominant performance regression
|
||||
|
||||
**Status: Verified architecture; strong attribution. Highest expected performance impact.**
|
||||
|
||||
A streamed LOD0 tile asks for a 35³ haloed density grid: **42,875 sample positions** ([VoxelMarchingCubesMesher.cpp:227](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:227>)).
|
||||
|
||||
At `34f06df`, that hot path was approximately:
|
||||
|
||||
- Resolve one cached chunk parameter set.
|
||||
- Enter one direct archetype switch.
|
||||
- Keep most intermediate values in local variables.
|
||||
- Skip the detailed cave suite when `bNearCaveSurface` was false—historically around 70% of deep-solid samples.
|
||||
- Apply disturbances/differences and return.
|
||||
|
||||
The relevant historical path is `34f06df:Source/VoxelForge/Private/VoxelGenerator.cpp:438-631` and `:947-1621`.
|
||||
|
||||
Current `experimental` instead:
|
||||
|
||||
- Allocates/initializes an `FVoxelOpSample` for every position, containing density, SDF, floor metadata, tunnel metadata, pointers and indices ([VoxelDensityOp.h:327](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelDensityOp.h:327>)).
|
||||
- Determines a block-wide active-operator list.
|
||||
- Traverses the sample array once per active operator.
|
||||
- Uses scalar per-sample fallback implementations for most concrete operators ([VoxelDensityOpStack.cpp:51](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:51>)).
|
||||
- Copies block scratch into the session cache and later back into output.
|
||||
- Still calls the scalar `GetDensityAt` wrapper for every grid point, so routing, coordinate setup and the post-stack structural tail remain per-sample ([VoxelGenerator.cpp:1690](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:1690>), [VoxelGenerator.cpp:2165](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2165>)).
|
||||
|
||||
Loop inversion removed much of the virtual dispatch cost, but it did not restore the old locality or short-circuiting. An operator whose interval intersects the surface anywhere in the block can be run across the whole block; the old path could decide per sample that expensive detail work was irrelevant.
|
||||
|
||||
That is the architectural mistake: **the composable authoring model was carried directly into execution instead of being compiled/lowered into a fused evaluator.** Composition is not itself the problem. Interpreting it through rich mutable sample records is.
|
||||
|
||||
The 128³ comparison strongly supports this conclusion: the editor export path owns a shared density grid and does not force the streaming outer-classifier path ([VoxelForgeExploreCommandlet.cpp:1700](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp:1700>)), yet it still takes approximately 1.04 seconds versus 0.130 seconds before the operator-stack wire-in.
|
||||
|
||||
The later features add cost, particularly the D-shaped floors, but the history indicates roughly a 17% net penalty there—not anything close to the remaining 8×.
|
||||
|
||||
## 2. The player can become active before any supporting collision exists
|
||||
|
||||
**Status: Verified. Highest correctness impact.**
|
||||
|
||||
`BeginPlay` initializes systems, but it does not synchronously request a spawn support tile, wait for a mesh, or gate pawn gravity/control ([VoxelWorld.cpp:988](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:988>)).
|
||||
|
||||
Streaming begins from `Tick`, and only when `GetPlayerPosition()` is not exactly `FVector::ZeroVector` ([VoxelWorld.cpp:1167](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:1167>), [VoxelWorld.cpp:1359](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:1359>)). Thus an exact world-origin pawn is indistinguishable from “no pawn.” Even when Z is nonzero and that specific sentinel is avoided, there is still no bootstrap barrier.
|
||||
|
||||
The supplied play trace shows:
|
||||
|
||||
- `BeginPlay` finished at about **50 ms**.
|
||||
- The first desired-tile pass did not begin until about **439 ms**.
|
||||
- The first LOD0 nonempty/mixed result was not ready until about **1.093 s**.
|
||||
- That result still represented mesh/collision submission, not collision-cook completion.
|
||||
|
||||
At ordinary gravity, one second is enough to fall roughly five metres.
|
||||
|
||||
There are three additional gaps:
|
||||
|
||||
- A tile is inserted into `LoadedTiles` before its empty/nonempty application path completes ([VoxelWorld.cpp:1532](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:1532>)).
|
||||
- Only LOD0 receives collision ([VoxelWorld.cpp:3109](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:3109>)). Coarser visible terrain can therefore be present while the corresponding collidable LOD0 tile is still queued.
|
||||
- Current architecture documentation says RMC collision is asynchronously cooked ([ARCHITECTURE.md:817](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:817>)), but `VoxelWorld` records no cook-complete state. Its `apply_s` measurement ends after submission and says nothing about when physics can actually collide.
|
||||
|
||||
I compared the equivalent paths on `main`. It also:
|
||||
|
||||
- Starts streaming from `Tick`.
|
||||
- Uses the zero-position guard.
|
||||
- Marks a tile loaded before handling its empty result.
|
||||
- Enables collision only for LOD0.
|
||||
- Has no spawn collision barrier or collision-cook acknowledgement.
|
||||
|
||||
So no explicit guarantee was removed inside `VoxelWorld`. What `experimental` broke was the **implicit timing budget** that had hidden this race.
|
||||
|
||||
The new origin-room geometry makes it worse. The first central LOD0 request in the trace is uniformly air and correctly receives no component. That is not itself an error: the player occupies air, while collision belongs to the neighboring floor/wall surface tile. With the current default origin geometry, the floor can lie near or below the lower tile boundary ([VoxelPassageGeometry.h:365](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelPassageGeometry.h:365>)). The supporting tile therefore has to arrive and finish cooking independently.
|
||||
|
||||
`CollisionOnlyTiles` does not close this gap. It controls presentation of already generated LOD0 components for non-player anchors; it does not synthesize or reserve collision underneath the local pawn. The supplied trace records zero collision-only tiles.
|
||||
|
||||
## 3. The mesher’s nested block classifier is probably self-inflicted overhead
|
||||
|
||||
**Status: Mechanism verified; net cost strongly suspected. High performance impact.**
|
||||
|
||||
The code contradicts its own guidance:
|
||||
|
||||
- The mesher header says the optional 8³ refinement should remain disabled during streaming because repeated proofs may cost more than they save ([VoxelMarchingCubesMesher.h:150](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h:150>)).
|
||||
- `VoxelWorld` unconditionally enables it ([VoxelWorld.cpp:1102](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:1102>)).
|
||||
|
||||
For every outer-Mixed tile, the mesher may run up to 64 additional `ClassifyTile` calls ([VoxelMarchingCubesMesher.cpp:254](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:254>)). On cache misses, each call builds another operator stack ([VoxelGenerator.cpp:5779](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:5779>)).
|
||||
|
||||
In `MyPlay`:
|
||||
|
||||
- 298 LOD0 tiles were outer-Mixed.
|
||||
- All 298 produced geometry.
|
||||
- Therefore every one eventually populated the full density grid.
|
||||
- The nested classifier could only avoid marching-cubes cell processing in uniform sub-blocks; it could not avoid the main density grid once any surface sub-block was found.
|
||||
|
||||
This is precisely the workload where the header warns it may lose.
|
||||
|
||||
There is also an instrumentation trap: the outer cache counters are snapped before meshing. Nested classifier calls occur inside the measured `mesh_s` interval ([VoxelWorld.cpp:2762](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:2762>)). Consequently, the reported “cache hits 0” does **not** describe classifier activity originating inside the mesher.
|
||||
|
||||
This deserves the first controlled A/B test: identical trace and seed with only `bUseBlockEarlyOut` changed, while separately counting outer and nested classifier calls.
|
||||
|
||||
## 4. The outer whole-tile classifier is probably earning its keep
|
||||
|
||||
**Status: Verified workload behavior; exact counterfactual unmeasured.**
|
||||
|
||||
I would not remove the outer classifier based on the 95 ms figure.
|
||||
|
||||
It rejected **547 of 845 LOD0 tiles**, or 64.7%, before meshing:
|
||||
|
||||
- 484 uniformly solid.
|
||||
- 63 uniformly air.
|
||||
- 298 mixed, all of which produced geometry.
|
||||
|
||||
A full 35³ grid for those rejected tiles would represent up to 23.45 million density positions. Previous history also shows that the outer classifier eliminated multi-second empty-tile fallbacks.
|
||||
|
||||
The 95 ms is not a clean CPU cost. It is elapsed wall time inside a `BackgroundNormal` worker task, so it includes preemption and contention. A second trace in the repository from the same current code has an LOD0 classifier median around **8.45 ms**, versus **94.7 ms** in the moving/rendering owner session. The real play number is important for latency, but it should not be interpreted as intrinsic classifier compute.
|
||||
|
||||
My assessment is:
|
||||
|
||||
- **Outer whole-tile classification:** valuable under the current expensive evaluator.
|
||||
- **Nested 8³ block classification:** likely negative in this workload.
|
||||
- **Long-term:** making density evaluation cheap again may change the outer classifier’s break-even point.
|
||||
|
||||
## 5. Movement does not cancel obsolete generation work
|
||||
|
||||
**Status: Verified mechanism; contribution to running falls strongly suspected.**
|
||||
|
||||
The abort predicate only notices shutdown or an explicit generation pause ([VoxelWorld.h:764](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelWorld.h:764>)). It does not abort a tile because the player has moved and the tile is no longer desired.
|
||||
|
||||
Desired work is globally distance-sorted using an artificial chunk-center position rather than the exact pawn position, with no strong collision-critical/L0 priority ([VoxelWorld.cpp:1864](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:1864>)). Up to 14 background tasks can therefore be occupied by old, coarse, or non-supporting work while the player’s new LOD0 floor waits.
|
||||
|
||||
By the steady point in the supplied session there were 1,481 loaded tiles for 899 currently desired tiles, with roughly 582 on the unload/transition side. Retaining old rendered tiles is not automatically wrong, but generation already launched for the old neighborhood is not preempted in favor of collision-critical tiles.
|
||||
|
||||
This is a credible explanation for falls while running. The trace cannot prove it because it omits tile coordinates, pawn position and collision-cook completion.
|
||||
|
||||
## 6. The verdict cache is mismatched to normal streaming
|
||||
|
||||
**Status: Implementation verified; negative startup ROI suspected. Medium impact.**
|
||||
|
||||
The cache:
|
||||
|
||||
- Is global and protected by one lock.
|
||||
- Advances a clock and linearly scans entries.
|
||||
- Holds only 512 proofs ([VoxelGenerator.cpp:1512](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:1512>)).
|
||||
- Keys include sample step, limiting reuse across LODs.
|
||||
- Does not store a useful outer proof for Mixed tiles.
|
||||
|
||||
Fresh streaming normally visits unique regions, which explains the zero outer hits in both supplied traces. Nested block classifiers can then insert many short-lived proofs, pay the lock/scan costs, and evict one another—without appearing in the trace’s cache telemetry.
|
||||
|
||||
It demonstrably helped a deliberately repeated commandlet case, but that access pattern is not representative of a moving player. I would treat it as unproven or likely negative during startup until nested calls are measured.
|
||||
|
||||
## 7. Field semantics are now implemented in several overlapping places
|
||||
|
||||
**Status: Verified duplication; cost not isolated. Medium impact and high maintenance risk.**
|
||||
|
||||
The effective field is no longer simply “the operator stack”:
|
||||
|
||||
1. The operator stack evaluates the composable field.
|
||||
2. The classifier maintains parallel interval/proof semantics.
|
||||
3. `GetDensityAt` performs a post-stack repair pipeline for origin floors, passage structure, passage floors and room floors.
|
||||
|
||||
For example, `FRoomGraphSource` evaluates the cached room graph SDF and then separately traverses tunnels again for metadata ([VoxelDensityOpStack.cpp:4234](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:4234>)). The common tail then reasserts origin and passage structure/floors ([VoxelGenerator.cpp:2244](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2244>)).
|
||||
|
||||
This is the clearest sign of overengineering: later correctness fixes are compensating for where an operation sits in the pipeline. It creates duplicated traversal, makes interval proofs harder to trust, and prevents the stack from being the single semantic source of truth.
|
||||
|
||||
Runtime also does not use the commandlet’s shared density-grid ownership, so adjacent tiles repeat halo/boundary samples. Earlier measurement put this duplication around 18%. It existed in the old architecture too, so it did not cause the original regression, but every repeated sample is now much more expensive.
|
||||
|
||||
## 8. There are two lower-confidence ways to get a permanent hole
|
||||
|
||||
**Status: Verified risk paths; not demonstrated by this trace.**
|
||||
|
||||
First, whole-tile AllAir/AllSolid verdicts are trusted without exact validation. They become loaded `bEmpty` tiles with no component. Internal block verdicts are later checked against the full grid when meshing occurs, but an outer uniform verdict has no equivalent safety net.
|
||||
|
||||
The supplied aggregate argues against a widespread problem—every LOD0 Mixed tile produced geometry—but it cannot establish whether the floor tile beneath the pawn was incorrectly declared uniform because tile coordinates were not logged.
|
||||
|
||||
Second, `ApplyMeshToTile` has no success return. If runtime-mesh initialization fails, the function returns while the tile remains recorded as loaded, so it will not naturally retry ([VoxelWorld.cpp:3064](<E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:3064>)). I found no trace evidence that this occurred.
|
||||
|
||||
The normal `bAborted` result path is not the likely culprit: it removes the pending record without marking the tile loaded, allowing a later desired pass to reschedule it. There is, however, an editor pause corner case where a worker can exit without enqueuing any result and leave its pending entry behind if pause acquisition times out. That is a real bug, but it does not match this epoch-zero play session.
|
||||
|
||||
## What I would prove next
|
||||
|
||||
In order:
|
||||
|
||||
1. A/B the forced nested block classifier on the exact owner trace, recording outer versus nested calls, proof outcomes, stack builds and cache probes.
|
||||
2. Add collision-specific timestamps: requested, mesh submitted, collision cook started/completed, component registered, plus tile and pawn coordinates.
|
||||
3. Force exact meshing/validation for every supposedly uniform LOD0 tile in the spawn neighborhood to rule a false classifier proof in or out.
|
||||
4. Benchmark a compiled/fused evaluator that retains the authored operator graph but restores per-sample short-circuiting and avoids spilling `FVoxelOpSample` through memory for every operator.
|
||||
5. Prioritize a small LOD0 support neighborhood and require collision-cook completion before releasing the pawn. Faster generation alone cannot make the current lifecycle correct.
|
||||
|
||||
The blunt version: **the operator abstraction leaked into the innermost loop, and streaming treats “work submitted/applied” as “safe to stand on.”** The classifier work then became a collection of compensators for the expensive evaluator, some useful and some apparently duplicative.
|
||||
|
||||
This was a read-only audit. No files were created, modified or deleted; `git status` remains clean on `experimental` at `aa62971`.
|
||||
@@ -0,0 +1,129 @@
|
||||
# VoxelForge Task 1: profiler, batch explorer, and research handoff
|
||||
|
||||
Research date: 2026-09-09. This note records the implementation and the read-only web research for Task 1. No RealtimeMeshComponent files were changed.
|
||||
|
||||
## 1. Measurement result
|
||||
|
||||
The old result in the task brief was 14.74 us/sample on the profiled density path versus 1.28 us/voxel without instrumentation: about 11.5x apparent overhead. That is consistent with putting `FPlatformTime::Cycles64()` around a roughly 100 ns operation. The timer call is not free, and at that granularity the measurement is mostly the measurement.
|
||||
|
||||
The new default is sampled profiling:
|
||||
|
||||
- Coarse calls and counters stay cheap and are collected in thread-local state.
|
||||
- `GetDensityAt` uses a deterministic count interval of 64 calls and one amortized block timer, rather than one timer pair per density call.
|
||||
- Full cycle timing remains an explicit opt-in (`-profiledensityfull`) for a small investigation, not the normal batch mode.
|
||||
- The default sampled mode reports the parent density/mesher wall estimate. Fine per-operation scopes are disabled in that mode; use Full or Unreal Insights for a detailed phase breakdown.
|
||||
- Sampling and timers only write report counters. They do not choose cells, alter density values, alter task partitioning, or participate in any reduction, so they cannot affect generated geometry.
|
||||
|
||||
The timer calibration in the validation run measured one `Cycles64` pair as approximately one platform cycle, or 0.1 us at the reported clock conversion. That is the direct self-check for the suspected timer-dominates-work failure mode.
|
||||
|
||||
Validation on the same 64^3 `TunnelNetwork` export:
|
||||
|
||||
| measurement | seconds |
|
||||
| --- | ---: |
|
||||
| profiled sampled export | 0.4630500004 |
|
||||
| fresh unprofiled reference export | 0.5584703982 |
|
||||
| discrepancy | -17.09% |
|
||||
| profiled/reference ratio | 0.82914 |
|
||||
|
||||
Both exports produced geometry hash `235AC2DC`. The commandlet now puts all four values in every profiled report and fails with exit code 2 when the absolute discrepancy exceeds 20% or the reference hash differs. The report also exposes `cpu_us` (sum of worker CPU estimates) separately from `wall_us`/`total_us` (largest-worker wall estimate). Summing CPU time across 15 workers is not a valid wall-clock parent; collapsing those fields was one way to make a parallel profile lie.
|
||||
|
||||
The density report has an explicit ledger. In sampled mode, the parent estimate is the measured quantity, fine phase buckets are marked unavailable, and the unattributed remainder makes the ledger sum exactly to the parent. This is honest about what was measured instead of inventing a fine breakdown from disabled timers.
|
||||
|
||||
## 2. Batch explorer
|
||||
|
||||
The explorer commandlet accepts one JSON file and keeps one editor process alive for every case:
|
||||
|
||||
```text
|
||||
UnrealEditor-Cmd.exe "<HostProject>.uproject" -run=VoxelForgeExplore -batch="E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BatchValidation.json" -out="E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BatchValidationRun2" -trace=cpu,counters
|
||||
```
|
||||
|
||||
`-out` must be an absolute directory below this plugin's `Saved` directory. The commandlet writes one `batch.json` below that directory. The input supports, per case: `id`, `seed`, `archetype`, `slot`, `export_size`, `export_step`/`step`/`lod`, `modes`, `operator_stack`, `profile_density`, `profile_density_full`, `profile_lod`, render/failure-focus settings, and a per-case budget. The parser caps one batch at 256 cases and clamps the total budget to 30 minutes; each case also has a bounded budget.
|
||||
|
||||
Before every case, the commandlet resets the density profile and all per-case report state, then applies the case's profile settings. The output contains per case: mesh seconds, microseconds per voxel, triangle count, geometry hash, counters, profile mode, and the case exit code. Invalid or timed-out cases are represented in the same machine-readable output rather than silently disappearing.
|
||||
|
||||
The final three-case confirmation completed in 6.59 seconds of commandlet case time after the single process launch:
|
||||
|
||||
| case | triangles | mesh seconds | hash | result |
|
||||
| --- | ---: | ---: | --- | --- |
|
||||
| repeat_unprofiled | 2,461 | 0.391420 | `235AC2DC` | pass |
|
||||
| repeat_profiled | 2,461 | 0.515374 | `235AC2DC` | self-check pass |
|
||||
| maze_lod1 | 5,218 | 0.277830 | `784EBF92` | pass |
|
||||
|
||||
The standalone profiled 64^3 case and the `repeat_profiled` case inside the batch both produced `235AC2DC`, 2,461 triangles, 343,000 tunnel lookups, and 554,381 passage evaluations. That is the contamination check: same final build, seed, archetype, slot, export size, step, and modes produced the same geometry and coarse counters alone and in a reused process. The standalone profiled export also matched its fresh unprofiled reference hash.
|
||||
|
||||
## 3. Cache footprint answer
|
||||
|
||||
The historical 15-worker report that prompted the question was:
|
||||
|
||||
```text
|
||||
capacity_entries=3840 valid_entries=1542 static_bytes=2534400
|
||||
dynamic_bytes=428627716 total_bytes=431162116
|
||||
avg_valid_entry_bytes=278703.6 largest_entry_bytes=541344
|
||||
tunnel_valid=1054 tunnel_dynamic=345243636
|
||||
roomgraph_valid=488 roomgraph_dynamic=83384080
|
||||
```
|
||||
|
||||
That is 431,162,116 bytes (411.2 MiB) in aggregate and 278,703.6 bytes per valid entry on average (278.7 kB decimal, about 265.7 KiB). The number is not one monolithic 272 kB struct. Each worker owns cache slots, and valid entries contain variable-length arrays.
|
||||
|
||||
A representative 128^3 breakdown added by this task was 360,063,124 bytes total:
|
||||
|
||||
| member family | tunnel cache | room-graph cache |
|
||||
| --- | ---: | ---: |
|
||||
| slot storage | 1,658,880 | 875,520 |
|
||||
| op stack | 8,264,000 | 0 |
|
||||
| rooms | 47,876,912 | 31,822,512 |
|
||||
| room-floor joins | 4,936,512 | 3,278,976 |
|
||||
| tunnels and control arrays | 5,560,248 | 3,591,584 |
|
||||
| support-column entries | 7,153,328 | 0 |
|
||||
| support columns | 30,699,008 | 0 |
|
||||
| room index bins + candidates | 30,759,720 | 22,700,380 |
|
||||
| tunnel index bins + candidates | 60,972,480 | 41,583,988 |
|
||||
| tunnel-world index bins + candidates | 34,635,272 | 23,693,804 |
|
||||
| dynamic total | 230,857,480 | 126,671,244 |
|
||||
| static + dynamic total | 232,516,360 | 127,546,764 |
|
||||
|
||||
The dominant allocation is the spatial index bin/candidate storage, followed by rooms and support-column arrays. Slot storage is comparatively small. The exact per-entry average moves with the generated case and cache occupancy, which explains why the 128^3 confirmation is not numerically identical to the historical 411 MiB run.
|
||||
|
||||
## 4. Research findings
|
||||
|
||||
### RealtimeMeshComponent, read-only
|
||||
|
||||
At research time the upstream releases page lists v5.3.2 as the latest tagged release: [RMC releases](https://github.com/TriAxis-Games/RealtimeMeshComponent/releases). The disposable build host's copy identifies itself as `VersionName: 5.4`, so the upstream tag number is not a safe linear comparison; this project also carries custom changes. The visible upstream history includes a 5.1 game-thread/render-thread restructuring and a 5.0.5 release mentioning mesh-update and memory-overuse fixes. The public release page does not provide a complete API diff against this custom 5.4 copy, so I am treating those as compatibility signals rather than promising source compatibility. The local copy was not upgraded, diffed destructively, or otherwise modified.
|
||||
|
||||
The current upstream guidance is to build an `FRealtimeMeshStreamSet` as a CPU-side value on a worker, then cross to the game thread to create/update the component section group; use a weak object pointer for the handoff. The structure documentation describes a game/render proxy mirror and distinguishes in-place dynamic/compute updates from static proxy recreation. See [updating mesh data](https://triaxis.games/realtime-mesh/docs/component-core/updating-mesh-data/) and [RMC component structure](https://triaxis.games/realtime-mesh/docs/component-core/structure/). The practical candidate is therefore worker-side stream construction plus the smallest possible section/buffer update, not touching a `URealtimeMeshComponent` from a worker. Custom RMC changes make this a high-risk follow-up.
|
||||
|
||||
### UE 5.7 tasking and tracing
|
||||
|
||||
Epic's UE 5.7 API documentation says `ParallelForWithExistingTaskContext` gives each spawned task a task-local workspace, and `MinBatchSize` limits launches to `DivUp(Num, MinBatchSize)` workers. The pre-work variant runs caller work before that thread helps the parallel loop. Both pages explicitly warn against clogging the task graph with long-running or blocking work: [existing task context](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Core/ParallelForWithExistingTaskConte-) and [pre-work task context](https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Core/ParallelForWithPreWorkWithTaskCo-). The next tuning target should be measured batch size and task-local scratch reuse for short density/cell jobs, not more tiny tasks.
|
||||
|
||||
UE's built-in tracing is the better fine-grained instrument. The documented channels include CPU, Counters, Task, and StackSampling; `-trace=cpu,counters` is a valid launch selection. `TRACE_CPUPROFILER_EVENT_SCOPE` with static scope names and `CountersTrace.h`/`TRACE_COUNTER_*` are the low-cost native hooks. See [Trace channels](https://dev.epicgames.com/documentation/en-us/unreal-engine/trace-in-unreal-engine-5), [the tracing guide](https://dev.epicgames.com/documentation/en-us/unreal-engine/developer-guide-to-tracing-in-unreal-engine?lang=en-US), and the [Unreal Insights reference](https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-insights-reference-in-unreal-engine-5). Timing Insights supplies timers, counters, callers/callees, and task execution views; Task Graph Insights exposes execution paths and critical paths: [Timing Insights](https://dev.epicgames.com/documentation/en-us/unreal-engine/timing-insights-in-unreal-engine?lang=en-US) and [Task Graph Insights](https://dev.epicgames.com/documentation/unreal-engine/task-graph-insights-in-unreal-engine-5?lang=en-US).
|
||||
|
||||
This task therefore keeps the lightweight sampled report for automated self-checks and adds native CPU scopes/counters for investigation. Insights should own future fine per-scope and task scheduling analysis; the hand-rolled profiler should not grow back into a timer around every density call.
|
||||
|
||||
### Marching cubes and density generation
|
||||
|
||||
The upstream [Transvoxel overview](https://transvoxel.org/) confirms that transition cells use local voxel data and a bounded lookup table to stitch different-resolution meshes without cracks. It is relevant to LOD boundary work and dynamic retriangulation, not a reason to change the current mesh in this task. A classic SIMD study reports nearly 4x over an unoptimized serial Marching Cubes implementation and about 2x over compiler-optimized C: [Newman et al., *High performance SIMD marching cubes*](https://www.sciencedirect.com/science/article/abs/pii/S0097849303002760). Those are historical results on older x86 and should be treated as an upper-bound signal, not a VoxelForge forecast.
|
||||
|
||||
## 5. Ranked next-task candidates (not implemented here)
|
||||
|
||||
The magnitude estimates are expected ranges for the affected stage, inferred from the sources and this run's counters; they are not promises for end-to-end time.
|
||||
|
||||
| rank | candidate | expected magnitude | risk |
|
||||
| ---: | --- | --- | --- |
|
||||
| 1 | Conservative macrocell/cell empty-space early-out using deterministic density bounds, with a fallback on boundary uncertainty | 2–10x on empty or mostly empty chunks; smaller on surface-heavy chunks | Medium-high: bounds must never skip a crossing cell or change traversal order |
|
||||
| 2 | Reuse one canonical density grid/field across meshing, LOD, export, and renderer preparation; invalidate by seed/slot/parameters | 20–60% less repeated density work and memory bandwidth where consumers overlap | Medium: cache lifetime and invalidation |
|
||||
| 3 | Tune `ParallelFor` granularity and task-local scratch for short jobs; measure `MinBatchSize`, worker count, and critical path in Insights | 5–25% less scheduling/coordination cost | Low-medium: load imbalance and contention can reverse the win |
|
||||
| 4 | SIMD/SoA batches for density and Marching Cubes table work | 1.5–4x for the vectorized stage in favorable workloads | High: exact cross-machine geometry and floating-point contraction; no runtime CPU dispatch is allowed |
|
||||
| 5 | Hierarchical sparse/occupancy metadata or distance bounds to skip whole regions before field evaluation | 2–20x in sparse worlds | High: build/update cost, conservative topology, and invalidation |
|
||||
| 6 | RMC partial section/buffer updates after worker-side stream construction | 10–50% less renderer update/bandwidth cost when only part of a section changes | High: custom RMC modifications and game/render-thread ownership |
|
||||
| 7 | Transvoxel transition cells for LOD boundaries | 10–30% less boundary patch/rebuild work in LOD-heavy scenes; also removes crack workarounds | Medium-high: transition topology and hash stability |
|
||||
|
||||
The first safe experiment should be the deterministic early-out or field reuse, after a baseline with the corrected profiler. SIMD is deliberately below those despite its attractive paper result because the project's cross-machine geometry requirement is absolute.
|
||||
|
||||
## 6. Pushback and constraints
|
||||
|
||||
The requirement that buckets sum to their parent needs a definition in a parallel program. Raw worker CPU time can exceed wall time by the worker count; the report now exposes both and makes the wall estimate the parent used for the self-check. A fine raw-bucket sum cannot be claimed when sampled mode intentionally does not time those scopes, so the report says that explicitly and accounts for the remainder.
|
||||
|
||||
The 20% self-check threshold is a practical alarm, not a proof of zero measurement cost: OS scheduling, cache state, DDC state, and editor startup can move two exports. The geometry hash check is exact; repeated cases or a median of several runs should be used when a performance regression is close to the threshold.
|
||||
|
||||
The sampling decision is non-deterministic only in its elapsed-time observation. It is isolated from the generation inputs and outputs. The batch commandlet resets profile/cache-report state per case, but it intentionally reuses the engine process to remove startup cost; the standalone-versus-batch geometry hash is the proof that this reuse did not contaminate the tested case.
|
||||
@@ -0,0 +1,140 @@
|
||||
# Task 2 performance report
|
||||
|
||||
Date: 2026-09-09
|
||||
Repository: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge`
|
||||
Work began at `experimental` / `e63f016`.
|
||||
|
||||
## Measurement rule
|
||||
|
||||
All performance numbers below are per-case `run.elapsed_seconds` from the batch report. This is
|
||||
the wall clock for a real commandlet case through the export or render result; it includes the
|
||||
shared-density-grid build when enabled. `summary.mesh_seconds` and profiler buckets are included
|
||||
only as diagnostics, never as the performance claim.
|
||||
|
||||
The export comparison is the same `128^3` `TunnelNetwork`, seed `1337`, slot `1`, operator stack,
|
||||
step `1`, with three cases in one commandlet process on each side. The 17% reporting floor is
|
||||
therefore met by both run count and the retained density-grid result.
|
||||
|
||||
## Part 1 — block early-out
|
||||
|
||||
I tested an `8^3` block reject behind conservative fallback logic. On this cave/stack world, every
|
||||
one of the 4,096 blocks was left uncertain and went through normal sampling:
|
||||
|
||||
| batch | runs | end-to-end wall seconds | blocks skipped | skip rate | result |
|
||||
| --- | ---: | --- | ---: | ---: | --- |
|
||||
| baseline | 3 | 3.805057, 3.522129, 3.569186 (avg 3.632124) | — | — | reference |
|
||||
| block reject | 3 | 6.341454, 6.562621, 7.300234 (avg 6.734770) | 0 / 4,096 | 0.00% | **reverted** |
|
||||
|
||||
The candidate added classification work without skipping a block (+85.42% wall clock). It also
|
||||
did not produce a useful supremum for the complete cave stack, so I did not promote a guessed
|
||||
`max` parameter into a topology decision. `ClassifyTile` remains untouched: its stack, disturbance,
|
||||
diff-layer, and cave guards are not a safe substitute for a new block proof. A false AllSolid
|
||||
verdict here would delete a 3D cave, which is not an acceptable trade.
|
||||
|
||||
This did not clear the floor and was reverted.
|
||||
|
||||
## Part 2 — density-grid reuse
|
||||
|
||||
The canonical mesher requests 35^3 samples per 32-cell tile. For 64 tiles that is:
|
||||
|
||||
- tile requests: `64 * 35^3 = 2,744,000`;
|
||||
- unique points in the expanded 128-cell lattice: `131^3 = 2,248,091`;
|
||||
- duplicate evaluations: `495,909` (18.07%).
|
||||
|
||||
The first implementation evaluated the shared lattice as Z planes. It removed all duplicates but
|
||||
was slower because it destroyed the generator's spatial locality:
|
||||
|
||||
| candidate | runs | end-to-end wall seconds | duplicates | result |
|
||||
| --- | ---: | --- | --- | --- |
|
||||
| matched control | 3 | 3.668954, 3.633445, 3.655658 (avg 3.652686) | 495,909 → 495,909 | reference |
|
||||
| global-plane precompute | 3 | 5.349520, 5.352927, 5.510752 (avg 5.404400) | 495,909 → 0 | **reverted** |
|
||||
| owner-grid precompute | 3 | 1.950207, 1.973673, 2.161305 (avg 2.028395) | 495,909 → 0 | retained candidate |
|
||||
|
||||
The retained implementation partitions the shared lattice into 64 disjoint rectangular owner
|
||||
regions. Each global sample is generated exactly once, then each tile copies its local halo window
|
||||
from the immutable run-scoped grid. The precompute is deliberately inside the wall-clock timer;
|
||||
the final three-case repeat measured:
|
||||
|
||||
| batch | runs | end-to-end wall seconds | average |
|
||||
| --- | ---: | --- | ---: |
|
||||
| final control (`density_grid_reuse=false`) | 3 | 3.659587, 3.619013, 3.582257 | 3.620286 s |
|
||||
| final reuse (`density_grid_reuse=true`) | 3 | 1.908017, 1.854344, 1.884170 | 1.882177 s |
|
||||
|
||||
That is a **48.01% wall-clock reduction / 1.92x speedup**, clearing the 17% floor. The final
|
||||
reuse runs averaged 1.474271 s for the shared-grid build, and still won end to end. The 128^3
|
||||
shared lattice is about 8.6 MiB of float storage. Direct explorer runs default to reuse; a batch
|
||||
case can pass `density_grid_reuse=false` for a matched control.
|
||||
|
||||
The run-scoped handoff is confined to the explorer's finite canonical export. The streaming game
|
||||
path has no one-shot set of neighboring tiles or invalidation boundary, so it was not given a
|
||||
locking plane cache that could serialize workers or outlive a generation epoch.
|
||||
|
||||
## Part 3 — task granularity
|
||||
|
||||
The final control observed 15 worker thread IDs and 64 logical tile invocations. Across its three
|
||||
runs, the measured tile-job averages were:
|
||||
|
||||
| metric | average |
|
||||
| --- | ---: |
|
||||
| fastest logical tile job | 0.188817 s |
|
||||
| mean logical tile job | 0.593751 s |
|
||||
| slowest logical tile job | 0.955788 s |
|
||||
| sum of tile-job durations | 38.000045 s |
|
||||
| tile parallel wall | 3.150925 s |
|
||||
| launch-fringe estimate | 14.1 us |
|
||||
| workers / logical jobs | 15 / 64 |
|
||||
|
||||
I tested `MinBatchSize=4` with three matched cases: wall seconds `3.590884, 3.634734,
|
||||
3.753986` (avg `3.659868`) versus the final control average `3.620286`. The tile parallel bucket
|
||||
was slightly lower (`3.125197` s vs `3.150925` s), but end-to-end wall clock was **1.09% slower**.
|
||||
The observed worker count and logical job count stayed 15 / 64, and the launch-fringe estimate
|
||||
remained about 13.1 us. This is the required bucket-win/no-wall-win case: the tuning was reverted;
|
||||
the production/default setting remains `MinBatchSize=1`.
|
||||
|
||||
`ParallelForWithExistingTaskContext` was not adopted. The measured launch fringe is negligible
|
||||
relative to 0.59 s tile work, and the mesher already uses thread-local scratch storage. No claim is
|
||||
made here about the separate game workload of many small streaming submissions.
|
||||
|
||||
## Part 4 — renderer
|
||||
|
||||
No renderer rewrite or RMC change was justified. The source audit and three final render cases
|
||||
showed:
|
||||
|
||||
- the canonical mesh is built once per explorer world;
|
||||
- the triangle cache/BVH is built once and reused by all eight viewpoints;
|
||||
- each viewpoint owns one `FColor` output allocation;
|
||||
- the per-pixel traversal uses a fixed stack array, not a heap allocation;
|
||||
- camera movement does not call the mesher again.
|
||||
|
||||
The three 512x288 render cases had total case wall seconds `8.704730, 8.985744, 8.517606`
|
||||
(avg `8.736027`), including camera-seed walking. Raster-only diagnostics averaged 1.482921 s
|
||||
for eight views, 0.286297 s for the first view, 0.170946 s per additional view, and 0.083190 s
|
||||
for one-time acceleration construction. This is consistent with the owner's approximately
|
||||
0.15 s/view observation; meshing benefits from Part 2 automatically. There was no renderer
|
||||
change, so there is no before/after performance claim to report or keep.
|
||||
|
||||
## Memory/index follow-up
|
||||
|
||||
The existing profiler research measured 431,162,116 bytes (411.2 MiB) across 15 workers in the
|
||||
large run. In the representative 128^3 breakdown, spatial-index bins and candidate arrays alone
|
||||
accounted for 214,345,644 bytes, inside a 360,063,124-byte cache total. The index addition did not
|
||||
show an end-to-end wall-clock gain in the prior measurements.
|
||||
|
||||
I did not remove it silently. The explicit follow-up proposal is an index-on/index-off batch
|
||||
ablation before deletion: removing it should recover roughly that allocation, at the cost of
|
||||
broader room/tunnel candidate scans and potentially slower density queries. That trade is not
|
||||
proven by this task's export comparison.
|
||||
|
||||
## Determinism, invariants, and scope
|
||||
|
||||
All three final control hashes and all three final reuse hashes are `ED0F1F31`; every case reported
|
||||
`json_deterministic=yes`. The three render cases also repeated the same geometry hash
|
||||
`847BE4B9`. Owner regions are disjoint, tile results are merged in fixed Z/Y/X order, no RNG or
|
||||
runtime CPU-feature dispatch was added, and the shared grid is read-only after its deterministic
|
||||
fill. The only worker writes are to distinct cells of that run-owned output lattice; generator and
|
||||
world state remain read-only.
|
||||
|
||||
The build succeeded with UnrealBuildTool after the final source change. Changes are limited to the
|
||||
VoxelForge source and this report, with generated evidence under the plugin's `Saved` directory.
|
||||
RealtimeMeshComponent was not modified. No VoxelM root, sibling plugin, `Content`, or `Config`
|
||||
files were written; no links or historical worktrees were created.
|
||||
@@ -0,0 +1,887 @@
|
||||
# DESIGN-SOL-2026-09-15-CODE: round two, Sol reads the code
|
||||
|
||||
Continues `DESIGN-SOL-2026-09-15.md`. At the owner's request, Sol's round-one session was forked
|
||||
(`codex exec fork`, `gpt-5.6-sol`, reasoning high, read-only sandbox, ~594k tokens). Sol was given
|
||||
the repository and asked **what could reasonably change and what it would take to prove each change
|
||||
worth it**. Advice only: no files were changed, nothing was built or run. The owner's stance: *"open
|
||||
to big changes, but only if they prove themselves worthy."*
|
||||
|
||||
## Orchestrator verification (2026-09-15, against HEAD `cf9904e` and the UE 5.7 engine source)
|
||||
Checked by reading, not by running anything.
|
||||
|
||||
| Sol's claim | Check |
|
||||
|---|---|
|
||||
| `FMath::Sin/Cos` reach the MSVC CRT on Win64 | **Verified.** `GenericPlatformMath.h:521/530` returns `sinf`/`cosf`; `FMicrosoftPlatformMathBase` (via `TUnrealPlatformMathSSE4Base`) and the SSE bases override neither. |
|
||||
| Per-sample lookup machinery is ~5% exclusive, not the 27% inclusive figure | **Verified**, `PERF-SAMPLED.md` lines 11-13: `GetRange` 3.8%, `VF_ForEachSpatialCandidate` 1.2%. My round-one note "the largest gap is the loop direction" overstated it. |
|
||||
| Parse-once switches are unsynchronised globals parsed on worker threads | **Verified**, `VoxelGenerator.cpp` (`VF_Parse*Switch` called in `GetDensityAt`). In practice every racer writes the same value; the real exposure is a live CVar change mid-generation. |
|
||||
| `MemCrc32` over raw param structs; the comment says no false positive is possible | **Verified** (`VoxelGenerator.cpp` ~3165). A CRC32 collision is possible in principle (about 2^-32 per comparison); padding can only cause a spurious rebuild. Low probability. |
|
||||
| Mesher dedups vertices by rounding position x100 | **Verified** (`VoxelMarchingCubesMesher.cpp` `GetOrCreateVertex`). Only amplifies an upstream numeric difference; not an independent source of divergence. |
|
||||
| ARCHITECTURE says every landing gets a connector to (0,0); the code says a source-fit landing stays local | **Verified contradiction**: `ARCHITECTURE.md` ~518 vs `VoxelStrateManager.cpp` ~2108. One of them is wrong. |
|
||||
| Lateral-region audit 4/16 | **Verified**, `ARCHITECTURE.md` ~122. That mechanism is explicitly "not ready to promote", so it is not live terrain. |
|
||||
| Layout shuffle uses a stateful `FRandomStream` | **Verified** (`VoxelStrateManager.cpp` ~1155), after a deterministic sort. Deterministic; only order-coupled when content is added. |
|
||||
| Coarse LOD derived from fine data would break the clipmap invariant | Agreed (`ARCHITECTURE.md` ~708). My round-one table wrongly listed it as a gap. |
|
||||
| Noise lattice float-to-int, MXCSR rounding mode, multiplayer edit ordering | Not re-checked in detail; recorded as Sol's claims. Edit ordering matters once networking exists. |
|
||||
|
||||
---
|
||||
|
||||
## The prompt, as sent
|
||||
|
||||
# Phase two: now you get the real code
|
||||
|
||||
Thank you for the first answer. It was the most useful design input this project has had in a
|
||||
while. The owner read it and his reaction was: **"what Sol proposed can be nice. I am open to big
|
||||
changes, but only if they prove themselves worthy."** That is the question now: **what could reasonably be changed in this code, and what would it take to
|
||||
prove each change worth it?**
|
||||
|
||||
**The earlier rule is lifted: you may now read and search files.** This remains a read-only
|
||||
consultation:
|
||||
- Reading commands only (`rg`, `Get-Content`, `git log/show/diff`). Do not write or edit any file.
|
||||
Do not build, compile, run tests, or launch Unreal. A single test run here takes up to an hour,
|
||||
and the machine is the owner's.
|
||||
- Use absolute paths; your shell's working directory can drift.
|
||||
- Your final message is the whole deliverable. Please list the files you actually read at the end.
|
||||
|
||||
## The code
|
||||
Plugin root: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge` (git branch `experimental`, HEAD `cf9904e`).
|
||||
Start with these, reading targeted sections rather than whole files; some are over 100 KB:
|
||||
- `CLAUDE.md` (rules), then `CODEMAP.md` (§2 data flow, §3 symbol→file index, §6 gotchas).
|
||||
- `ARCHITECTURE.md` §8, especially **§8.10, the performance invariants** that exist for measured
|
||||
reasons.
|
||||
- `WORK-NEXT.md`, section "State after 2026-09-15" and "Open design questions for the owner".
|
||||
- `PERF-SAMPLED.md`, section "Current-tip cost re-rank — 2026-09-15 (1d824aa)" (from line ~1669):
|
||||
the measured cost map. Two later commits cut a further ~12% (`a668833`, reach gating) and ~4%
|
||||
(`410788f`, per-chunk routing cache).
|
||||
- `DESIGN-SOL-2026-09-15.md`: your first answer, plus **my** mapping of it onto this code (the
|
||||
"Orchestrator notes"). I wrote that mapping from memory of the code; check it and correct me.
|
||||
- `AUDIT-SOL-2026-09-11.md` and `AUDIT-COLD-2026-09-14.md`: earlier read-only audits, so you don't
|
||||
repeat them.
|
||||
- Hot code: `Source/VoxelForge/Private/VoxelGenerator.cpp` (`GetDensityAt`,
|
||||
`GetDensityWithParams`), `VoxelCaveMorphology.cpp` (tunnel SDF, world-space tunnel core, player-fit
|
||||
memo), `VoxelDensityOpStack.cpp` (operator stack, fused evaluator), `VoxelStrateManager.cpp`
|
||||
(strates, passages), the mesher / tile scheduling, and `VoxelForge.Build.cs` (`/fp:precise` is
|
||||
already set).
|
||||
|
||||
## What is measured (don't re-derive; do challenge it if the code says otherwise)
|
||||
- In-game worker CPU for the standard static scenario is ~27.6 s, down from ~184 s at the start of
|
||||
this push, with the same world and the same capability. Measured on the headless game path with a
|
||||
stack sampler plus remove-one ablation.
|
||||
- **Tried and rejected, please don't re-propose these without new evidence:**
|
||||
- Merging the warped tunnel SDF with the world-space tunnel core into one pass: the world core is
|
||||
load-bearing (its control points keep authored floor anchors before warp). Replacing it lost
|
||||
1,146 player-fit and 975 walk-reachable cells.
|
||||
- An always-on exact per-sample cull: +23% slower.
|
||||
- Cheaper value-noise worms: no gain. Worms off entirely saves only ~3%.
|
||||
- The acceptance rules, which any proposal must live with:
|
||||
- **Determinism:** same build + same seed = same world on every machine. Bit-identity with
|
||||
*older* builds is NOT required, so a change that alters the field is allowed. Classifiers and
|
||||
skip proofs must never change the field.
|
||||
- **Capability:** the existing gate (player-fit 20,830 / walk-reachable 10,909 cells, connected,
|
||||
on the canonical scenario) must not regress.
|
||||
- **Performance:** judged on the headless GAME path, static and moving, comparing the final
|
||||
default build against the committed-tip DLL. A switch-off A/B inside the new build does not
|
||||
count.
|
||||
- The operator stack and the composer stay as the authoring language: worlds are composed, not
|
||||
hand-built.
|
||||
|
||||
## What we would like from you
|
||||
1. **Correct my mapping.** For each row of the table in `DESIGN-SOL-2026-09-15.md`, say whether it
|
||||
is right, and give file:symbol evidence where it is wrong.
|
||||
2. **Your ideas against this code.** For each idea from your first answer that survives contact with
|
||||
the code:
|
||||
- where it plugs in (files, symbols) and what it replaces or keeps;
|
||||
- its rough size;
|
||||
- its risks to capability, determinism, the composer and the §8.10 invariants;
|
||||
- its expected gain, grounded in the measured cost map. Say what share of the ~27.6 s it could
|
||||
touch, and mark each number measured or inferred.
|
||||
3. **Rank them.** Then, for the top one or two big changes, design a **proof-of-worth ladder**:
|
||||
- Rung 1: the cheapest experiment that could kill the idea early. Ideally instrumentation only,
|
||||
e.g. how much time goes to per-sample candidate search, and how many candidates a sample sees.
|
||||
- Rung 2: a prototype scoped to one feature type, behind a console switch.
|
||||
- Rung 3: full adoption.
|
||||
Each rung needs an explicit, falsifiable kill criterion, and the numbers that would justify going
|
||||
on. The owner wants big changes to earn their place, so make the early rungs cheap and honest.
|
||||
4. **Determinism audit of numerics on the world-deciding path.** We build with `/fp:precise`.
|
||||
Enumerate the remaining cross-machine hazards with file:line:
|
||||
- CRT transcendentals (`sin/cos/pow/exp/log/atan2/...`, including through `FMath`);
|
||||
- any runtime CPU-feature dispatch (SIMD paths chosen at runtime, ISPC);
|
||||
- float-to-int conversions at boundaries;
|
||||
- order-dependent accumulation across threads, uninitialised data, timing dependence.
|
||||
Give severity and the minimal fix for each. In particular: on UE 5.7 Win64, do `FMath::Sin/Cos`
|
||||
end up in the MSVC CRT (whose FMA3 variant is chosen at runtime), or in engine code? If engine
|
||||
source is not reachable from here, say so and say what you would check.
|
||||
5. **What not to graft here**, and why.
|
||||
|
||||
This is advice only. Nothing will be changed as a result of this run until the owner has read your
|
||||
answer and decided, so don't write code or patches. Describe what could reasonably change, and what
|
||||
it would take.
|
||||
|
||||
Push back wherever the code shows my framing or my mapping is wrong. That is worth more than
|
||||
agreement. Say plainly where you are unsure.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Sol's answer, verbatim
|
||||
|
||||
# Executive conclusion
|
||||
|
||||
I would not replace VoxelForge’s representation, mesher, streaming model, or operator authoring language wholesale. The current design is already much closer to the “lazy compiled field” architecture from the first answer than the orchestrator notes imply, and the measured reduction from ~184 s to ~27.6 s shows that its major structural choices are viable.
|
||||
|
||||
Three changes remain worth serious consideration:
|
||||
|
||||
1. A tile-wide, feature-major lowering of the canonical tunnel evaluator. This is the only remaining architectural change with a plausible 8–18% total worker-CPU gain, but it has not yet earned implementation.
|
||||
2. Gameplay reuse of exact density samples across adjacent tiles. The existing mesher already has most of the consumer-side plumbing, and the theoretical duplicate-sample ceiling is surprisingly large.
|
||||
3. A canonical numeric ABI for world-deciding math. This is primarily correctness, not performance: `/fp:precise` does not currently close the determinism requirement.
|
||||
|
||||
I would also harden the traversal contract. The canonical capability gate passes, but the code and documentation do not support the stronger claim that every composed world has a structurally guaranteed entry-to-exit route.
|
||||
|
||||
All performance percentages below distinguish:
|
||||
|
||||
- **Measured:** directly present in the supplied profiles or commit evidence.
|
||||
- **Inferred:** derived from those measurements; not yet benchmarked.
|
||||
|
||||
---
|
||||
|
||||
# 1. Correction of the orchestrator mapping
|
||||
|
||||
There is no table in `DESIGN-SOL-2026-09-15.md` at this HEAD. There are three mapping bullets at [DESIGN-SOL-2026-09-15.md:9](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/DESIGN-SOL-2026-09-15.md:9>). I have treated those as the requested rows.
|
||||
|
||||
| Orchestrator note | Verdict | Correction |
|
||||
|---|---|---|
|
||||
| Determinism risk from `FMath::Sin/Cos` and UCRT FMA3 dispatch | **Substantively correct; count is stale/ambiguous** | The risk is real and `/fp:precise` does not fix library implementations. The current source contains 42 direct `Sin/Cos` invocations in terrain generation if legacy and fused implementations are both counted, plus eight in deterministic content placement—not simply 34. |
|
||||
| “The largest gap is the loop direction” | **Plausible, not proved** | Every tunnel sample does query a 2D spatial bucket and iterate candidates, but the exclusive lookup machinery was only about 5% in the newer profile. The old 27% figure was inclusive of the feature kernels. Feature-major batching may still win through SIMD, hoisting and locality, but candidate lookup alone cannot justify a rewrite. |
|
||||
| Already shares built-first traversal, hash RNG, collision gate, partial narrow-band skipping | **Mixed** | Collision gating and narrow-band/reach proofs are real. Hash-keyed randomness is dominant but not universal. Traversal is structurally constructed for major archetypes, but the universal guarantee is not established, especially for fallback passage landings and lateral-region composition. |
|
||||
|
||||
## 1.1 Determinism note
|
||||
|
||||
The direct terrain-generation `Sin/Cos` sites are:
|
||||
|
||||
- [VoxelGenerator.cpp:1129](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:1129>), `1178`, `1377`, `1456`, `1557`, `1612`, `4708`, `4738`, `5723`.
|
||||
- [VoxelDensityOpStack.cpp:7507](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:7507>) and `7609`.
|
||||
- [VoxelHeightOpStack.cpp:232](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelHeightOpStack.cpp:232>).
|
||||
- [VoxelCaveMorphology.cpp:2931](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:2931>), `3266`, `6826`, `6843`, `7367`, `7487–7488`, `7520–7521`.
|
||||
- [VoxelStrateManager.cpp:1830](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelStrateManager.cpp:1830>), `1993`, `2008`, `2021–2022`, `2032–2033`, `2045`, `2087`, `2142`.
|
||||
|
||||
The duplication matters: several calls belong to parallel legacy/fused implementations, so a raw count overstates unique runtime work while still accurately describing the audit surface.
|
||||
|
||||
The engine source resolves the uncertainty conclusively:
|
||||
|
||||
- `FMath` inherits `FPlatformMath` at [UnrealMathUtility.h:269](</E:/Program Files/Epic Games/UE_5.7/Engine/Source/Runtime/Core/Public/Math/UnrealMathUtility.h:269>).
|
||||
- Win64 aliases `FPlatformMath` to `FMicrosoftPlatformMathBase` at [WindowsPlatformMath.h:18](</E:/Program Files/Epic Games/UE_5.7/Engine/Source/Runtime/Core/Public/Windows/WindowsPlatformMath.h:18>).
|
||||
- The Microsoft class inherits the generic implementation and does not override `Sin` or `Cos` at [MicrosoftPlatformMath.h:12](</E:/Program Files/Epic Games/UE_5.7/Engine/Source/Runtime/Core/Public/Microsoft/MicrosoftPlatformMath.h:12>).
|
||||
- Generic `Sin` calls `sinf` and generic `Cos` calls `cosf` at [GenericPlatformMath.h:521](</E:/Program Files/Epic Games/UE_5.7/Engine/Source/Runtime/Core/Public/GenericPlatform/GenericPlatformMath.h:521>) and [GenericPlatformMath.h:530](</E:/Program Files/Epic Games/UE_5.7/Engine/Source/Runtime/Core/Public/GenericPlatform/GenericPlatformMath.h:530>).
|
||||
|
||||
So, on UE 5.7 Win64, `FMath::Sin/Cos` use the MSVC/UCRT functions. They do not use Unreal’s polynomial `FMath::SinCos(float)`, which is separate engine code at [UnrealMathUtility.h:752](</E:/Program Files/Epic Games/UE_5.7/Engine/Source/Runtime/Core/Public/Math/UnrealMathUtility.h:752>).
|
||||
|
||||
Microsoft documents that the x64 CRT selects FMA3 implementations at startup based on CPU support and that the alternate algorithms can produce different results. [`_set_FMA3_enable` documentation](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-fma3-enable-set-fma3-enable?view=msvc-170).
|
||||
|
||||
The existing cross-platform test already recognizes the libm hole at [VoxelForgeCrossPlatformTest.cpp:158](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp:158>), but it reports risk rather than enforcing an identical field digest. Therefore constraint 1 is not currently proved.
|
||||
|
||||
## 1.2 “Largest gap is loop direction”
|
||||
|
||||
What the note gets right:
|
||||
|
||||
- A cache builds 2D chunk buckets at [VoxelCaveMorphology.cpp:1325](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:1325>).
|
||||
- Each sample calls `VF_ForEachSpatialCandidate`, including `GetRange`, at [VoxelCaveMorphology.cpp:1462](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:1462>).
|
||||
- Tunnel SDF and world-core evaluation perform separate candidate walks at [VoxelCaveMorphology.cpp:7847](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:7847>) and [VoxelCaveMorphology.cpp:8042](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:8042>).
|
||||
- The canonical fused path remains scalar per sample: `GetDensityAt` calls `GetDensityWithParams` at [VoxelGenerator.cpp:3386](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:3386>).
|
||||
- The mesher calls `GetDensityAt` once for every required grid sample at [VoxelMarchingCubesMesher.cpp:694](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:694>).
|
||||
|
||||
What is overstated:
|
||||
|
||||
- The `1d824aa` sampler’s `VF_ForEachSpatialCandidate` share was inclusive of work executed inside the visitor.
|
||||
- The later `410788f` exclusive attribution puts `GetRange` around 3.8% and `VF_ForEachSpatialCandidate` itself around 1.2%.
|
||||
- Thus the directly removable search overhead is approximately 5% of the old profile, not 27%.
|
||||
- The expensive swept-tunnel work remains inside those callbacks. A loop inversion must make that work cheaper—not merely remove the lookup—to justify itself.
|
||||
|
||||
“Coarse LOD from fine data is absent” is factually true but not evidence of a gap. Direct evaluation at every LOD is a deliberate clipmap invariant: it maintains roughly constant tile complexity and does not force far terrain to materialize at LOD0. See [ARCHITECTURE.md:708](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:708>).
|
||||
|
||||
“Shared halo samples are absent” needs qualification:
|
||||
|
||||
- Every tile already has a single local density grid plus a one-sample margin; cell corners and normals are not resampled. See [VoxelMarchingCubesMesher.cpp:656](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:656>) and [ARCHITECTURE.md:652](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:652>).
|
||||
- `FVoxelSharedDensityGrid` already exists and the mesher can consume it at [VoxelMarchingCubesMesher.cpp:660](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:660>).
|
||||
- It is populated only by the editor exploration commandlet, culminating in `SetSharedDensityGrid` at [VoxelForgeExploreCommandlet.cpp:1905](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp:1905>).
|
||||
- Gameplay does not share density samples across neighboring tiles.
|
||||
|
||||
So the missing opportunity is specifically **cross-tile gameplay reuse**, not basic grid or halo caching.
|
||||
|
||||
## 1.3 “Already shared”
|
||||
|
||||
### Traversal: partial, not universal
|
||||
|
||||
The current system has real construction-first features: walk-critical tunnel forests, D-floors, option-B winding and structural posts. That part of the mapping is fair.
|
||||
|
||||
But two facts prevent me from calling the hard guarantee closed:
|
||||
|
||||
1. `ARCHITECTURE.md` says every landing receives a deterministic connector to the origin landing at [ARCHITECTURE.md:518](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:518>). Current code explicitly says the opposite: a source-fit landing remains local and no radial connector is synthesized at [VoxelStrateManager.cpp:2108](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelStrateManager.cpp:2108>). The document is stale or the implementation regressed from its stated contract.
|
||||
|
||||
2. The lateral-region audit remains at 4/16 connected cases after landing, including region-zero controls, at [ARCHITECTURE.md:122](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:122>).
|
||||
|
||||
`VF_SuggestLandingPoint` can make a local landing safe when it succeeds, but the documentation says unsupported/no-footing cases fall back to historical random reach at [ARCHITECTURE.md:512](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:512>). That is not a construction proof.
|
||||
|
||||
The canonical 20,830 / 10,909 gate is valuable regression protection. It is not a proof across seeds, composer rolls and archetype mixtures.
|
||||
|
||||
### Randomness: mostly hash-keyed
|
||||
|
||||
Most feature decisions are keyed hashes. The layout shuffle still uses one stateful `FRandomStream` after sorting the asset pool at [VoxelStrateManager.cpp:1145](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelStrateManager.cpp:1145>) and [VoxelStrateManager.cpp:1157](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelStrateManager.cpp:1157>).
|
||||
|
||||
That is deterministic for the same build and pool. It is nevertheless order-coupled: adding one pool choice can shift every later choice. It therefore falls short of the first answer’s “stable ID plus counter-based randomness” ideal, though it does not presently violate the stated cross-machine rule.
|
||||
|
||||
### Collision gate: correct
|
||||
|
||||
The implementation now treats actual collision-cook completion as admission authority. This is stronger than speculative prefetch. It satisfies the hard safety part even under overload by stopping motion.
|
||||
|
||||
What remains from the first answer is UX improvement: predictive fall/shaft prefetch could make the gate engage less often. It is not required for correctness.
|
||||
|
||||
### Narrow bands: correct and substantial
|
||||
|
||||
The sealed-solid proof, tunnel-core reach, passage-carving reach, worm block proof, cached topology and lateral-region preparation all exist. They are not “partial” in the sense of being superficial; §8.10 records a considerable exact-skip system.
|
||||
|
||||
---
|
||||
|
||||
# 2. Ideas that survive contact with the code
|
||||
|
||||
## Ranked recommendations
|
||||
|
||||
| Rank | Change | Share of 27.6 s it could touch | Expected net gain | Rough size |
|
||||
|---|---|---:|---:|---:|
|
||||
| 1 | Tile-wide, feature-major lowering for tunnels | **35–50% inferred**: 9.7–13.8 s | **8–18% inferred**: 2.2–5.0 s | 0.8–1.5 KLOC prototype; 3–6 KLOC production |
|
||||
| 2 | Cross-tile exact density-grid reuse | Up to **22.9% theoretical/inferred**: 6.3 s; actual unknown | **5–12% inferred** if reuse is high | 0.6–1.2 KLOC prototype; 1.5–3 KLOC production |
|
||||
| 3 | Canonical numeric ABI | Probably under 5% runtime touched; correctness is the value | Approximately neutral, perhaps −1% to +1% inferred | 0.3–0.8 KLOC plus tests |
|
||||
| 4 | Traversal-plan contract hardening | 0% of static worker time | Correctness, not speed | 1–3 KLOC plus seed-matrix tests |
|
||||
| 5 | Exact base-density reuse and canonical edit compaction | **0% measured** in the static scenario | Potentially large only after repeated edits; unmeasured | 1.5–3 KLOC |
|
||||
| 6 | Deadline/fall-aware prefetch | Moving-streaming work only; unknown | Gate-engagement and latency improvement, not established CPU savings | 1–2 KLOC |
|
||||
| 7 | Topology-aware underground visibility | Far streaming only; unknown | Speculative | Large |
|
||||
|
||||
The `1d824aa` percentages are historical measured shares from a 29.30 s build, not additive and not exact for the current 27.6 s build. Scaling them onto 27.6 s is explicitly inferred.
|
||||
|
||||
## 2.1 Tile-wide, feature-major lowering
|
||||
|
||||
### Where it plugs in
|
||||
|
||||
Add a tile/grid evaluation entry point beside:
|
||||
|
||||
- `UVoxelGenerator::GetDensityAt` at [VoxelGenerator.cpp:2799](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2799>).
|
||||
- `UVoxelGenerator::GetDensityWithParams` at [VoxelGenerator.cpp:3921](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:3921>).
|
||||
- The mesher grid-fill loop at [VoxelMarchingCubesMesher.cpp:694](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:694>).
|
||||
|
||||
Conceptually:
|
||||
|
||||
```text
|
||||
composer/operator stack
|
||||
↓ lowering
|
||||
canonical tile program
|
||||
↓
|
||||
tile-local SoA buffers
|
||||
↓
|
||||
existing density grid
|
||||
↓
|
||||
existing two-pass marching cubes
|
||||
```
|
||||
|
||||
It should replace only the repeated scalar execution of the canonical tunnel-network graph. It should keep:
|
||||
|
||||
- `FVoxelOpStack` as the authoring language.
|
||||
- `BuildTunnelNetworkStack` and the fused-evaluator designation.
|
||||
- `FChunkSDFCache` and its search-box validity.
|
||||
- The warped tunnel SDF and world-space core as separate semantic stages.
|
||||
- The common structural tail, boundary seal and edit layer.
|
||||
- Scalar fallback for custom recipes, fractional point queries and unsupported stacks.
|
||||
- The existing marching-cubes grid, margin and two-pass cell loop.
|
||||
|
||||
The production algorithm I would try is:
|
||||
|
||||
1. Prepare the same chunk/tile context once.
|
||||
2. Allocate thread-local SoA arrays for density, warped coordinates, nearest-room ownership and world-core handoff flags.
|
||||
3. Compute sample-invariant and column-invariant data in batches.
|
||||
4. Iterate rooms/tunnels in existing stable feature-index order.
|
||||
5. Clip each feature’s conservative 3D influence bounds to the tile lattice.
|
||||
6. Traverse X-contiguous spans and evaluate four or eight positions together where the formula permits.
|
||||
7. Apply smooth unions in the same per-sample feature order.
|
||||
8. Run later operators and the structural tail in their canonical order.
|
||||
9. Write the existing `DensityGrid`.
|
||||
|
||||
The index builder inserts item indices in ascending feature order at [VoxelCaveMorphology.cpp:1445](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:1445>). Maintaining that order matters because smooth-min and other floating operations are order-sensitive.
|
||||
|
||||
### Measured basis
|
||||
|
||||
Historical remove-one shares:
|
||||
|
||||
- Tunnel core: **20.84% measured**, corresponding to 5.75 s when scaled to 27.6 s.
|
||||
- Tunnel SDF: **12.64% measured**, corresponding to 3.49 s.
|
||||
- Room SDF: **4.24% measured**, corresponding to 1.17 s.
|
||||
- Cave warp: **6.68% measured**, corresponding to 1.84 s.
|
||||
- Passage carving/structure can be incorporated later.
|
||||
|
||||
The tunnel core plus tunnel SDF therefore account for **33.48% measured historically**, or 9.24 s scaled to the current total. A batch evaluator cannot eliminate that geometry; it can only reduce its loop, load, branch and arithmetic cost.
|
||||
|
||||
The newer exclusive profile says lookup machinery itself was approximately **5% measured historically**, about 1.38 s scaled. This is why “invert the loops” alone is not enough. The hoped-for gain must come from:
|
||||
|
||||
- Hoisting tunnel metadata.
|
||||
- Avoiding repeated lambdas/cache lookups and repeated world-coordinate construction.
|
||||
- SIMD across adjacent X samples.
|
||||
- Reusing centerline segment data across samples.
|
||||
- Better sequential reads and writes.
|
||||
- Clipping feature loops in 3D rather than retrieving a 2D bucket and rejecting on Z for every sample.
|
||||
|
||||
### Risks
|
||||
|
||||
- **Capability:** high. The core/SDF handoff and room-floor ownership are subtle. The rejected merge shows this is load-bearing.
|
||||
- **Determinism:** medium-high. SIMD must use the same fixed instruction path on every x64 machine. Any conservative span skip must be reference-checked and field-identical.
|
||||
- **Composer:** low if this remains a lowering backend. High if it becomes a separate hand-authored terrain definition.
|
||||
- **§8.10:** high implementation risk. It must preserve search-box validity, lateral per-chunk preparation, reach masks, the margin grid, cancellation points, TLS reuse and direct evaluation at each LOD.
|
||||
- **Maintenance:** high if the scalar and batched semantics fork. Shared primitive functions and generated/lowered stage descriptions are preferable to copying thousands of lines.
|
||||
|
||||
I would not generalize it to every archetype first. TunnelNetwork/Underwater already have a canonical fused designation and dominate the measured cost.
|
||||
|
||||
## 2.2 Exact density sharing across gameplay tiles
|
||||
|
||||
This is a more concrete surviving piece of the “shared bricks/halos” proposal than a full sparse-brick rewrite.
|
||||
|
||||
### Why it may be larger than it looks
|
||||
|
||||
A normal full-resolution tile has 32 cells but samples a 35³ grid because of the cell boundary and one-point normal margin.
|
||||
|
||||
For an ideal dense array of adjacent same-LOD tiles, the asymptotic duplicate fraction is:
|
||||
|
||||
```text
|
||||
1 − 32³ / 35³ = 23.6%
|
||||
```
|
||||
|
||||
This is an inferred theoretical ceiling. Actual reuse will be lower because of clipmap shell boundaries, Z bands, cancellation and tile completion order.
|
||||
|
||||
The old sampled profile attributed **96.9% measured, inclusive** to `GetDensityAt`. If duplicated coordinates had average cost, the absolute theoretical CPU ceiling would be approximately:
|
||||
|
||||
```text
|
||||
23.6% × 96.9% = 22.9% of total
|
||||
```
|
||||
|
||||
That is 6.3 s of the 27.6 s total. It is not an expected gain; it is the reason this deserves instrumentation.
|
||||
|
||||
### Where it plugs in
|
||||
|
||||
- Existing consumer support: `FVoxelSharedDensityGrid` in [VoxelMarchingCubesMesher.h:25](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h:25>).
|
||||
- Existing copy path: [VoxelMarchingCubesMesher.cpp:660](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:660>).
|
||||
- Scheduler and tile lifetime: `AVoxelWorld::LoadTile`, task completion and cancellation paths.
|
||||
- Edit revision: `UVoxelDiffLayer::ModsVersion` and the snapshot mechanism.
|
||||
|
||||
I would prototype an **opportunistic completed-neighbor slab cache**, not a global per-sample hash table:
|
||||
|
||||
- Store exact float boundary slabs from recently completed tiles.
|
||||
- Key by generator owner, layout/generation epoch, final-field edit revision, sample step and integer world-lattice bounds.
|
||||
- Before sampling a tile, copy all available overlap slabs with simple block loops.
|
||||
- Evaluate only missing rectangular regions.
|
||||
- Never wait for a neighboring tile.
|
||||
- Evict outside the desired/in-flight neighborhood.
|
||||
|
||||
The current `SetSharedDensityGrid` member pointer is suitable for the single-threaded editor commandlet, not for concurrent gameplay tasks. Production should pass immutable donor views with each mesh request, not mutate shared mesher state.
|
||||
|
||||
Do not reuse the quantized density-volume capture for meshing. Its bytes are sufficient for lighting volume capture, not exact marching-cubes interpolation.
|
||||
|
||||
### Risks
|
||||
|
||||
- **Streaming:** a support tile must never wait for a batch or neighbor.
|
||||
- **Revision correctness:** an edit or layout rebuild must prevent reuse of stale final densities.
|
||||
- **Memory:** retaining complete 35³ float grids is ~171 KB per tile. Store bounded slabs or short-lived grids, not every loaded tile indefinitely.
|
||||
- **Concurrency:** avoid global locks in the sample loop.
|
||||
- **Determinism:** exact copied floats are safe if the key is complete. A stale cache hit is a geometry error.
|
||||
- **§8.10:** preserve task cancellation and the thread-local destination grid; copying merely replaces a subset of its fill operations.
|
||||
|
||||
Expected net gain is **5–12% inferred**, conditional on actual ready-before-use reuse being high. It may also fail completely if most neighboring tasks execute concurrently and finish too late to donate samples.
|
||||
|
||||
## 2.3 Canonical numeric ABI
|
||||
|
||||
This is the surviving portion of the first answer’s WorldPlan recommendation with the highest correctness value.
|
||||
|
||||
The code already has a substantial plan/layout layer, cached topology, per-strate parameters and operator recipes. I would not introduce a second “WorldPlan” representation. I would add:
|
||||
|
||||
- A generation format/version ID.
|
||||
- Stable feature IDs for topology objects and composer choices.
|
||||
- Canonical deterministic `SinCos`, normalization and checked coordinate conversions.
|
||||
- Golden plan, density and mesh digests.
|
||||
- An explicit floating-environment contract for worker tasks.
|
||||
|
||||
This touches very little of the 27.6 s. The per-sample `Sin` calls may be measurable, but the topology and passage calls are cold. Expect approximately neutral performance.
|
||||
|
||||
## 2.4 Traversal contract hardening
|
||||
|
||||
The least invasive structural fix is to make passage creation return one of two outcomes:
|
||||
|
||||
- A landing with a certificate identifying the existing structural feature/component to which it belongs.
|
||||
- A newly constructed explicit connector included in the mandatory route graph.
|
||||
|
||||
A failed `VF_SuggestLandingPoint` should cause a deterministic retry/recomposition or explicit connector. It should not silently fall back to an unproved random reach.
|
||||
|
||||
For lateral compositions, generate and reserve the mandatory entry-to-exit spine at the parent level before regional creative stacks. Regional terrain may join or decorate it but not own its existence.
|
||||
|
||||
This preserves the composer and adds no material density-loop cost. The proof is a seed/archetype/composition matrix, not merely the canonical scenario.
|
||||
|
||||
## 2.5 Edit compaction and base-grid reuse
|
||||
|
||||
The current edit layer duplicates an edit into each affected chunk and evaluates the list serially at [VoxelDiffLayer.cpp:361](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDiffLayer.cpp:361>). There is no compaction into canonical per-brick deltas.
|
||||
|
||||
This does not touch the standard static 27.6 s scenario: **0% measured**.
|
||||
|
||||
For mining-heavy sessions, I would eventually keep:
|
||||
|
||||
- Immutable generated base samples for a recently remeshed edited tile.
|
||||
- A server-sequenced edit stream.
|
||||
- Canonical per-tile delta compaction after a threshold.
|
||||
- Separate base-generation, edit, mesh and collision revision numbers.
|
||||
|
||||
Do this only after creating an edit-heavy benchmark. Depending on edit-list length, it might remove 30–80% of edit-layer work or much of repeated base evaluation, but that is wholly inferred and should not be sold using the static benchmark.
|
||||
|
||||
---
|
||||
|
||||
# 3. Proof-of-worth ladders
|
||||
|
||||
## Change A: tile-wide feature-major tunnel evaluation
|
||||
|
||||
### Rung 1 — Instrument the current evaluator
|
||||
|
||||
Use the existing counters:
|
||||
|
||||
- `CaveRoomCandidates/Evaluated`
|
||||
- `CaveTunnelCandidates/Evaluated`
|
||||
- `TunnelCoreCandidates/Evaluated`
|
||||
|
||||
They are recorded in [VoxelCaveMorphology.cpp:7765](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:7765>), [VoxelCaveMorphology.cpp:7844](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:7844>) and [VoxelCaveMorphology.cpp:8042](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:8042>), then logged per tile at [VoxelWorld.cpp:4961](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:4961>).
|
||||
|
||||
Add profile-only counters for:
|
||||
|
||||
- `GetRange` calls/hits/fallbacks and cycles.
|
||||
- Candidate visits rejected by 3D AABB.
|
||||
- Candidate visits rejected by bounding sphere.
|
||||
- Actual swept-tunnel evaluations.
|
||||
- Centerline segment iterations.
|
||||
- For each tile, the predicted feature-major lattice visits obtained by clipping every feature’s conservative 3D bounds to that tile.
|
||||
- Separate static/moving and LOD0/all-LOD totals.
|
||||
|
||||
Do not time every sample with a heavyweight timer; aggregate cycles around whole loops or rely on sampling.
|
||||
|
||||
**Kill immediately if:**
|
||||
|
||||
- Exclusive candidate/rejection machinery remains below 6–7% of worker time, and
|
||||
- Predicted feature-major visits are at least 80% of current expensive evaluations, and
|
||||
- There is no clear X-lane SIMD opportunity in the swept kernel.
|
||||
|
||||
That combination caps the likely gain below what a large rewrite deserves.
|
||||
|
||||
**Proceed if either:**
|
||||
|
||||
- Conservative feature spans reduce candidate/kernel visits by at least 2×, or
|
||||
- The combined measured loop/branch/kernel share with realistic SIMD opportunity is at least 12% of total worker time.
|
||||
|
||||
Also require projected scratch below roughly 2 MB per worker for a single-tile prototype.
|
||||
|
||||
### Rung 2 — Tunnel SDF only
|
||||
|
||||
Behind a diagnostic console switch:
|
||||
|
||||
- Batch the warped tunnel SDF.
|
||||
- Leave `EvaluateTunnelCoreWorld` entirely unchanged.
|
||||
- Preserve room-mouth ownership and feature order.
|
||||
- Keep scalar evaluation as the reference path.
|
||||
- Add a debug mode comparing every produced sample with the reference field.
|
||||
|
||||
The tunnel SDF had a **12.64% measured historical share**, so the prototype has enough budget to show a useful result without touching the load-bearing core.
|
||||
|
||||
**Kill immediately on:**
|
||||
|
||||
- Player-fit below 20,830.
|
||||
- Walk-reachable below 10,909.
|
||||
- Loss of connected status.
|
||||
- Any nondeterministic digest across repeated runs or machines.
|
||||
- A span/proof skip that changes a reference density bit.
|
||||
- A violation of the two-region cache window, margin grid or cancellation contracts.
|
||||
|
||||
**Performance kill criterion:**
|
||||
|
||||
- Less than 3% total worker-CPU improvement in either static or moving, or
|
||||
- More than 3% regression in generation/collision-ready p95.
|
||||
|
||||
**Justification to continue:**
|
||||
|
||||
- At least 5% total worker-CPU improvement in both static and moving, with no material p95 or memory regression.
|
||||
|
||||
The switch-on/off result is only an internal decision aid. It does not satisfy final acceptance.
|
||||
|
||||
### Rung 3 — Separate batch stages for world core and passage work
|
||||
|
||||
Add the world-space core as a second, semantically separate batch stage. Do not merge it with the warped SDF. Then consider passage carving/structure if the same infrastructure applies.
|
||||
|
||||
Full adoption requires:
|
||||
|
||||
- Default-on build compared against the untouched `cf9904e` DLL.
|
||||
- At least 10% worker-CPU improvement in both static and moving, or at least 8% plus a material collision-ready/p95 improvement.
|
||||
- Canonical capability counts and connection.
|
||||
- Skip/reference tests clean.
|
||||
- Memory and task-cancellation behavior within existing bounds.
|
||||
- No custom-recipe or lateral-composition semantic fork.
|
||||
|
||||
## Change B: cross-tile exact density reuse
|
||||
|
||||
### Rung 1 — Count exact duplicate opportunities
|
||||
|
||||
Record, for every generated tile:
|
||||
|
||||
- Origin, step, sampled Z band and generation/edit revision.
|
||||
- Task start and completion order.
|
||||
- Count of requested lattice coordinates.
|
||||
- Count of distinct lattice coordinates across the scenario.
|
||||
- Count whose donor tile had already completed before the consumer started.
|
||||
- Reuse grouped by LOD, same/cross-LOD, static/moving and density-cost bucket.
|
||||
|
||||
This can be computed offline from a compact tile trace; there is no need for a runtime hash set of every sample in the production path.
|
||||
|
||||
**Kill if:**
|
||||
|
||||
- Exact duplicate coordinates are below 12% of all density calls, or
|
||||
- Fewer than half of duplicates have a completed donor in time, or
|
||||
- Time-weighted eligible duplicates represent less than 5% of worker CPU.
|
||||
|
||||
**Proceed if:**
|
||||
|
||||
- At least 15% of calls are exact duplicates and at least 60% are available opportunistically, or
|
||||
- The conservative time-weighted saving ceiling exceeds 10% of total worker CPU.
|
||||
|
||||
### Rung 2 — LOD0 completed-neighbor slabs
|
||||
|
||||
Prototype only LOD0:
|
||||
|
||||
- Exact float slabs.
|
||||
- Immutable per-request donor views.
|
||||
- No waiting.
|
||||
- No cross-LOD reuse.
|
||||
- Strict owner/layout/edit revision key.
|
||||
- Bounded memory, preferably 32–64 MB.
|
||||
- Missing samples follow the current path.
|
||||
|
||||
**Kill immediately on any field/mesh digest difference.** This should be a pure reuse, not a field-changing implementation.
|
||||
|
||||
**Performance kill criterion:**
|
||||
|
||||
- Less than 4% worker-CPU improvement in either static or moving.
|
||||
- More than 3% collision-ready or generation p95 regression.
|
||||
- Memory above the agreed cap.
|
||||
- Significant loss of task concurrency.
|
||||
|
||||
**Proceed if:**
|
||||
|
||||
- At least 6% worker-CPU improvement in both scenarios.
|
||||
- No readiness regression.
|
||||
- No stale-hit failure under edit/rebuild/cancellation stress.
|
||||
|
||||
### Rung 3 — Full same-step gameplay reuse
|
||||
|
||||
Extend to same-step tiles at all useful clip levels, with revisioned invalidation and bounded eviction. Cross-LOD reuse is optional and should be separately justified.
|
||||
|
||||
Adopt only if the final default build beats `cf9904e` by at least 7–8% overall or produces a slightly smaller CPU gain plus a clear collision-readiness improvement. Remove the diagnostic switch only after the committed-DLL comparison.
|
||||
|
||||
---
|
||||
|
||||
# 4. Numeric determinism audit
|
||||
|
||||
## 4.1 Critical: CRT `sin/cos`
|
||||
|
||||
**Severity: critical.**
|
||||
|
||||
These functions affect:
|
||||
|
||||
- Passage control points and landing directions.
|
||||
- Tunnel winding and pinch orientation.
|
||||
- Room/feature placement.
|
||||
- Per-sample line/rib/surface detail.
|
||||
- Walkability thresholds.
|
||||
|
||||
The UCRT can select different implementations in the same Win64 binary based on CPU features. Windows and Linux libm implementations may also differ.
|
||||
|
||||
Minimal fix:
|
||||
|
||||
1. Introduce one canonical float `SinCos` implementation with explicitly bounded input, fixed range reduction and fixed operation grouping.
|
||||
2. The UE float `FMath::SinCos` polynomial is a reasonable prototype because it is engine arithmetic rather than CRT dispatch. I would wrap or vendor it so the numeric contract is explicit.
|
||||
3. Replace paired `Sin`/`Cos` calls together.
|
||||
4. Use the same canonical sine output for individual sine-only sites.
|
||||
5. Pin golden input/output bit patterns and world-plan/field digests.
|
||||
6. Retune if necessary; a new build may change the field.
|
||||
|
||||
Calling `_set_FMA3_enable(0)` is not a complete fix. It is process-global, Windows-specific and does not align UCRT with glibc.
|
||||
|
||||
## 4.2 Medium: `sqrt` and implicit vector normalization
|
||||
|
||||
`FMath::Sqrt` also reaches `sqrtf` at [GenericPlatformMath.h:552](</E:/Program Files/Epic Games/UE_5.7/Engine/Source/Runtime/Core/Public/GenericPlatform/GenericPlatformMath.h:552>). On x64 this will commonly become a hardware square-root instruction, which is much less alarming than `sin/cos`, but the source-level contract still delegates to the platform library/compiler.
|
||||
|
||||
World-deciding examples include:
|
||||
|
||||
- Morphology topology/walkability: [VoxelCaveMorphology.cpp:639](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:639>), `785`, `2171`, `2188`, `3019`, `3027`, `4919`, `5101`, `5355`.
|
||||
- Tunnel/room field evaluation: `7704`, `8356`, `8550`.
|
||||
- Passage/layout geometry: [VoxelStrateManager.cpp:666](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelStrateManager.cpp:666>), `2393`, `2406`, `3701`, `3804`.
|
||||
- Composer decisions: [VoxelStrateComposer.cpp:1572](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelStrateComposer.cpp:1572>), `1606–1608`, `4141`, `4172`, `4662–4665`, `4849`, `5016–5018`.
|
||||
- Numerous SDF sites in `VoxelGenerator.cpp` and `VoxelDensityOpStack.cpp`.
|
||||
- `FVector::Size`, `Dist` and `Normalize`, for example passage direction normalization at [VoxelStrateManager.cpp:2085](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelStrateManager.cpp:2085>).
|
||||
|
||||
Minimal fix:
|
||||
|
||||
- Replace square-root comparisons with squared comparisons where mathematically equivalent.
|
||||
- Canonicalize only the remaining square roots that affect topology, integer conversion or threshold decisions.
|
||||
- Verify emitted Win64 instructions and run golden vectors before attempting to replace every SDF square root. I am not claiming a demonstrated cross-CPU `sqrt` divergence here.
|
||||
|
||||
No direct runtime uses of `FMath::Pow`, `Exp`, `Log`, `Atan`, `Atan2` or `Tan` were found in the plugin generation sources. That does not rule out transitive engine/library use, but there are no direct call sites to fix.
|
||||
|
||||
## 4.3 High: runtime CPU state and dispatch
|
||||
|
||||
### Confirmed dispatch
|
||||
|
||||
- UCRT FMA3 `sin/cos`: critical, as above.
|
||||
|
||||
### Plugin SIMD
|
||||
|
||||
`VoxelNoise` selects scalar versus SSE at compile time through `VF_NOISE_USE_SIMD` at [VoxelNoise.h:35](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelNoise.h:35>). The hot implementation uses explicit SSE4.1 at [VoxelNoise.h:248](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelNoise.h:248>).
|
||||
|
||||
I found no plugin runtime CPUID dispatch and no ISPC path. The same Win64 build therefore takes the same plugin noise implementation on all supported x64 CPUs.
|
||||
|
||||
The “scalar and SIMD are bit-identical” claim at [VoxelNoise.h:20](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelNoise.h:20>) should still have a pinned exhaustive/random corpus. It is a claim worth continuously testing, not merely documenting.
|
||||
|
||||
### MXCSR rounding mode and FTZ/DAZ
|
||||
|
||||
UE’s integer conversions inherit SSE implementations such as `FloorToInt32` and `RoundToInt32` at [UnrealPlatformMathSSE.h:86](</E:/Program Files/Epic Games/UE_5.7/Engine/Source/Runtime/Core/Public/Math/UnrealPlatformMathSSE.h:86>). Several use rounding-mode-sensitive conversion instructions and assume the normal environment.
|
||||
|
||||
Another library changing a worker thread’s MXCSR rounding mode could change world-to-cell decisions. FTZ/DAZ can likewise alter very small intermediates.
|
||||
|
||||
Minimal fix:
|
||||
|
||||
- At generation-task entry, assert or install a canonical MXCSR configuration and restore the previous state at exit.
|
||||
- Prefer explicit rounding instructions or checked conversion helpers for canonical boundaries.
|
||||
- Test all supported task threads, not only the game thread.
|
||||
|
||||
## 4.4 Critical/high: float-to-integer boundaries
|
||||
|
||||
### Noise lattice conversion
|
||||
|
||||
[VoxelNoise.h:96](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelNoise.h:96>) floors a float and casts it directly to `int32`. The SIMD version uses `_mm_cvttps_epi32` at [VoxelNoise.h:255](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelNoise.h:255>).
|
||||
|
||||
For NaN or out-of-range values, scalar C++ conversion and SSE conversion do not provide a suitable canonical world rule.
|
||||
|
||||
**Severity: high**, especially with `WorldRadiusVoxels == 0` permitting an unbounded world.
|
||||
|
||||
Fix:
|
||||
|
||||
- Define a supported integer-coordinate envelope.
|
||||
- Reject or clamp malformed/out-of-range queries before noise evaluation.
|
||||
- Use one checked floor/wrap policy shared by scalar and SIMD paths.
|
||||
- Test the boundary values and their immediate float neighbors.
|
||||
|
||||
### Chunk routing
|
||||
|
||||
`GetDensityAt` converts public float coordinates to chunk coordinates without a finite/range guard at [VoxelGenerator.cpp:2836](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2836>).
|
||||
|
||||
Internal mesher calls are bounded integer coordinates, but other callers need not be.
|
||||
|
||||
Fix:
|
||||
|
||||
- Give the mesher an integer-lattice entry point.
|
||||
- Keep the public float query checked.
|
||||
- Decide explicitly what happens outside representable chunk space.
|
||||
|
||||
The edit layer already demonstrates the correct pattern at [VoxelDiffLayer.cpp:220](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDiffLayer.cpp:220>).
|
||||
|
||||
### Procedural cell selection
|
||||
|
||||
`VoxelCaveMorphology.cpp` contains many `FloorToInt` cell selections, including the placement/cache families around `3902–3903`, `3954–3956`, `4038–4039`, `4180–4181`, `4554–4555`, `4683–4684`, `4881–4882`, `5085–5086`, `5195–5196` and `6055–6058`.
|
||||
|
||||
These are safe only while inputs remain finite and representable. They should consume the same declared coordinate envelope.
|
||||
|
||||
### Conservative skip boundaries
|
||||
|
||||
Passage and box proofs use float-to-int lattice conversions, for example around `VoxelStrateManager.cpp:3333–3334`. These are lower severity if they are genuinely one-sided: disagreement must cause a missed skip, never omitted geometry.
|
||||
|
||||
The better formulation is integer/rational lattice `ceil`/`floor` from validated bounds, with explicit outward rounding.
|
||||
|
||||
### Mesh vertex identity
|
||||
|
||||
The mesher deduplicates a vertex by rounding its interpolated world position multiplied by 100 at [VoxelMarchingCubesMesher.cpp:154](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:154>).
|
||||
|
||||
A small numeric difference crossing a half-centimetre rounding boundary can change vertex sharing and triangle/index output even if the surface samples remain on the same side.
|
||||
|
||||
**Severity: medium-high.**
|
||||
|
||||
Minimal fix: identify marching-cubes vertices canonically by integer cell coordinate plus edge axis, not rounded interpolated position. That is both cheaper and independent of interpolation rounding.
|
||||
|
||||
## 4.5 High: worker-side global races
|
||||
|
||||
The following “parse once” flags and their backing values are ordinary globals accessed from worker generation:
|
||||
|
||||
- Generator flags at [VoxelGenerator.cpp:47](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:47>), `73`, `98`, `124`, `143`, `202`.
|
||||
- The parsers are invoked inside every `GetDensityAt` at [VoxelGenerator.cpp:2820](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2820>).
|
||||
- Mesher flags at [VoxelMarchingCubesMesher.cpp:20](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:20>) and `42`, invoked at `97–98`.
|
||||
|
||||
Multiple first-time worker calls can read/write the booleans concurrently. `FAutoConsoleVariableRef` can also mutate backing integers while workers read them. That is C++ data-race undefined behavior.
|
||||
|
||||
Minimal fix:
|
||||
|
||||
- Parse command-line state once on the game/module initialization thread before generation begins.
|
||||
- Construct an immutable generation-policy snapshot.
|
||||
- Copy that snapshot into each task or generation epoch.
|
||||
- A live CVar change that affects the field must create a new epoch and rebuild; it must not alter active workers in place.
|
||||
- Diagnostics that are intentionally live should use atomics and must not influence the canonical field.
|
||||
|
||||
## 4.6 Critical for multiplayer edits: order-dependent accumulation
|
||||
|
||||
Edits are appended to per-chunk arrays at [VoxelDiffLayer.cpp:253](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDiffLayer.cpp:253>) and accumulated serially in insertion order at [VoxelDiffLayer.cpp:374](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDiffLayer.cpp:374>).
|
||||
|
||||
Floating addition is order-dependent. If clients apply replicated edits in packet-arrival order, their density fields can differ.
|
||||
|
||||
Minimal fix:
|
||||
|
||||
- Give every accepted edit a monotonic authoritative server sequence.
|
||||
- Buffer out-of-order edits and evaluate them in that sequence.
|
||||
- Include sequence ranges in edit/density cache revisions.
|
||||
- During compaction, use a canonical order or quantized/fixed-point delta representation.
|
||||
- Never parallel-reduce floating edit contributions without a deterministic reduction tree.
|
||||
|
||||
Current local serial evaluation is stable if every machine receives the exact same order. The network contract must guarantee that.
|
||||
|
||||
## 4.7 Medium: raw-memory fingerprints and padding
|
||||
|
||||
The code hashes raw parameter structs using `FCrc::MemCrc32` at:
|
||||
|
||||
- [VoxelGenerator.cpp:3170](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:3170>) and `3842`.
|
||||
- [VoxelDensityOpStack.cpp:1116](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:1116>), `1119` and `3693`.
|
||||
|
||||
The accompanying comments say a raw-memory CRC cannot produce a false positive. That is incorrect:
|
||||
|
||||
- Padding may be indeterminate, yielding nondeterministic false misses.
|
||||
- CRC32 can collide, yielding a false hit.
|
||||
- If the fingerprint is the only cache identity, a false hit can reuse terrain computed from different parameters.
|
||||
|
||||
Minimal fix:
|
||||
|
||||
- Hash fields individually in declared canonical order.
|
||||
- Normalize bools, floats and zero.
|
||||
- Prefer 64- or 128-bit hashes.
|
||||
- On a hash match, compare the complete canonical key before reusing semantic data.
|
||||
|
||||
This is probably a low-frequency failure, but an absolute determinism/correctness contract should not use “CRC collisions cannot happen” as an invariant.
|
||||
|
||||
## 4.8 Stateful randomness and ordering
|
||||
|
||||
`FRandomStream` layout shuffle is serial and deterministic for the current sorted pool, so it is not presently a cross-thread race.
|
||||
|
||||
Its weakness is evolutionary stability: inserting a pool member changes subsequent random consumption. Since older-build identity is not required, this is not urgent. Stable per-slot keyed selection becomes useful if saved plans, live content packs or replicated compact layouts must survive content additions predictably.
|
||||
|
||||
Current spatial candidate iteration is serial and ordered. I found no current parallel reduction of SDFs or edit values. A feature-major rewrite must preserve per-sample feature order or deliberately become a field-changing new canonical evaluator.
|
||||
|
||||
## 4.9 Uninitialized data and timing
|
||||
|
||||
I inspected the important `SetNumUninitialized` sites and found no confirmed uninitialized read:
|
||||
|
||||
- Spatial-index bound arrays are filled for every valid item before use at [VoxelCaveMorphology.cpp:1343](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelCaveMorphology.cpp:1343>).
|
||||
- Index storage is completely filled from prefix offsets at `1443–1455`.
|
||||
- The mesher samples only the Z band subsequently read; this is documented at [VoxelMarchingCubesMesher.cpp:680](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:680>).
|
||||
|
||||
The raw struct padding used by `MemCrc32` is the confirmed uninitialized-byte concern.
|
||||
|
||||
I found no density value chosen by wall-clock timing. Task timing controls which tiles become ready first, but epoch, cancellation and collision gates prevent obsolete work from becoming canonical terrain. Cache identity and edit sequencing are the places where timing could become semantic if keys/order are incomplete.
|
||||
|
||||
---
|
||||
|
||||
# 5. What I would not graft onto this code
|
||||
|
||||
- **No wholesale persistent sparse base-density brick store.** It would duplicate a pure procedural evaluator, add large revision/save/memory machinery and invalidate the current direct-LOD clipmap advantage. Transient tile buffers and edited-brick deltas capture most of the value.
|
||||
|
||||
- **No coarse LOD derived from mandatory fine voxels.** It would make far terrain require fine generation and undermine the constant-cell-count clipmap at [ARCHITECTURE.md:708](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:708>).
|
||||
|
||||
- **No min/max octree classifier as another always-on front end.** The outer classifier and exact per-sample cull already demonstrated that extra proof work can lose. Bounds should cheaply clip batch spans, not become a parallel evaluator.
|
||||
|
||||
- **No warped-SDF/world-core merge.** The rejected capability loss demonstrates that they express different geometry. Batch them separately.
|
||||
|
||||
- **No value-noise worm substitution or worm-focused rewrite.** Worm removal is only about 3%; that budget cannot support architectural work.
|
||||
|
||||
- **No general-purpose interpreted CSG evaluator.** The project has already paid to lower the canonical stack. General composition belongs in the authoring IR, not the sample loop.
|
||||
|
||||
- **No shell/B-rep base terrain or local shell-to-voxel collars.** Arbitrary noise, junctions, mixed archetypes and mining make the watertight seam problem much larger than the likely benefit.
|
||||
|
||||
- **No dual-contouring/transvoxel or mesh-extractor rewrite without an extractor-specific profile.** Density evaluation is the measured bottleneck. Changing topology would risk capability and seams while attacking an unmeasured target.
|
||||
|
||||
- **No GPU-authoritative field or meshing.** Cross-vendor bit identity remains unproved. GPU rendering/culling can be non-authoritative later.
|
||||
|
||||
- **No global concurrent hash map for individual density samples.** Per-sample lookup/locking could repeat the always-on-cull failure. Reuse should operate on rectangular slabs or immutable tile buffers.
|
||||
|
||||
- **No actor/UObject per brick.** Current plain worker data and game-thread publication are the right split.
|
||||
|
||||
- **No second WorldPlan/composer hierarchy.** `FStrateLayout`, recipes, cached topology and the operator stack already constitute one. Add stable IDs and contracts to those structures.
|
||||
|
||||
- **No conservative collision proxy unless gate stalls remain a real UX problem.** The current cook-complete admission gate already supplies correctness. A second proxy system needs a user-visible problem and measurements.
|
||||
|
||||
- **No claim that generated traversal remains guaranteed after arbitrary edits.** If that becomes a requirement, it needs an explicit server product rule: protect, reject or repair the final route.
|
||||
|
||||
---
|
||||
|
||||
# Bottom line
|
||||
|
||||
The owner’s “prove it worthy” stance is appropriate. The code has reached the point where another clever per-sample condition is more likely to lose than win.
|
||||
|
||||
I would authorize three preliminary efforts:
|
||||
|
||||
1. Instrument feature-major work potential using the existing candidate/evaluated counters.
|
||||
2. Trace exact duplicate density coordinates and ready-before-use donors across gameplay tiles.
|
||||
3. Close the numeric ABI hole, beginning with a FMA3-on/off and cross-platform golden corpus.
|
||||
|
||||
Only the first two are performance bets. Neither should become a production rewrite unless it clears its early kill criteria. The numeric work is different: the code currently documents, rather than satisfies, the hard field-determinism requirement.
|
||||
|
||||
# Files read
|
||||
|
||||
No files were changed. I did not build, run tests or launch Unreal.
|
||||
|
||||
Targeted sections read:
|
||||
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\CLAUDE.md`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\CODEMAP.md`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\ARCHITECTURE.md`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\WORK-NEXT.md`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\PERF-SAMPLED.md`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\DESIGN-SOL-2026-09-15.md`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\AUDIT-SOL-2026-09-11.md`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\AUDIT-COLD-2026-09-14.md`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\fable-idea.md`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\VoxelForge.Build.cs`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelGenerator.cpp`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelCaveMorphology.cpp`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelDensityOpStack.cpp`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelStrateManager.cpp`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelMarchingCubesMesher.cpp`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelWorld.cpp`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\VoxelDiffLayer.cpp`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelNoise.h`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelDensityProfile.h`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Public\VoxelMarchingCubesMesher.h`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForge\Private\Tests\VoxelForgeCrossPlatformTest.cpp`
|
||||
- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Source\VoxelForgeEditor\Private\VoxelForgeExploreCommandlet.cpp`
|
||||
|
||||
Files inspected through targeted symbol searches:
|
||||
|
||||
- `VoxelContentManager.cpp`
|
||||
- `VoxelHeightOpStack.cpp`
|
||||
- `VoxelStrateComposer.cpp`
|
||||
- `VoxelStrateMeasure.cpp`
|
||||
- `VoxelCaveMorphology.h`
|
||||
- `VoxelDensityPrimitives.h`
|
||||
- `VoxelHeightOp.h`
|
||||
- `VoxelPassageGeometry.h`
|
||||
- `VoxelStrateTypes.h`
|
||||
- `VoxelForgeComposerStructureRollTest.cpp`
|
||||
- `VoxelForgePassageOpenSpaceTest.cpp`
|
||||
- `VoxelForgeScaleDiagnosisTest.cpp`
|
||||
- `VoxelForgeStrateConnectivityTest.cpp`
|
||||
- `VoxelForgeWormBlockSkipTest.cpp`
|
||||
|
||||
UE 5.7 engine source read:
|
||||
|
||||
- `GenericPlatformMath.h`
|
||||
- `UnrealMathUtility.h`
|
||||
- `MicrosoftPlatformMath.h`
|
||||
- `WindowsPlatformMath.h`
|
||||
- `UnrealPlatformMathSSE.h`
|
||||
- `UnrealPlatformMathSSE4.h`
|
||||
|
||||
I also inspected git metadata/diffs for `1d824aa`, `a668833` and `410788f`.
|
||||
@@ -0,0 +1,580 @@
|
||||
# DESIGN-SOL-2026-09-15: "build it every better way", asked cold
|
||||
|
||||
On the owner's idea (see memory `reframe-when-returns-diminish`): after the exact-skip perf push
|
||||
reached diminishing returns, we asked Sol (`gpt-5.6-sol`, reasoning high) a first-principles
|
||||
design question. It was run from an empty folder, read-only, told not to read files, and it read
|
||||
none (0 commands). It got the game's goals and constraints, never the code. The prompt and the
|
||||
answer are verbatim below.
|
||||
|
||||
## Orchestrator notes (checked against the code, 2026-09-15)
|
||||
- **Determinism risk confirmed in our code.** Sol warns against platform transcendentals on the
|
||||
path that decides the world. VoxelForge has 34 `FMath::Sin/Cos` calls in generation code:
|
||||
per-sample strata line/rib detail (`VoxelGenerator.cpp:4708, 4738, 5723`;
|
||||
`VoxelDensityOpStack.cpp:7507, 7609`) and shape building (passage spirals/cascades
|
||||
`VoxelStrateManager.cpp:1830-2142`, option B winding waves `VoxelCaveMorphology.cpp:6826-6843`,
|
||||
tunnel pinches `7487-7521`). The MSVC C runtime switches its math functions to FMA3
|
||||
implementations at runtime on CPUs that support it (`_set_FMA3_enable`), so bit-identical results
|
||||
across machines are not guaranteed. Found by reading, not measured, and not changed yet.
|
||||
- **The largest gap is the loop direction.** Every sample searches a spatial index for nearby
|
||||
primitives. Sol's recommended architecture rasterises each feature into the bricks it touches.
|
||||
Coarse LOD from finer data and shared halo samples are also absent.
|
||||
- Already shared with Sol's design: built-first traversal (walk forest, D-floors, option B),
|
||||
hash-keyed randomness, the collision gate, and partial narrow-band skipping (reach proofs, the
|
||||
sealed-rock proof).
|
||||
|
||||
---
|
||||
|
||||
## The prompt, as sent
|
||||
|
||||
# If you had to build this from scratch, in every better way: how would you do it?
|
||||
|
||||
You are being asked for **architecture**, not a code review. There is no code here on purpose: we want
|
||||
your own design, not a critique of ours. Think freely, then be concrete.
|
||||
|
||||
⛔ **Do not read, search or open any files on this machine, and do not run commands.** Answer purely
|
||||
from the description below and your own knowledge. Anchoring on an existing implementation is
|
||||
exactly what this question is trying to avoid. Your final message is the whole deliverable.
|
||||
|
||||
## The game
|
||||
An underground, multiplayer mining and exploration game in Unreal Engine 5.7 (C++). The world is a
|
||||
vertical stack of **strates**: thick horizontal layers, each a distinct underground world (tunnel
|
||||
networks, big chambers, mazes, shafts, floating islands, flooded areas, an open "surface" layer lit
|
||||
from within). Strates are sealed from each other by solid rock, except through a few authored
|
||||
**passages** that the player walks through to go deeper. Players spawn at the world origin and
|
||||
travel down. They **mine and carve** the terrain freely.
|
||||
|
||||
## Hard constraints: any design must satisfy these
|
||||
1. **Deterministic across machines.** Every client generates the world locally from a seed. Only the
|
||||
seed, the strate layout and players' terrain edits are replicated, never geometry. The same build
|
||||
with the same seed must produce the exact same world on every machine (x64 CPUs; a GPU path would
|
||||
have to prove cross-vendor/driver bit-exactness).
|
||||
2. **Composable generation.** Worlds are not hand-authored. A "composer" invents strates from tagged,
|
||||
authored building blocks (room shapes, tunnel styles, noise detail, terrain features) and a seed.
|
||||
The generator must stay a composition of operators that can be recombined, and archetypes can mix
|
||||
inside one strate.
|
||||
3. **Traversal guarantees.** From the point where a player arrives in a strate to where they leave,
|
||||
there must always be a walkable route (player capsule ~1.8 m tall, ~0.34 m radius, 45 cm step,
|
||||
~44 degree max walkable slope). Tunnels have walkable D-shaped floors, wind when a direct route
|
||||
would be too steep, and may have dramatic climbable drops only where a walkable alternative
|
||||
exists. Rooms cut cleanly into tunnels.
|
||||
4. **Terrain is volumetric and editable.** Caves, overhangs and arbitrary mining, so not a
|
||||
heightfield. Edits must re-mesh quickly and deterministically.
|
||||
5. **Streaming must keep up with a running player** (~8 m/s, faster when falling), with collision
|
||||
ready under the player before they can stand there. Far terrain is visible at lower detail.
|
||||
6. Resolution: ~25 cm voxels near the player. Target PCs: 8-core/16-thread desktop CPUs and a decent
|
||||
GPU.
|
||||
|
||||
## What we would like from you
|
||||
1. **Two or three genuinely different architectures** for generation, representation, LOD,
|
||||
meshing, streaming and editing that satisfy ALL the constraints. For each: how the terrain is
|
||||
represented and evaluated, where the work happens, how LODs are produced, how edits work, how
|
||||
determinism is guaranteed, the expected performance character, and the biggest risks.
|
||||
2. **Where the real performance wins are**, compared with the obvious design ("for every voxel
|
||||
sample, evaluate every nearby shape and noise"). Be specific: algorithms and data structures.
|
||||
3. **What you would deliberately NOT build**, and why.
|
||||
4. **If you could only change three things** in a working implementation of the obvious design,
|
||||
what would they be?
|
||||
|
||||
Be concrete. "Use the GPU" or "use ECS" is not an answer unless it addresses constraint 1 and the
|
||||
composition requirement. Say plainly where you are unsure.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Sol's answer, verbatim
|
||||
|
||||
I would make the deterministic artifact a compact “world plan,” then compile that plan locally into terrain. The plan describes topology, portals, feature instances, archetype regions, and generation-version hashes—not voxels or meshes.
|
||||
|
||||
My preferred implementation would be a feature-rasterized sparse brick volume. It retains operator composition, but executes operators feature-by-feature into the bricks they touch instead of asking every voxel about every feature. That is usually the best balance of predictable performance, editability, and determinism.
|
||||
|
||||
Two constraints need precise interpretation:
|
||||
|
||||
- If traversal must remain guaranteed after arbitrary player edits, “mine freely” and “always walkable” conflict. Players can mine away every possible floor. You must either protect a route, reject edits that destroy the last route, or automatically create a replacement.
|
||||
- Collision readiness cannot be guaranteed under arbitrary stalls or unbounded falling using prefetch alone. A hard guarantee requires bounded terminal velocity plus admission control, an immediate conservative collision proxy, or both.
|
||||
|
||||
Everything below assumes generated terrain must guarantee traversal. For post-edit traversal, I would use a server-enforced “last-route covenant”: ordinary mining is unrestricted, but an edit that would destroy the final walkable route is rejected or replaced with an authored support/bridge.
|
||||
|
||||
## Common foundation
|
||||
|
||||
Before discussing terrain representations, all three architectures should share the same deterministic world-planning layer.
|
||||
|
||||
### 1. The composer produces a WorldPlan
|
||||
|
||||
For each strate, the composer creates:
|
||||
|
||||
- Bounds and interface rock thickness.
|
||||
- Entry and exit passage transforms.
|
||||
- A mandatory traversal graph.
|
||||
- Optional chambers, side routes, drops, shafts, islands, flooded regions, and rewards.
|
||||
- Spatial archetype regions and their tags.
|
||||
- Feature instances and their stable IDs.
|
||||
- Material and detail rules.
|
||||
- A generation-format version and content-pack hash.
|
||||
|
||||
The replicated “strate layout” could be the full WorldPlan or only the high-level choices needed to reconstruct it. I favor replicating the compact plan: it prevents small composer changes from silently changing an existing saved world while still avoiding geometry replication.
|
||||
|
||||
Adjacent strates share authored portal contracts. The interface is solid by default, with only explicitly reserved passage volumes allowed to cross it. Cave noise is bounded and cannot affect the interface exclusion zone.
|
||||
|
||||
### 2. Traversal is constructed, not discovered afterward
|
||||
|
||||
Generate the mandatory route before decorative terrain:
|
||||
|
||||
1. Place entry, exit, and required intermediate anchors.
|
||||
2. Route a centerline through a coarse 3D planning grid.
|
||||
3. Include elevation, heading, and accumulated grade in the search state.
|
||||
4. When a direct segment exceeds the design grade, generate switchbacks or a spiral.
|
||||
5. Sweep a guaranteed-clear tunnel profile along that centerline.
|
||||
6. Add rooms and optional routes.
|
||||
7. Apply destructive detail and noise only outside the protected clearance envelope.
|
||||
8. Run an independent capsule validator as an assertion.
|
||||
|
||||
I would design the mandatory route below the mechanical limit—perhaps 35–38 degrees rather than 44—to leave room for meshing, collision quantization, and network movement tolerances.
|
||||
|
||||
The protected route volume is the capsule’s configuration space: at least the capsule radius plus margin laterally, enough headroom vertically, and a conservative floor envelope. It is stronger than merely checking whether an air voxel exists.
|
||||
|
||||
D-shaped tunnels should come from a swept profile with:
|
||||
|
||||
- A controlled floor curve.
|
||||
- An arched roof.
|
||||
- Fixed-point parallel-transport frames along the centerline.
|
||||
- Junction transition pieces where tunnels meet rooms.
|
||||
- A “no detail displacement” band around the walking surface.
|
||||
|
||||
A dramatic drop is added only as a graph chord or optional branch. It never replaces a mandatory walkable graph edge. Graph bridges and articulation points make that easy to verify.
|
||||
|
||||
The final validator should perform capsule sweeps or search through capsule-eroded free space at collision resolution. If it fails, retry only the optional layout using a deterministic attempt number; do not rely on an unconstrained random retry loop.
|
||||
|
||||
### 3. Determinism is an explicit numeric ABI
|
||||
|
||||
I would not trust “same C++ code on x64” by itself.
|
||||
|
||||
The generation contract would require:
|
||||
|
||||
- Integer or fixed-point world coordinates, preferably `int64`.
|
||||
- Fixed-point density and interpolation with specified rounding and saturation.
|
||||
- Integer lattice noise with fixed lookup tables—no platform `sin`, `pow`, or floating-point noise libraries.
|
||||
- Counter-based random values keyed by tuples such as
|
||||
`(world seed, strate ID, feature ID, decision kind, attempt)`.
|
||||
- No shared sequential PRNG streams.
|
||||
- Canonical ordering by stable ID whenever operations are noncommutative.
|
||||
- No dependence on hash-map iteration order, thread scheduling, pointer values, or allocation order.
|
||||
- Canonical triangulation, vertex ordering, ambiguity resolution, and material selection.
|
||||
- Explicit overflow behavior and wide intermediate arithmetic.
|
||||
- A generation-version/content hash checked during multiplayer connection and save loading.
|
||||
|
||||
Floating point can be used after canonical geometry exists—for rendering transforms, shading, and other non-authoritative work. It should not determine topology, occupancy, route selection, or edit results.
|
||||
|
||||
GPU work is limited to rendering, culling, upload, and possibly non-authoritative visual effects. I would not make GPU field evaluation or meshing canonical unless an integer-only implementation had been proven bit-exact across vendors and drivers.
|
||||
|
||||
---
|
||||
|
||||
# Architecture A: Lazy compiled implicit field
|
||||
|
||||
This is the most flexible architecture for arbitrary procedural shapes.
|
||||
|
||||
## Representation and evaluation
|
||||
|
||||
The WorldPlan is compiled into an immutable field program. Operators include:
|
||||
|
||||
- Room primitives.
|
||||
- Swept tunnel primitives.
|
||||
- Terrain unions and subtractions.
|
||||
- Bounded displacement/noise.
|
||||
- Material masks.
|
||||
- Authored feature fields.
|
||||
- Player edit operators.
|
||||
|
||||
The field is a deterministic integer scalar field, not necessarily a mathematically exact signed-distance field. Only its sign, interpolation, bounds, and material semantics need to be defined precisely.
|
||||
|
||||
Every feature has:
|
||||
|
||||
- A conservative bounding box.
|
||||
- A bound on field contribution.
|
||||
- A stable ID and priority.
|
||||
- A reference to a specialized evaluation kernel.
|
||||
|
||||
A spatial index maps macro-cells and bricks to relevant operators. When a chunk is requested, its operator set is gathered once, canonically sorted, simplified, and compiled into a chunk-local program.
|
||||
|
||||
Examples of compilation:
|
||||
|
||||
- Collapse unions of compatible room primitives into one loop.
|
||||
- Separate terrain topology from material evaluation.
|
||||
- Group identical tunnel kernels.
|
||||
- Remove operators whose bounds cannot influence the chunk.
|
||||
- Create archetype masks so that a noise style is evaluated only in its own region.
|
||||
- Propagate maximum noise amplitude for later pruning.
|
||||
|
||||
## LOD and meshing
|
||||
|
||||
Use world-aligned sparse bricks, for example:
|
||||
|
||||
- 4 m brick: `16³` cells at 25 cm.
|
||||
- 8 m mesh page: `2³` bricks.
|
||||
- 32–64 m macro-cell for planning and indexing.
|
||||
|
||||
Sample only requested bricks and their one-cell halos. Cache corner samples so neighboring chunks do not recompute them.
|
||||
|
||||
LOD is generated by evaluating the same field on world-aligned grids at spacings of 25, 50, 100 cm, and so on. An interval hierarchy records minimum and maximum possible density over coarse nodes:
|
||||
|
||||
- If the interval is entirely solid or air, no finer samples are needed.
|
||||
- If it crosses zero, refine.
|
||||
- Mandatory passages can be marked as topology-critical so coarse visual LOD does not accidentally seal them.
|
||||
|
||||
For meshing, I would use either:
|
||||
|
||||
- Fixed-point surface nets with deterministic vertex placement, or
|
||||
- Deterministic marching cubes with an integer asymptotic decider.
|
||||
|
||||
Dual contouring can produce better sharp features, but its QEF solver must be fixed-point with completely specified pivoting and rounding. A conventional floating-point QEF is a determinism trap.
|
||||
|
||||
Neighboring LODs remain 2:1 balanced and use fixed transition-cell tables. Every page owns vertices according to a canonical boundary rule, avoiding duplicate and differently rounded seam vertices.
|
||||
|
||||
LOD0 supplies the canonical collision triangles. Far LOD is render-only.
|
||||
|
||||
## Edits
|
||||
|
||||
Edits are quantized CSG stamps:
|
||||
|
||||
- Shape and dimensions.
|
||||
- Fixed-point transform.
|
||||
- Operation: mine, add, replace material, and so forth.
|
||||
- Server sequence number.
|
||||
- Author and optional gameplay metadata.
|
||||
|
||||
The server assigns ordering for noncommutative operations. Pure subtraction can be treated as commutative, but I would still preserve a total order for debugging and save compatibility.
|
||||
|
||||
Edits are indexed spatially. A changed edit invalidates only intersected bricks plus the meshing halo. Long edit lists are deterministically folded into per-brick edit blocks or checkpoints; the checkpoint is still player-edit data, not base-world geometry.
|
||||
|
||||
## Performance character
|
||||
|
||||
Advantages:
|
||||
|
||||
- Excellent composability.
|
||||
- Low persistent memory.
|
||||
- Arbitrary procedural primitives remain natural.
|
||||
- Any LOD can be evaluated directly.
|
||||
- Untouched, distant regions never need materialization.
|
||||
|
||||
Costs:
|
||||
|
||||
- Worst-case chunks can still have expensive field evaluation.
|
||||
- Domain warping and heavily overlapping operators weaken spatial bounds.
|
||||
- Repeated edits require compaction or their query cost grows indefinitely.
|
||||
- Fixed-point vector math and meshing require careful engineering.
|
||||
|
||||
The main risk is believing the spatial index alone solves performance. If every surface sample still evaluates 40 candidate features and six noise octaves, this architecture can miss streaming deadlines.
|
||||
|
||||
---
|
||||
|
||||
# Architecture B: Feature-rasterized sparse brick volume
|
||||
|
||||
This is the architecture I would most likely ship.
|
||||
|
||||
Instead of the field being the runtime truth, the WorldPlan is compiled into canonical sparse voxel bricks. Operators remain composable, but execute in bulk.
|
||||
|
||||
## Representation and generation
|
||||
|
||||
Each brick is one of:
|
||||
|
||||
- Uniform solid.
|
||||
- Uniform air.
|
||||
- Compressed material-only block.
|
||||
- Dense or narrow-band fixed-point density samples.
|
||||
- Edited dense block.
|
||||
|
||||
The generator processes features rather than samples:
|
||||
|
||||
1. Determine which bricks a tunnel, room, or feature intersects.
|
||||
2. Rasterize that feature into those bricks using a specialized kernel.
|
||||
3. Apply phases in a canonical order: structural voids, rooms, optional features, surface detail, materials, edits.
|
||||
4. Never touch bricks outside the feature’s conservative bounds.
|
||||
|
||||
A swept tunnel rasterizer iterates its centerline segments and their affected scanlines. A room stamp writes spans or masks. Authored building blocks can contain canonical sampled volumes and be placed using resampling-free transforms such as 90-degree rotations and reflections. More general transforms use a precisely specified fixed-point resampler.
|
||||
|
||||
For boolean-only phases, contributions can be accumulated independently and reduced in any order. Noncommutative phases use sorted feature IDs and ping-pong channels.
|
||||
|
||||
Noise becomes a bulk brick kernel. Better still, it runs only on bricks already known to contain a possible surface. Bounded noise cannot turn a deeply solid or deeply empty brick into a surface brick.
|
||||
|
||||
A sparse radix tree or hashed macro-cell table owns bricks. The storage itself need not have deterministic iteration order, because all externally visible processing uses sorted integer coordinates.
|
||||
|
||||
## LOD and meshing
|
||||
|
||||
Every brick produces a small hierarchy containing:
|
||||
|
||||
- Minimum density.
|
||||
- Maximum density.
|
||||
- Material summary.
|
||||
- Surface-presence mask.
|
||||
- Optional conservative solid/air coverage.
|
||||
|
||||
Parent LOD bricks are derived using an exact integer reduction. A simple averaged density is insufficient because it can erase thin walls or close tunnels. Min/max information decides whether a coarse region is definitely homogeneous or must retain detail.
|
||||
|
||||
Mesh generation reads the brick and its halo, then uses the same deterministic extractor and transition cells as Architecture A.
|
||||
|
||||
A useful split is:
|
||||
|
||||
- 25 cm render and collision near players.
|
||||
- Potentially coarser conservative collision away from traversal-critical regions.
|
||||
- Far render pages assembled from brick hierarchy nodes.
|
||||
|
||||
## Edits
|
||||
|
||||
Edits operate directly on affected brick spans or samples. This makes mining latency extremely predictable:
|
||||
|
||||
- Copy-on-write the changed brick.
|
||||
- Apply the quantized edit kernel.
|
||||
- Recompute only its mip ancestry.
|
||||
- Remesh it and its boundary neighbors.
|
||||
- Recook only affected collision pages.
|
||||
|
||||
Keep the original generated brick reconstructible from the WorldPlan. A save stores the edit operations or a canonical edit-derived brick delta, never the generated base brick.
|
||||
|
||||
## Performance character
|
||||
|
||||
Advantages:
|
||||
|
||||
- Work is proportional to affected brick volume, not `samples × candidate operators`.
|
||||
- Excellent cache locality and straightforward SIMD.
|
||||
- Uniform regions are extremely cheap.
|
||||
- Edits are fast and bounded.
|
||||
- Collision and LOD data naturally derive from the same representation.
|
||||
- Performance is more predictable than lazy per-sample evaluation.
|
||||
|
||||
Costs:
|
||||
|
||||
- Higher memory use.
|
||||
- A requested area must be materialized before use.
|
||||
- Poorly bounded features can still touch too many bricks.
|
||||
- Large filled volumes are wasteful unless uniform-brick detection happens early.
|
||||
- Authored sampled blocks can become repetitive if the composer does not provide enough transforms and procedural variation.
|
||||
|
||||
The main risk is turning it into a dense voxel world. The architecture works because homogeneous regions remain implicit and only surface/edit bricks become dense.
|
||||
|
||||
---
|
||||
|
||||
# Architecture C: Deterministic shell-first base with local voxelization for edits
|
||||
|
||||
This is a genuinely different option and can be extremely fast when most terrain remains untouched. It is also the most difficult to engineer robustly.
|
||||
|
||||
## Representation and generation
|
||||
|
||||
The unedited world is represented as deterministic watertight boundary patches:
|
||||
|
||||
- Polygonal swept tunnels.
|
||||
- Parametric room shells.
|
||||
- Terrain surface patches.
|
||||
- Portal and junction patches.
|
||||
- Bounded displacement patches.
|
||||
|
||||
Solid rock is implicit outside the air shells. There is no full base density volume.
|
||||
|
||||
All topology decisions use fixed-point or exact rational predicates. Curves are canonical polygonal approximations generated from quantized tables. Intersections and room/tunnel cuts use a deliberately restricted exact boolean system—not a general floating-point mesh boolean library.
|
||||
|
||||
The WorldPlan spatially indexes these patches. Untouched chunks triangulate only the surface patches that intersect them.
|
||||
|
||||
## LOD and meshing
|
||||
|
||||
Base patches have deterministic subdivision hierarchies. Adjacent patches share boundary vertex schedules, so independently generated LODs meet exactly. Far LOD selects coarser patch subdivisions.
|
||||
|
||||
When a player edits terrain, the affected area switches representation:
|
||||
|
||||
1. Allocate sparse voxel bricks around the edit.
|
||||
2. Classify the base shell into those bricks.
|
||||
3. Apply the edit operations.
|
||||
4. Mesh the local volume.
|
||||
5. Use a collar region whose boundary samples match the untouched shell.
|
||||
|
||||
An edit completely inside solid rock begins from a uniform-solid brick and creates a new cavity. An edit touching a cave wall classifies the nearby base shell first.
|
||||
|
||||
Collision uses base shell triangles in untouched regions and voxel-derived triangles in edited regions.
|
||||
|
||||
## Determinism
|
||||
|
||||
The shell topology, clipping predicates, tessellation, and triangle order must all be integer/rational and canonical. Conventional runtime mesh booleans are not acceptable.
|
||||
|
||||
## Performance character
|
||||
|
||||
Advantages:
|
||||
|
||||
- Untouched terrain costs roughly proportional to visible surface area.
|
||||
- No field evaluation across solid interiors.
|
||||
- Very cheap far LOD.
|
||||
- Works especially well for huge chambers and relatively localized mining.
|
||||
- Traversal proxy geometry is available immediately from the route plan.
|
||||
|
||||
Costs and risks:
|
||||
|
||||
- Exact, watertight joins between arbitrary procedural shapes are hard.
|
||||
- Noisy displacement and multi-way junctions are particularly troublesome.
|
||||
- The boundary between shell terrain and voxelized edited terrain is complex.
|
||||
- Large-scale mining gradually converts the world into Architecture B anyway.
|
||||
- Tooling and debugging burden are substantially higher.
|
||||
|
||||
I would consider this only if profiling showed base-world generation dominating and player edits remained spatially sparse. I would not choose it as the first production architecture.
|
||||
|
||||
---
|
||||
|
||||
# Streaming and collision
|
||||
|
||||
The streaming scheduler should be shared across architectures and driven by deadlines, not simple distance.
|
||||
|
||||
Maintain separate interest sets:
|
||||
|
||||
- Immediate safety set: LOD0 collision required.
|
||||
- Near visual set: LOD0 or LOD1 meshes.
|
||||
- Predicted movement set: locations reachable within a time horizon.
|
||||
- Far visual set.
|
||||
- Edit-dirty set.
|
||||
- Passage prefetch set for the next strate.
|
||||
|
||||
Priority is approximately “time until the player could touch this page,” accounting for:
|
||||
|
||||
- Current velocity and acceleration.
|
||||
- Character braking.
|
||||
- Network prediction uncertainty.
|
||||
- Gravity and terminal fall velocity.
|
||||
- Nearby shaft connectivity.
|
||||
- Other players on the authoritative server.
|
||||
|
||||
A vertical fall requires a swept ballistic volume, not a horizontal streaming radius. Known shafts and authored passages should prefetch their entire reachable landing corridor as soon as a player approaches them.
|
||||
|
||||
The worker pipeline is:
|
||||
|
||||
`plan/index lookup → field sampling or brick rasterization → mesh → collision cook → game-thread publication`
|
||||
|
||||
Each stage has revision numbers and supports cancellation. Results are published only if their input revision is still current.
|
||||
|
||||
Collision receives the highest priority. For a hard guarantee, I would use both:
|
||||
|
||||
- Immediately available conservative collision proxies derived from the route plan.
|
||||
- Movement admission: a character cannot cross into a page whose required collision revision is not active.
|
||||
|
||||
The latter should almost never trigger, but without it “collision will always be ready” is only a performance hope. Under severe overload, slowing a character at a streaming boundary is better than letting them fall through the world.
|
||||
|
||||
On Unreal’s side, generation data should remain plain C++ memory off-thread. UObject/component creation and scene publication happen on the game thread. I would use a custom terrain scene proxy and a dedicated collision provider rather than one heavy actor/component per tiny brick.
|
||||
|
||||
The server generates collision for all relevant players and remains authoritative. Clients generate the same static terrain for prediction and rendering. Physics simulation itself does not need cross-machine determinism as long as the server is authoritative, but the collision input triangles must still be canonical.
|
||||
|
||||
---
|
||||
|
||||
# Where the real performance wins are
|
||||
|
||||
The dominant improvement is changing the work from:
|
||||
|
||||
`number of samples × number of nearby shapes × number of noise layers`
|
||||
|
||||
toward:
|
||||
|
||||
`features touching requested bricks + cells near actual surfaces + dirty edit cells`
|
||||
|
||||
Specifically:
|
||||
|
||||
## 1. Feature-to-brick compilation
|
||||
|
||||
Build a macro-cell index when the WorldPlan is created. A requested brick gets a small, already-filtered operator program.
|
||||
|
||||
Better still, Architecture B iterates each feature over its covered cell spans. A sphere or swept tunnel should rasterize scanlines; it should not be rediscovered independently by every voxel sample.
|
||||
|
||||
Use separate indices for immutable world features and mutable edits.
|
||||
|
||||
## 2. Interval bounds and narrow-band evaluation
|
||||
|
||||
Every primitive and noise stack should expose conservative bounds.
|
||||
|
||||
If the base field for a region is farther from zero than the maximum possible displacement, skip all detail noise. This is especially important for expensive domain warps.
|
||||
|
||||
For an octree node:
|
||||
|
||||
- Entirely solid: emit no samples.
|
||||
- Entirely air: emit no samples.
|
||||
- Possibly crosses the surface: subdivide or evaluate.
|
||||
|
||||
This often removes more work than SIMD optimization.
|
||||
|
||||
## 3. Stop evaluating every archetype everywhere
|
||||
|
||||
Give archetypes spatial masks determined at planning resolution. A crystal-cavern noise stack should not even enter the chunk program for a neighboring sedimentary tunnel.
|
||||
|
||||
At transitions, compile only the two or three contributing archetypes and a deterministic blend mask.
|
||||
|
||||
## 4. Cache lattice samples and halos
|
||||
|
||||
Adjacent cells and chunks share corners. Cache canonical world-grid samples by brick revision. Do not evaluate each corner up to eight times, and do not let two chunks compute a shared boundary using different local coordinate paths.
|
||||
|
||||
## 5. Use bulk data layouts
|
||||
|
||||
For dense bricks:
|
||||
|
||||
- Structure-of-arrays density and material channels.
|
||||
- Fixed-size aligned slabs.
|
||||
- SIMD integer kernels.
|
||||
- Bitmasks for active surface cells.
|
||||
- Scanline/span writes for primitives.
|
||||
- Small thread-local scratch arenas.
|
||||
- No allocation in inner loops.
|
||||
|
||||
Bitsets are particularly effective for “possibly contains surface,” material-presence, and dirty-cell masks.
|
||||
|
||||
## 6. Incremental edit dependency tracking
|
||||
|
||||
An edit should invalidate:
|
||||
|
||||
- Intersected bricks.
|
||||
- One-cell mesh halos.
|
||||
- Their LOD ancestors.
|
||||
- Their collision pages.
|
||||
|
||||
Nothing else.
|
||||
|
||||
Give every brick separate generation, edit, mesh, and collision revisions. This prevents a late worker result from publishing obsolete terrain.
|
||||
|
||||
An ever-growing edit list is not viable. Compact edits into canonical per-brick deltas after a threshold while preserving the original network/save semantics.
|
||||
|
||||
## 7. Separate collision urgency from visual quality
|
||||
|
||||
Generate collision first. It can initially use a conservative, lower-complexity surface as long as it preserves walkability and does not allow passage into solid rock. Refine the render mesh afterward.
|
||||
|
||||
Do not block collision readiness on material extraction, tangents, decoration, meshlets, or high-quality far-LOD work.
|
||||
|
||||
## 8. Exploit underground visibility
|
||||
|
||||
In an underground world, Euclidean distance is a poor visibility predictor. Build coarse visibility cells from the chamber/tunnel graph. Far terrain behind several sealed turns can be deprioritized even if spatially close.
|
||||
|
||||
GPU occlusion and meshlet culling help rendering, but avoiding CPU generation of invisible far surfaces is the larger win.
|
||||
|
||||
---
|
||||
|
||||
# What I would deliberately not build
|
||||
|
||||
- A dense 3D array for an entire strate. Solid rock and empty chambers must remain sparse or implicit.
|
||||
- A global sequential random stream. Adding one decoration would change the rest of the world.
|
||||
- Floating-point generation decisions, floating mesh booleans, or platform math functions in the canonical path.
|
||||
- GPU-authoritative generation or meshing without proven integer bit-exactness.
|
||||
- A generic interpreted CSG graph evaluated from its root for every sample.
|
||||
- Unlimited domain-warp noise. It destroys useful spatial and amplitude bounds.
|
||||
- A general-purpose runtime B-rep boolean engine. Architecture C should support a narrow, controlled set of exact operations.
|
||||
- Independent marching-cubes meshes at each LOD with skirts added later as a patch. LOD ownership and transition topology need to be designed from the start.
|
||||
- Runtime full-strate flood fills as the primary traversal guarantee. Connectivity should be structural; validation is a backstop.
|
||||
- A never-compacted replicated edit list.
|
||||
- One Unreal actor or UObject per voxel brick.
|
||||
- Dependence on dynamic Nanite behavior for core terrain correctness. It may be useful for rendering, but collision, edits, and determinism need an independent path.
|
||||
- Streaming based only on concentric distance rings.
|
||||
- A system where render mesh completion is the only thing preventing a player from falling through unloaded terrain.
|
||||
- A claim that arbitrary post-edit traversal is guaranteed without restricting, rejecting, or repairing edits.
|
||||
|
||||
---
|
||||
|
||||
# If I could change only three things
|
||||
|
||||
1. **Introduce a deterministic topology-first WorldPlan compiler.**
|
||||
This establishes stable IDs, counter-based randomness, portal contracts, protected walkable routes, bounded feature instances, archetype masks, and a versioned numeric ABI. It fixes correctness and gives later systems much better information.
|
||||
|
||||
2. **Replace per-sample feature iteration with feature-to-brick compilation and sparse surface bricks.**
|
||||
Build spatial indices, rasterize features in bulk where possible, apply noise only within bounded surface bands, cache shared samples, and maintain min/max LOD hierarchies. This is likely the largest CPU improvement.
|
||||
|
||||
3. **Replace distance-only chunk streaming with a revisioned, deadline-driven collision pipeline.**
|
||||
Predict reachable motion including falls, prefetch passages and shafts, prioritize collision independently, invalidate only edit dependencies, and retain a conservative collision/admission fallback. This turns streaming from “usually fast enough” into an enforceable gameplay contract.
|
||||
|
||||
My first production choice would be Architecture B, while retaining Architecture A’s immutable operator language as the authoring and compilation input. In other words: compose with procedural operators, but execute them into sparse bricks feature-by-feature. It gives up a little theoretical elegance in exchange for much more controllable frame time, edit latency, collision readiness, and debugging.
|
||||
|
||||
> Round two, with the code: see `DESIGN-SOL-2026-09-15-CODE.md`. Sol corrected two of the notes above: per-sample lookup is ~5% exclusive (not the main cost), and coarse LOD from fine data is excluded by the clipmap design, not a gap.
|
||||
@@ -0,0 +1,93 @@
|
||||
# PERF-ATTRIBUTION
|
||||
|
||||
Measured 2026-09-12 at `cf0319d` (`experimental`). This was measurement-only: the source changes add
|
||||
diagnostic counters and clocks; they do not change density or meshing decisions. The primary case is
|
||||
the 128^3 `TunnelNetwork`, seed 1337, slot 1, operator-stack setting 1, origin `(-64,-64,-224)`
|
||||
voxels, step 1, 64 canonical 32^3 tiles, and 15 workers.
|
||||
|
||||
`clean` means diagnostics off and `-blockearlyout=0`, with fused evaluation and shared density-grid
|
||||
reuse enabled. The three clean primary runs used an exact-HEAD DLL staged before instrumentation:
|
||||
603.599, 590.746, and 569.627 ms mesh time, mean **587.991 ms**. The requested 0.556 s is therefore
|
||||
within the observed run spread but was not reproduced exactly on this host. `attr` means the final
|
||||
instrumented DLL, `-perfattribution`, sample interval 128. Its wall-clock self-check compares the
|
||||
same export against an unprofiled run in the same process.
|
||||
|
||||
The prescribed build was run successfully with:
|
||||
|
||||
```text
|
||||
dotnet "E:\Program Files\Epic Games\UE_5.7\Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll" UnrealEditor Win64 Development "-Project=E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\HostProject.uproject" -WaitMutex -FromMsBuild -architecture=x64 -NoUBA "-Log=E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\PerfAttributionBuild.log"
|
||||
```
|
||||
|
||||
The measured process loaded the staged runtime DLL at
|
||||
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\Plugins\VoxelForge\Binaries\Win64\UnrealEditor-VoxelForge.dll`, SHA-256
|
||||
`C0CB812BB25EFE8C836DC1C1718C408DA5FE9D9F258B4AE12A6CA846510CA0B2`, length 7,673,856 bytes. The
|
||||
staged editor DLL was `E64C9E80869EFA53FA1556947718DB8E64975ABF6B546657BCFF5D775A2B679A`, length
|
||||
502,784 bytes. The game harness recorded the same runtime path and hash. The primary attribution and
|
||||
final classifier exports returned `status=ok`, geometry CRC `07C14005`, and the required OBJ SHA-256
|
||||
`b3e5f4c398dd0547c6c55f415872058246d4a292c2f0d636ee1cd0829de9b377`.
|
||||
|
||||
The table is the attribution. Only rows marked **additive** are summed. Profiler component rows are
|
||||
sampled nested diagnostics; their CPU and worker-wall figures are evidence about composition, not
|
||||
additional time to add to the wall ledger.
|
||||
|
||||
| Bucket / question | Measurement and mode | Elapsed time, volume, or count | Whole-clock accounting / interpretation |
|
||||
|---|---|---:|---|
|
||||
| Historical comparison | `34f06df` value supplied in the request | 130.000 ms | Historical pre-op-stack reference; the exact old binary was not rebuilt because that commit predates the current editor commandlet and host harness. |
|
||||
| Current clean primary | 3 exports, exact `cf0319d` DLL, diagnostics **off**, fused=1, reuse=1, classifier early-out=0 | **587.991 ms mean**; 569.627–603.599 ms | Clean headline mesh clock. Mean is 4.523x the supplied 130 ms reference. |
|
||||
| Primary attribution self-check | Final DLL, `attr`; profiled 606.128 ms versus same-argument unprofiled 602.341 ms | 1.0063x; +0.629% | Passed. Geometry equal. The profiler did not reproduce the 11.5x inflation seen in the earlier diagnostic mode. |
|
||||
| Full commandlet export | Final DLL, `attr`; mesh plus output and bookkeeping | **1,004.557 ms** | Full export wall, not just mesh generation. The OBJ writer is a material part of this clock. |
|
||||
| Full export: mesh generation | Additive export ledger | 606.128 ms; 60.338% of export | Includes the complete additive mesh ledger below. |
|
||||
| Full export: OBJ/output interval | Additive; includes nested `MeasureExploreMesh` | 394.273 ms; 39.249% | `measure_seconds` is 82.964 ms inside this interval and is not added again. |
|
||||
| Full export: geometry hash | Additive | 2.946 ms; 0.293% | CRC/hash bookkeeping. |
|
||||
| Full export: manifest | Additive | 0.984 ms; 0.098% | Manifest write. |
|
||||
| Full export: remainder | Additive export remainder | 0.225 ms; 0.022% | Export ledger remainder. Mesh + OBJ interval + hash + manifest + remainder = 1,004.557 ms. |
|
||||
| Mesh ledger: preparation | **Additive** | 0.000101 ms; 0.0000% of mesh | No meaningful cost. |
|
||||
| Mesh ledger: shared density-grid fill | **Additive**; 2,248,091 unique shared samples | **590.923 ms; 97.4913% of mesh** | This is the dominant remaining cost. It is the caller-side density-grid stage and includes the canonical fused `GetDensityAt` work. |
|
||||
| Mesh ledger: pre-tile setup | **Additive** | 0.001602 ms; 0.0003% | No meaningful cost. |
|
||||
| Mesh ledger: tile parallel wall | **Additive**; 64 jobs, 15 workers | 10.972 ms; 1.8102% | Per-tile cell scan, interpolation, normals, stream assembly, and task scheduling after the shared grid exists. |
|
||||
| Mesh ledger: tile merge | **Additive** | 4.232 ms; 0.6982% | Cross-tile output merge. |
|
||||
| Mesh ledger: accounting remainder | **Additive** | 0.000097 ms; 0.0000% | Stage sum 606.128205 ms versus mesh clock 606.128301 ms; `sum_matches_mesh=true`. Nothing is hidden in this remainder. |
|
||||
| Mesh ledger total | Final DLL, `attr` | **606.128301 ms** | Complete mesh wall clock. |
|
||||
| `GetDensityAt` parent | Final primary, `attr`; 2,248,091 calls, 17,558 sampled parent intervals, 15 workers | 580.184 ms worker-wall estimate | Nested inside the 590.923 ms density-grid stage. The remaining 10.738 ms is grid-loop/task/read overhead outside the parent scope; it is accounted for by the additive density-grid row. |
|
||||
| Fused evaluator itself | Final primary, `attr`; 17,571 sampled component scopes; 2,248,091 fused tunnel samples | 107.183 ms sampled CPU; 12.301 ms sampled worker wall | Diagnostic sample only, not additive. Detail ran for 1,079,777 samples and was skipped for 1,168,314, about 52.0% of fused lattice samples. |
|
||||
| Fused density core | Final primary, `attr`; sampled nested scope | 108.371 ms sampled CPU; 12.377 ms sampled worker wall | Diagnostic sample only. It is not a 108 ms total bucket and must not be added to the mesh ledger. |
|
||||
| Room/tunnel graph traversal | Final primary counters | Rooms: 11,099,891 candidates / 1,730,920 evaluated; tunnels: 28,951,420 candidates / 3,462,565 evaluated; tunnel-core: 28,951,420 candidates / 1,881,812 evaluated | Room and tunnel candidate traversal is repeated inside the common density work. This is evidence of work volume, not a separately additive clock. |
|
||||
| Room graph build and SDF | Final primary, `attr` | `RoomGraphBuild`: 77 calls, 192.146 ms sampled CPU / 19.323 ms worker wall; `RoomGraphSdf`: 12.101 ms / 0.989 ms sampled CPU/wall | The build is cold/cache construction work; SDF figures are nested samples. The room graph is not a large independent wall bucket on the primary fused route. |
|
||||
| Tunnel-core world evaluation | Final primary, `attr` | 8.857 ms sampled CPU; 0.872 ms sampled worker wall | Nested diagnostic sample. The core traversal is present, but the measured evidence does not support calling it the whole 4.3x. |
|
||||
| Post-stack structural/passage tail | Final primary, `attr`; `GetDensityAt` tail | `StructuralTail`: 20.853 ms CPU / 1.896 ms wall; `DensityStructuralPosts`: 21.909 ms / 1.990 ms; `PassageStructuralPosts`: 10.476 ms / 0.997 ms sampled CPU/wall | This is real current-only post-stack work, but it is a small sampled component relative to the 590.923 ms density-grid wall. All rows are nested and non-additive. Passage modifier was 4.579 ms CPU / 0.404 ms wall; landing-floor and room-floor samples were 1.691 ms / 0.177 ms and 0.665 ms / 0.060 ms. |
|
||||
| Floor/profile work | Final primary counters, fused route; classifier figures from the separate classifier control | Primary support-column builds/queries/checks: 0 / 0 / 0; authored-floor samples: 0; floor-profile builds 2,636; segments 12,624; backstops 6,509 tunnel and 49 room. Classifier control: 211,289 support-column builds, 2,171,763 candidates, 1,370,314 floor queries, 2,296,243 checks, 9,710 authored-floor samples, 97,950 profiles and 442,278 profile segments. | The fused primary route does not execute the classifier’s support-column path. The classifier does, and that work is part of its separate 2.057 s route, not the clean 0.588 s headline. |
|
||||
| Software tunnel cache | Final primary, `attr` | 2,248,043 hits, 48 misses, 77 cache builds | Cache lookup is not the removed verdict cache. The current tunnel cache is overwhelmingly hit; no hardware cache-miss counter was available. |
|
||||
| Classifier absent from the clean headline | Clean exact-HEAD control, diagnostics **off**, classifier early-out=0 | 0 classifier calls | The stated 0.556 s case does not pay interval proofs. It is incorrect to charge classifier time to that baseline. |
|
||||
| Classifier control when enabled | 3 clean exact-HEAD controls, diagnostics **off**, classifier early-out=1 | **2,056.596 ms mean** | +1,468.605 ms versus the clean primary mean; 3.498x the primary. This is a separate, expensive route, not the source of the clean 0.556 s. |
|
||||
| Classifier total self-check | Final DLL, `attr`, classifier early-out=1 | 1,980.835 ms profiled versus 2,013.547 ms unprofiled; 0.9838x | Passed; geometry equal. Profile overhead was -1.62% in this run. |
|
||||
| Interval proofs | Final classifier attribution; 32 sampled calls out of 4,096 | 494,976 refine/box calls; 479,872 whole-mixed, 15,104 whole-solid, 0 whole-air; 63,488 splits; max depth 4 | Proof detail is present and conservative, but its route is costly. The classifier’s scaled diagnostic work was 14,113.190 ms interval-box, 62.221 ms exact-core, and 399.770 ms exact-final microseconds; these are diagnostic work estimates, not additive wall time. |
|
||||
| Classifier repeated room traversal | Final classifier attribution | 2,041,600 room-tail queries, 933,120 evaluated; 6,030,720 exact-core cache hits; 612,864 exact-core samples; 768 exact-final samples | The classifier does repeat room-graph work. Scaled diagnostic work was 11,423.590 ms room propagation, 4,665.818 ms exact primitive, 5,815.002 ms cache-window, and 1,227.366 ms room tail microseconds. |
|
||||
| Shared grid traffic | Final primary, `attr` | 2,744,000 tile-grid samples; 2,248,091 unique shared samples; 495,909 duplicate evaluations before reuse, 0 after | The shared lattice removes duplicate evaluations, but filling the remaining 2.248 M samples is still 590.923 ms. |
|
||||
| Mesher cell classification | Final primary, `attr`; nested sample | 2,097,152 cell calls; 1.344 ms sampled worker wall | The cell loop is not the dominant wall bucket. |
|
||||
| Mesher vertex interpolation | Final primary, `attr`; nested sample | 76,808 calls; 0.012 ms sampled worker wall | Negligible in the observed sample. |
|
||||
| Mesher gradient normals | Final primary, `attr`; nested sample | 76,808 calls; 1.056 ms sampled worker wall | Small nested cost; gradient normals come from the density grid. |
|
||||
| Mesher stream building | Final primary, `attr`; nested sample | 76,808 calls; 3.140 ms sampled worker wall | Small relative to density-grid fill. Runtime stream handoff is not executed by the export commandlet. |
|
||||
| Mesher output and memory | Final primary, `attr` | 82,273 vertices, 153,346 triangles; logical output arrays 7,434,716 bytes; allocated output 8,449,820 bytes; OBJ 17,587,937 bytes; estimated working set 14,851,564 bytes | Byte counters are logical allocator/output traffic, not physical DRAM traffic. |
|
||||
| Grid read/write bytes | Final primary, `attr` | Density-grid bytes 10,976,000; shared-grid read bytes 10,976,000 | The instrumentation sees logical grid traffic. It does not see hardware cache misses, NUMA traffic, or memory-controller events. |
|
||||
| Operator-block scratch/copy | Final primary, `attr` | 0 scratch bytes, 0 copy bytes, 0 builds, 0 operators | The old interpreted block machinery is not on the canonical primary route. |
|
||||
| Operator-block control | Clean diagnostics-off controls with reuse=0 and fused=0 | 2,332.186 ms; 1,566 block builds; 2,486,750 block samples; 31,320 operator visits; 99,470,000 scratch bytes; 101,956,750 copy bytes | This is a deliberately separate block-path control, not the clean fused baseline. It demonstrates the traffic that is absent from the primary route. |
|
||||
| Scalar and fused no-reuse controls | Clean diagnostics-off controls with reuse=0 | Scalar 2,733.080 ms; fused 2,218.132 ms | These controls show the cost of removing shared-grid reuse, but they are not attribution buckets for the primary run. |
|
||||
| What `34f06df` did not do | Read-only source comparison at the historical commit | No active TunnelNetwork operator-stack evaluation, no fused evaluator, no current post-stack tunnel-floor/structural tail, no current editor export commandlet | The old source already had a shared density grid, `ClassifyTile`, marching-cubes cell classification, interpolation, grid-gradient normals, and stream construction. Those are not new current-only work. The old source also had room/tunnel morphology and passage carving, so “rooms/tunnels exist” alone is not a delta. |
|
||||
| Primary operation-level attribution gap | Final primary profiler’s own ledger | Sampled phase sum 14.443 ms versus sampled `GetDensityAt` parent 580.184 ms; sampling gap 565.741 ms | This is the honest limit: component scopes were sampled to keep profiled/unprofiled totals within 0.63%, so their expanded operation-level wall shares are not identifiable from this run. The full mesh wall is nevertheless closed by the additive stage ledger, with a 0.000097 ms remainder. |
|
||||
| Headless game clean check | `PerfAttribGameCleanFinal_20260912`; diagnostics **off**, tile profiling off; 343 LOD0-ready samples | request-to-ready p95 **0.375470 s**; queue wait p95 0.082292 s; generation p95 0.309538 s; collision cook p95 0.052945 s | Secondary check agrees with the earlier conclusion that queue wait is not the main generation constraint. |
|
||||
| Headless game attribution check | `PerfAttribGameAttrFinal_20260912`; diagnostics **on**, tile profiling on; 834 profile lines, 822 applied | request-to-ready p95 **0.395715 s**; queue wait p95 0.083363 s; generation p95 0.315281 s; collision cook p95 0.061387 s | Attribution overhead was modest in the end-to-end game summary. Tile-profile p95 was 1.295847 s, of which classifier was 0.106626 s, mesh 1.284040 s, stream building 0.000361 s, room work 0.008842 s, and stack work 0.012019 s; these game tiles are not the same workload as the single 128^3 export. |
|
||||
|
||||
The surprise is that the clean primary is almost entirely the shared density-grid fill: the ordinary
|
||||
mesher stages after it total only about 15.204 ms. The fused evaluator is genuinely on the canonical
|
||||
path, and the room/tunnel candidate counters show substantial repeated traversal, but the sampled
|
||||
component timers cannot honestly turn that 590.923 ms into exact per-operation percentages without
|
||||
reintroducing the profiler inflation this round was designed to avoid. The classifier is an even
|
||||
larger cost when enabled, yet it is absent from the clean baseline. The old path already performed
|
||||
the grid and marching-cubes phases, so those phases should not be described as newly introduced
|
||||
work; the current-only items are the active fused route and the later structural/floor machinery.
|
||||
|
||||
What could not be attributed: physical allocation-event counts, physical memory/cache misses, and an
|
||||
exact numeric per-operation delta against the old `34f06df` binary. The old commit has no compatible
|
||||
current export commandlet, and rebuilding it would not be the same measurement harness. The complete
|
||||
current clocks, deterministic geometry, clean/profiled comparison, current counters, and all logical
|
||||
memory-byte counters are accounted for above. Existing logs and outputs under `Saved\` were retained.
|
||||
@@ -0,0 +1,371 @@
|
||||
# WORK-NEXT — the queue after the read-only audit (2026-09-11)
|
||||
|
||||
Ordered by importance, then by size. Everything here comes from Sol's read-only audit of
|
||||
`experimental` @ `aa62971`, checked against `main`.
|
||||
|
||||
The audit's two-sentence verdict:
|
||||
> *The operator abstraction leaked into the innermost loop, and streaming treats
|
||||
> "work submitted/applied" as "safe to stand on."*
|
||||
|
||||
**Standing rules for every item**: determinism is absolute (same build + same seed = same world on
|
||||
every machine); output may change between builds; nothing is deleted to go fast; capability before
|
||||
speed (player-fit 20,830 / walk-reachable 10,909 — do not drop below); scope is
|
||||
`Plugins\VoxelForge` only, no links, absolute `-out=` paths under the plugin's own `Saved\`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Collision-ready before player-ready · CORRECTNESS · medium
|
||||
|
||||
The player falls through the world. There is **no invariant** that a tile you occupy has collision.
|
||||
`main` has no such barrier either — it was simply fast enough to hide the race.
|
||||
|
||||
- `apply_s` ends at mesh **submission**. RMC cooks collision **asynchronously** and we never record
|
||||
cook-complete. We have never measured when physics can actually collide.
|
||||
- A tile enters `LoadedTiles` before its apply path completes (`VoxelWorld.cpp:1532`).
|
||||
- Only LOD0 carries collision (`VoxelWorld.cpp:3109`), so visible coarse terrain can exist while the
|
||||
collidable tile is still queued.
|
||||
|
||||
**Do**: record cook-complete; gate the pawn (spawn and movement) on the supporting tile being
|
||||
collidable, not merely applied.
|
||||
|
||||
✅ **Owner playtest, 2026-09-13: he can no longer fall through the world.** The gate uses RMC's
|
||||
`SetCollisionConfig()` future as the authority (`HandleTileCollisionCookComplete`,
|
||||
`IsPlayerSupportCollisionReady`).
|
||||
|
||||
📖 **Reading `Plugins/RealtimeMeshComponent` is permitted for this item — READ ONLY.** Find whether a
|
||||
cook-complete delegate/flag exists. ⛔ No edits, no builds against it: the owner's copy carries custom
|
||||
modifications and was destroyed once by an over-broad delete.
|
||||
|
||||
## 2. Lower the operator graph into a fused evaluator · PERFORMANCE · large
|
||||
|
||||
This is where the ~8× lives (pre-op-stack `34f06df` did 128³ in 0.130 s; we are at ~1.04 s).
|
||||
|
||||
A LOD0 tile samples 42,875 positions. The old path short-circuited the whole cave suite per sample
|
||||
when `bNearCaveSurface` was false — roughly **70% of deep-solid samples**. We now allocate a rich
|
||||
`FVoxelOpSample` per position, run each active operator across the entire block, use scalar fallbacks
|
||||
for most operators, copy scratch in and out, and still call scalar `GetDensityAt` per grid point.
|
||||
|
||||
> *An operator whose interval intersects the surface anywhere in the block runs across the whole
|
||||
> block; the old path could decide per sample that expensive detail work was irrelevant.*
|
||||
|
||||
**Do**: keep the authored operator graph, but **compile/lower it** into a fused evaluator that restores
|
||||
per-sample short-circuiting and stops spilling `FVoxelOpSample` through memory per operator.
|
||||
⚠️ Byte-identical geometry is the acceptance test. Stage it so the tree never breaks.
|
||||
|
||||
Note: the D-floors/terracing features are only ~17% of the penalty — not the 8×.
|
||||
|
||||
## 3. Cancel obsolete work, and prioritise the floor · BOTH · medium
|
||||
|
||||
Nothing aborts a tile when the player moves and it stops being desired. Work is distance-sorted from
|
||||
an artificial chunk centre, not the pawn, with no collision-critical priority
|
||||
(`VoxelWorld.cpp:1864`). Up to 14 workers can be busy with stale coarse tiles while the player's LOD0
|
||||
floor waits.
|
||||
|
||||
**Do**: abort no-longer-desired tiles; sort from the true pawn position; give the occupied tile and the
|
||||
next tile along the heading absolute priority.
|
||||
|
||||
## 4. Collapse the duplicated field semantics · PERFORMANCE ARCHITECTURE · medium-large
|
||||
|
||||
The effective field now lives in three places: the operator stack, the classifier's parallel interval
|
||||
logic, and a post-stack repair tail in `GetDensityAt` (origin floors, passage structure, passage
|
||||
floors, room floors). `FRoomGraphSource` evaluates the room graph then traverses tunnels *again* for
|
||||
metadata (`VoxelDensityOpStack.cpp:4234`).
|
||||
|
||||
The audit calls this *"the clearest sign of overengineering"*: later fixes compensating for where an
|
||||
operation sits in the pipeline. It also makes interval proofs harder to trust.
|
||||
|
||||
**Do**: make the stack the single semantic source of truth. Likely overlaps heavily with item 2.
|
||||
|
||||
**Promoted 2026-09-12**: this is performance architecture, not maintenance. The classifier is a
|
||||
second evaluator (interval proofs, exact certificates, and final-field validation) beside the
|
||||
canonical lowered evaluator, and the runtime can validate the same tile again before meshing.
|
||||
Items 1 and 2 in the current round deliberately leave this lowering work untouched; any
|
||||
classifier retained later must consume the lowered representation and reusable density data.
|
||||
|
||||
## 5. Verdict cache removed after moving-session audit · complete
|
||||
|
||||
The clean moving-session audit found zero outer or nested hits: 2,109 outer probes and 19,904 nested
|
||||
probes produced 15,877 stores and 15,365 replacements in the full 512-entry table. Same-binary
|
||||
cache-on/cache-off request-to-ready was 0.367645/0.365978 s p95 and generation was 0.285559/0.285992
|
||||
s p95, so the cache was neutral within run noise while paying the global lock and linear scans.
|
||||
|
||||
The global verdict cache, lock, entry table, invalidation path, cache-only profiler fields, and
|
||||
deliberate cache probe are removed. The proof classifier remains; item 4 is still deliberately untouched.
|
||||
|
||||
## 6. Three small, sharp ones · tiny
|
||||
|
||||
- **The zero-position guard.** Streaming only starts when `GetPlayerPosition()` is not exactly
|
||||
`FVector::ZeroVector` (`VoxelWorld.cpp:1167`, `:1359`) — a pawn at the world origin is
|
||||
indistinguishable from "no pawn", and **the owner spawns at (0,0)**.
|
||||
- **`ApplyMeshToTile` has no success return** (`VoxelWorld.cpp:3064`). If runtime-mesh init fails the
|
||||
tile stays recorded as loaded and never retries — a permanent hole.
|
||||
- **Whole-tile uniform verdicts are trusted without exact validation.** Block verdicts get checked
|
||||
against the full grid when meshing; an outer AllAir/AllSolid verdict has no equivalent safety net.
|
||||
|
||||
## 7. Deferred, with reasons
|
||||
- **Coarse-first LOD refinement** — blocked until coarse tiles can carry collision, or item 1 lands.
|
||||
- **GPU evaluation** — right hardware, but bit-exactness across vendors/drivers is unproven and
|
||||
determinism is absolute. Item 2 is the honest prerequisite either way.
|
||||
- **The origin spawn room** — the owner considers it a design mistake that should not exist. Design
|
||||
conversation, not a fix.
|
||||
|
||||
---
|
||||
|
||||
## Outer-classifier A/B — measured 2026-09-12 (nested refinement OFF in every mode)
|
||||
|
||||
Clean, diagnostics off, headless game path. Static = 343 LOD0 samples, two runs per mode; moving =
|
||||
160 m at 8 m/s, 1,274 LOD0 samples. Density calls = validation + mesher.
|
||||
|
||||
| mode | ready p95 static | gen p95 static | worker CPU static | density calls static | ready p95 moving | gen p95 moving | density calls moving |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 0 no outer classifier | **182.4-182.7 ms** | **124.9-127.0 ms** | **184.9-193.5 s** | **18.05 M** | **198.9 ms** | **129.9 ms** | **57.7 M** |
|
||||
| 1 current (validate, discard) | 189.7-194.8 ms | 139.1-140.6 ms | 201.2-202.3 s | 21.89 M | 208.0 ms | 143.4 ms | 69.6 M |
|
||||
| 2 validation-grid reuse | 190.0-194.7 ms | 140.3-141.3 ms | 201.2-201.3 s | 21.89 M | 210.7 ms | 143.3 ms | 69.6 M |
|
||||
|
||||
**Verdict: the outer classifier is net-negative on the game path.** Mode 0 is better on every latency
|
||||
figure and does ~18% fewer density calls. It cannot remove geometry, since it skips nothing.
|
||||
|
||||
**Mode 2 cannot win as designed.** It reuses the validation grid only when a uniform verdict is
|
||||
*disproved* (`VoxelWorld.cpp` ~4012), and validation disproved none: 0 reuse tiles in every run. Mixed
|
||||
tiles, the ones that actually need a grid, never run validation, so there is nothing to reuse.
|
||||
|
||||
**Done 2026-09-13, owner approved:** `GVoxelForgeOuterClassifierMode` now defaults to 0, and mode 2
|
||||
is deleted along with its reuse grid, its mesher parameter and its counter. Mode 1 is still
|
||||
reachable with `voxel.OuterClassifierMode=1` as an A/B instrument. Verified:
|
||||
- export OBJ SHA-256 unchanged (`b3e5f4c3..9b377`)
|
||||
- a run without the override prints `outer_classifier_mode=0 validation_density_calls=0`
|
||||
- **841/841 tiles have identical triangle counts between mode 0 and mode 1**. The 394 tiles mode 1
|
||||
skipped as uniform produce 0 triangles in mode 0, so the strate band cut was not hiding geometry
|
||||
behind the classifier.
|
||||
|
||||
⚠️ **Correction to `f204413`.** Its message says the nested classifier's 3.5x "cannot be reproduced".
|
||||
That was my measurement error: I passed `-voxel.UseBlockEarlyOut=1` (the STREAMING console variable)
|
||||
to the export commandlet, which reads its own `-blockearlyout=` argument, so both of my runs had
|
||||
nested refinement off. Re-run with the right flag: `-blockearlyout=0` 0.553 / 0.572 s,
|
||||
`-blockearlyout=1` 2.036 / 2.060 s, which is **3.6x**, with byte-identical geometry. The 3.5x was real,
|
||||
and disabling nested refinement in streaming (`f204413`) was worth it.
|
||||
|
||||
## Room-cache work, 2026-09-13 (`2990009`, `2de603c`, `cfad3d1`)
|
||||
Player-fit memo, then the tile-sized room cache gated to LOD3+ (fused) / LOD4+ (op-stack). Against
|
||||
`2990009`, static worker CPU is 48 -> 39 s and moving 125 -> 114 s, with identical geometry.
|
||||
|
||||
⚠️ **Correction to `cfad3d1`.** Its message says absolute numbers drift because "the machine is in
|
||||
daytime use." That was my unverified guess, and it is false: the owner had not used the PC all day.
|
||||
The real signal is systematic. The switch-off baseline came out at 58 s in two rounds hours apart,
|
||||
against 48 s at `2990009`. So something left ON with `voxel.TileCacheWindow=0` costs ~10 s, and LOD0
|
||||
generation p50 is ~4% slower than at `2990009` (73 -> 77 ms static). Queued: attribute it with
|
||||
in-tree switches (first suspect: `voxel.SpatialIndex` auto-on at fused LOD1-2), never by running old
|
||||
commits.
|
||||
|
||||
**Resolved 2026-09-13 (`ab4bbe3` + the switch sweep):** not the spatial index; every `SpatialIndex`
|
||||
setting is within ~1%. `TileCacheWindow=0` at HEAD is a different configuration from `2990009`, not
|
||||
the old path. It still thrashes at L3/L4 (p95 ~250 ms), now at 54 s. The DEFAULT beats `2990009` on
|
||||
every metric: static worker 48 -> 34 s, moving 125 -> 101 s, LOD0 generation p50 73 -> 66 ms static
|
||||
and 70 -> 62 ms moving. The "LOD0 ~4% slower" worry is gone. `SpatialIndex` now defaults to 1
|
||||
(marginally best, within noise). Per-tile identity: 0/841 tiles differ against traces recorded
|
||||
before the always-on LOD0 changes.
|
||||
|
||||
**Next LOD0 target, found by the sampler's caller aggregation:** the "unknown" ~13.8% LOD0 leaf is
|
||||
`ucrtbase.dll!_finite`, the non-inlined CRT call behind `FMath::IsFinite`. Its callers are the tunnel
|
||||
chain, passage landing/floor checks and `GetDensityAt`. An inline bit test is exactly the same
|
||||
predicate, so it changes no field.
|
||||
|
||||
## State after 2026-09-14 (HEAD `7a972f5`)
|
||||
|
||||
**In-game generation since the start of 2026-09-13**, headless game path (static = 841 tiles;
|
||||
moving = 160 m at 8 m/s):
|
||||
```
|
||||
worker CPU static LOD0 generation p50 static
|
||||
c65b015 classifier off ~184 s ~75 ms
|
||||
2990009 player-fit memo 48 s 73 ms
|
||||
cfad3d1 tile cache window (LOD3+) 39 s 77 ms
|
||||
ab4bbe3 field-exact LOD0 overhead 33 s 66 ms
|
||||
5489b5e inline exact IsFinite ~31-34 s ~62-66 ms
|
||||
7a972f5 (current) ~34 s ~66 ms
|
||||
```
|
||||
The later rounds' absolute figures come from slightly different harness settings. Compare within a
|
||||
round.
|
||||
|
||||
**Measured, not guessed: what each component costs** (`6ccf371`, remove-one ablation, static worker
|
||||
saving): tunnel SDF 11.4% (and it owns the LOD0 p95 tail), cave warp 10.3%, passage carving 10.1%,
|
||||
landing posts 9.9%, structural posts 5.6%, worms 2.4%, everything else <= 2.2%. Ablations change
|
||||
the field, so these mix computation cost with the cost of the geometry each component creates.
|
||||
|
||||
**Tried and not kept:**
|
||||
- the exact worm block skip: 4% skip rate, neutral, off by default;
|
||||
- value-noise / lattice worms: no gain, removed;
|
||||
- exact culling (SmoothMin cutoff, tile candidates, post early-outs): the build was 23% slower even
|
||||
with every switch off, parked on local branch `wip/exact-cull-20260914`. Only the tunnel-core
|
||||
ablation gate fix was ported.
|
||||
|
||||
**Measurement lessons**, recorded because each cost a round:
|
||||
- line-level sampler attribution of inlined code is a hint; confirm it with an ablation;
|
||||
- a switch-off A/B inside a new build cannot see always-on cost; always compare the final default
|
||||
build against the committed tip's DLL, with a fixed command line;
|
||||
- launch Unreal serially (it is a GUI exe that returns at once), and reap crash reporters.
|
||||
|
||||
**Next candidates:** a cheaper cave warp (field-changing: coarse warp lattice); the tunnel SDF (field-
|
||||
changing merge of the warped SDF and the world-space core); a real per-tile candidate filter
|
||||
designed to cost nothing per sample; the cold read-only audit's findings.
|
||||
|
||||
## State after 2026-09-15 (HEAD `a668833`)
|
||||
|
||||
**Gameplay fixes, from the owner's playtest:**
|
||||
- `4c8d06f` the player can walk from one strate into the next. `StrateContentCutMinLevel`
|
||||
defaulted to 0, which content-cut the LOD0 collision tiles, so the collision gate blocked the
|
||||
passage: a deadlock. Regression test `-voxel.TestStrateCrossing=1`.
|
||||
- `39c306b` its +29% cost recovered by an exact sealed-rock proof (`TryProveSealedSolidTile`).
|
||||
- `cf2b2ad` tunnel floors, option B (the owner's choice): no staircases; steep tunnels WIND
|
||||
(walk-critical, a reserved spanning forest) or get a few big climbable DROPS (only leaf/redundant
|
||||
edges). Visual acceptance is the owner's in-game check; the renders did not show it.
|
||||
- `9260775` passage D-floors were missing inside gap chunks (a real bug since `adb34df`), plus
|
||||
two stale tests corrected.
|
||||
|
||||
**Correctness:** `8879045` fixed the cold audit's findings (`AUDIT-COLD-2026-09-14.md`), including
|
||||
a determinism bug introduced by the tile-cache work (a moved-from operator stack could be evaluated
|
||||
on a cache miss), plus cache world-identity, lifetimes and input guards.
|
||||
|
||||
**Performance since `7ece3ce`** (each measured against the committed tip's DLL, exact field):
|
||||
`ce4d878` landing/structural reach -12%; `1d824aa` cave warp SIMD batching -4%; `a668833`
|
||||
tunnel-core tile reach + passage-carving 8^3 reach -1.3 to -2.6%. Static worker time is now ~28 s,
|
||||
from ~184 s at the start of the push, with the same world and capability.
|
||||
|
||||
**Current cost map** (`0208e93`, remove-one, static): tunnel core 20.8%, tunnel SDF 12.6%, passage
|
||||
carving 9.1%, cave warp 6.7%, structural posts 6.6%, unattributed ~28%. The tunnel core is
|
||||
near-dominated (~99% of its cycles are in tiles near tunnels), so exact skips cannot remove it. The
|
||||
next lever is the field-changing SDF/core merge (in progress), which will also add an automation
|
||||
test for the reach proofs.
|
||||
|
||||
**Parked:** `wip/exact-cull-20260914` (per-sample culls, a 23% always-on regression; only its gate
|
||||
fix was ported).
|
||||
|
||||
**CLOSED 2026-09-15: the tunnel SDF/core merge. Do not re-propose it without a new idea.** The
|
||||
world-space core is load-bearing: `WorldControlPoints` keep the authored room-floor anchors and
|
||||
walkable routes before cave warp (`8e42d6d`, `cf2b2ad`). Replacing it cost 1,146 fit / 975
|
||||
walk-reachable cells; the exact shared-segment variant was 5.6-12.6% SLOWER. The remaining big
|
||||
costs (tunnel core ~20%, tunnel SDF ~13%) are the geometry itself, so the exact-skip perf push has
|
||||
reached diminishing returns. The reach proofs are now guarded by
|
||||
`VoxelForge.Correctness.TilePostReachProof`.
|
||||
|
||||
## State after 2026-09-16: Sol's design review, deterministic maths (`10fd0b6`)
|
||||
|
||||
**Reframe.** Local "find the hot spot" rounds had reached diminishing returns, so, on the owner's
|
||||
idea, Sol High was asked how it would build the project from scratch (`DESIGN-SOL-2026-09-15.md`),
|
||||
then read the code and said what could reasonably change (`DESIGN-SOL-2026-09-15-CODE.md`). Its
|
||||
verdict: don't rebuild. There are two performance bets, each with instrumentation-only first steps
|
||||
and kill criteria, plus one determinism hole. Owner's rule: each idea gets its own branch and is
|
||||
dropped if it doesn't hold capability and performance.
|
||||
|
||||
**Done: `sol/deterministic-math`, merged.** On Win64, `FMath::Sin/Cos` were CRT `sinf/cosf`, and the
|
||||
UCRT picks an FMA3 variant by CPU at start-up. Now `VoxelMath::DetSinCos` (fixed reduction and
|
||||
polynomial, max error 5.96e-8) is used on every world-deciding site; `-voxel.CrtFma3=0` forces the
|
||||
CRT's other path, and exports are byte-identical either way. Before the fix, the two paths also
|
||||
matched on this Ryzen, so the hole was real but not observed here. Capability is exact;
|
||||
static/moving worker time is -3.6%/-3.5% vs the tip DLL. Order-independent vertex distance is at
|
||||
most 8.7 micrometres (a line-by-line OBJ diff reported "11.4 m", a line-order artefact: compare
|
||||
meshes by nearest vertex, not by line).
|
||||
|
||||
**New canonical hashes (the world changed once, by design):**
|
||||
- export-only OBJ SHA-256 `CBB8AE185E949B33CF2656441BA4AB17398975A098967870DFD45B435A37906F`
|
||||
(geometry `B9EF84AC`), was `b3e5f4c3..9b377`;
|
||||
- worms/capability region `9242C093`, was `311CA42C` (walk OBJ `20CCF7BD..0100`);
|
||||
- field digest (cross-platform test grid) shape `0x767FB0650936435D`, field `0x2AE570A5A1A9C5CE`;
|
||||
- capability unchanged: 20,830 / 10,909, connected.
|
||||
|
||||
**Rung 1 done (`b003bee`, `ba7a083`, branch `sol/perf-probes`, probes off by default):**
|
||||
- ⛔ **Bet A (tile-wide feature-major tunnel evaluation) is KILLED. Do not re-propose it without a
|
||||
new idea.** The per-sample lookup it removes is only 5.1-6.1% exclusive, and feature-major
|
||||
visiting would touch **5.2-6.6x more lattice points** than the current code evaluates (static
|
||||
LOD0: 49.3M predicted vs 9.5M expensive evaluations), because the cheap AABB/sphere rejection
|
||||
(63M of 80M visits) is exactly what it throws away. The swept kernel is branch-divergent
|
||||
(per-sample segment choice, ownership and support branches), so no low-risk X-lane SIMD.
|
||||
- ✅ **Bet B (cross-tile density reuse) cleared every gate**, and the orchestrator re-derived it
|
||||
from the raw trace independently (`scratchpad/dupcheck.py`): static LOD0 20.04% of density calls
|
||||
are exact duplicates, 80.6% with a donor already finished (~2.96 s of 18.3 s); moving 20.70%,
|
||||
63.3% (~5.94 s of 45.3 s). Ceiling ~16% static / ~13% moving of worker CPU.
|
||||
|
||||
**Rung 2 (`sol/tile-sample-reuse`, WIP `9a67452`): INTERRUPTED by the Codex usage limit**
|
||||
(2026-09-16 02:07, resets 19 Sep 12:44). The slab cache, its key test, mesher donor views and
|
||||
`VoxelWorld` plumbing are committed **UNVALIDATED: never compiled, run or measured** (zero Unreal
|
||||
launches that round). Resume brief: `scratchpad/tile-sample-reuse-resume.md`. Acceptance unchanged:
|
||||
byte-identical canonical export, capability 20,830 / 10,909, >=6% worker CPU in both scenarios vs
|
||||
the tip; kill under 4%. Also still owed: a same-session switch-off A/B, because rung 1's probes-off
|
||||
static (28.18 s) sits above the maths round's (27.41 s) for the same code.
|
||||
|
||||
**Then:** landing gap (ARCHITECTURE, passage landings): replace the random-reach fallback with a
|
||||
deterministic retry.
|
||||
Smaller hardening noted by Sol, not scheduled: field-wise cache hashing instead of raw-memory
|
||||
CRC32; an integer-lattice mesher entry point; server-sequenced edit order once networking exists.
|
||||
|
||||
## State after 2026-09-17 night: tunnels from the owner's playtest (branch `fix/tunnel-shape`)
|
||||
|
||||
**Found in play, not in harness:**
|
||||
- His editor had been running a 14 Sep build (no crossing fix: `CutMinLevel=0`).
|
||||
- A gentle slope of 0.1 froze world loading.
|
||||
- Tunnels were ribbed on floor, walls and ceiling with every terrace setting at 0.
|
||||
- Tunnels poke into rooms.
|
||||
|
||||
The harness uses plugin defaults, not his `DA_Strate3`/`DA_Settings`. **Reproduce owner reports with
|
||||
his assets staged into `Saved\BuildHost`.**
|
||||
|
||||
**Cause of the ribs (`4fa3931`), proven in game by the owner.** `voxel.TunnelAblateTunnelCore=1`
|
||||
removes them. That switch is read ONCE per process, at the first Play: set it before pressing Play,
|
||||
and check `[VoxelForgeTunnelAblation] resolved mask`. The tunnel core's final reassertion replaced
|
||||
interior samples with a fixed +16 air density, so the field lost its distance gradient and marching
|
||||
cubes snapped the surface to the voxel grid. It now reasserts the continuous `-Core.SDF`. The
|
||||
periodic amplitude along a tunnel is now under 1.1 cm (was up to 16.9 cm on walls).
|
||||
- ⛔ The first attempt (`0231072`, a Catmull-Rom centreline) was wrong, cost 18-30% CPU, and is
|
||||
reverted. It was "verified" by small, unmatched renders. **Use a quantitative instrument**, e.g. the
|
||||
surface-position oscillation along the axis (`tunnelcoreprobe` mode in the explore commandlet).
|
||||
|
||||
**Owner decisions implemented:**
|
||||
- no authored tunnel floor: `bTunnelFloorEnabled` is ignored and the slab/support-floor paths are
|
||||
detached;
|
||||
- slope in degrees (0-90, migrated from tangents, 3840-voxel route cap);
|
||||
- winding and ledges independent of any floor switch.
|
||||
|
||||
Canonical walk: Connected, player-fit 41,184, walk-only 13,879 (was 20,830 / 10,909). One perf pair:
|
||||
+2.7% static / +4.2% moving vs `bf9384a`, to be re-measured. Not merged: waiting for the owner's
|
||||
in-game look.
|
||||
|
||||
## State after 2026-09-18 night: room wins over tunnels (branch `fix/tunnel-mouths`, `d01af46`)
|
||||
|
||||
The owner played the merged rib fix: *"the tunnels do look better now"*, performance on par. Then he
|
||||
reported a rock shelf spanning a room, ripple rings around tunnel mouths, and tunnels still running
|
||||
into rooms.
|
||||
|
||||
- **Cause of the shelf:** the tunnel core reasserted solid below its own floor **inside room
|
||||
volumes**, plus the landing-floor post. Measured as tunnel-attributed solid inside rooms:
|
||||
`469 → 0`. A matched re-render of his room shows the slab gone.
|
||||
- **Room ownership is now enforced in every tunnel path** (generic tunnel SDF, core evaluator, both
|
||||
native fallbacks): a room suppresses the tunnel bottom inside itself, while a short fade keeps the
|
||||
exterior mouth fillet.
|
||||
- **Mouth trim is now ON by default** (`voxel.TunnelMouthTrim`, dev-only cvar, shipping constant
|
||||
true). It broke walkability before; it no longer does, because trimmed endpoints take their height
|
||||
from the room's own vertical zero crossing (join offsets `-2.08/+0.40 → -1.13/-0.71` voxels).
|
||||
- **Mouth ripples:** periodic amplitude `1.49 cm` floor / `0 cm` wall (degree-six trend removal;
|
||||
raw local-median `6.19 cm` kept as audit). ⚠️ Instrument lesson: a broad room-floor bowl reads as a
|
||||
6 cm "period" unless the slow trend is removed first.
|
||||
- Canonical walk `Connected`, `42,870` fit / `17,413` walk-reachable (was 41,184 / 13,879); export
|
||||
identical with `-voxel.CrtFma3=0`; `TilePostReachProof` and `DetSinCos` pass.
|
||||
- Left when the owner reclaimed his PC: strate crossing and the perf pair.
|
||||
|
||||
**Harness notes (they cost real rounds):**
|
||||
- `-run=VoxelForgeExplore` belongs to explore/export/probe launches ONLY. With it, automation tests
|
||||
and `-game` runs silently run the commandlet instead.
|
||||
- The staged host's `Content` lost `/Game/Untitled`; the crossing test needs that level (it carries
|
||||
the `AVoxelWorld` actor), else Unreal fails the map load and crashes on the empty world.
|
||||
- Codex's headless runs deleted the owner's `%LOCALAPPDATA%\...\EditorSettings.ini` on 17 Sep (a
|
||||
failed cross-volume move, error 5). Runs now use isolated user/config dirs; verify that file's
|
||||
hash before and after a round.
|
||||
|
||||
**Queued, waiting for the owner's go:**
|
||||
1. Terracing he can target: rooms / tunnels / terrain, and floors / walls / ceilings (brief ready:
|
||||
scratchpad `terrace-targets.md`).
|
||||
2. Tunnel mouths trimmed at room walls (`voxel.TunnelMouthTrim`, default off: 36 m walk gap when on).
|
||||
3. The landing random-reach fallback.
|
||||
4. Bet B rung 2 (paused).
|
||||
|
||||
### Open design questions for the owner
|
||||
- All five strate slots resolve to `DA_Strate3`. Intended?
|
||||
- Should a composer roll be allowed to overwrite an explicitly authored value?
|
||||
@@ -0,0 +1,476 @@
|
||||
# Game Design Document
|
||||
|
||||
**Status:** first consolidation, 2026-08-17. Distilled from `COMPOSER-NOTES.md`, which stays as the
|
||||
working transcript (chronological, with wrong turns preserved on purpose). This document is the
|
||||
opposite: **the decided state, without the journey.**
|
||||
|
||||
> **Where this file lives, and why it is the wrong place.** This is a design document for *the game*.
|
||||
> Most of it is not about VoxelForge at all — the city, combat, the economy and seasons are all
|
||||
> outside the plugin. It lives here only because the plugin folder is the only part of the project
|
||||
> under version control. Move it to the project root once that becomes a repository.
|
||||
|
||||
**How to read this:** everything under **Decided** was settled by Jahni. Everything under **Open** has
|
||||
not been. Where a design is mine rather than his, it says so. Nothing here is a proposal wearing a
|
||||
decision's clothes.
|
||||
|
||||
---
|
||||
|
||||
## 1. The game, in a paragraph
|
||||
|
||||
A fantasy exploration and combat RPG, online-first and party-based. The setting is medieval-ish and
|
||||
progresses toward better and more modern infrastructure without ever leaving fantasy. Players descend
|
||||
from a city on the surface into an endless underworld to find the resources that surface has none of,
|
||||
and bring them back to build that city up. Side systems: housing and farming. The underworld is
|
||||
generated, shared by everyone, and replaced every season. The city is hand-built, shared by everyone,
|
||||
and permanent.
|
||||
|
||||
## 2. The premise
|
||||
|
||||
**Civilization emerged from the depths.** The first strate below the surface is artificial — someone
|
||||
built it — and it holds crystals with humans inside them. The game opens on one of those humans being
|
||||
un-crystallized.
|
||||
|
||||
The surface is a sky island, or a mountain so tall nobody can descend it. Either way it is small and it
|
||||
has no resources. Everything a civilization needs has to come from below.
|
||||
|
||||
So descending is not conquest. **It is a return.** You are going back to where you came from, and
|
||||
nobody remembers why you left.
|
||||
|
||||
## 3. The core loop
|
||||
|
||||
> The surface is limited, so you must descend. The depths supply resources. Resources build the
|
||||
> settlement. The settlement lets you descend further.
|
||||
|
||||
Every system in this document hangs off that loop, and it closes. The sky island does quiet work in
|
||||
it: it explains why there is no trade with anywhere else, why the depths are the only economy, and why
|
||||
going down is the civilization's whole project rather than an adventurer's hobby.
|
||||
|
||||
---
|
||||
|
||||
## 4. Vocabulary
|
||||
|
||||
Two words got used for the same thing early on and caused real confusion. Pinned here.
|
||||
|
||||
**THE SURFACE / THE CITY** — a hand-authored, ordinary Unreal level. Not voxel terrain, not generated,
|
||||
not part of VoxelForge. Communal and identical for every player. **It never resets.**
|
||||
|
||||
**THE DEPTHS** — everything VoxelForge generates: bounded, subterranean, a stack of strates below the
|
||||
city's hole. Some strates only *mimic* being outdoors. **This is what is regenerated each season.**
|
||||
|
||||
**A SEASON** — one generation of the depths, roughly six to twelve months, plus the settlement tier the
|
||||
community earns during it.
|
||||
|
||||
---
|
||||
|
||||
## 5. The city
|
||||
|
||||
### Decided
|
||||
|
||||
The city is **communal** — the same place for everyone, not instanced — and **permanent**. The season
|
||||
wipe never touches it.
|
||||
|
||||
It **grows across seasons**, one tier at a time: camp, village, town, city, advanced city. Players
|
||||
gather resources in the depths and donate them to the mayor or equivalent; a global counter rises until
|
||||
the next tier is reached. A season is the unit that advances it. The game opens at *camp*.
|
||||
|
||||
Because groups never meet in the depths, **the city is the entire social surface of the game.** Trade,
|
||||
the retrieval guild, shared builds, and simply seeing another person all happen there.
|
||||
|
||||
### Design rules
|
||||
|
||||
**A tier grants access and convenience, never power.** Services, workshops, NPCs, a bank, fast travel,
|
||||
new quests, better aesthetics — but not stronger starting gear or stat bonuses. Otherwise players
|
||||
arriving in season eight begin meaningfully stronger than those in season one, the early strates
|
||||
trivialize, and the difficulty curve erodes a little every season until the top of the world is
|
||||
scenery. This is the same rule the depth economy already follows: **buy access and time, never
|
||||
numbers.**
|
||||
|
||||
**The city should be a ledger.** Somebody arriving in season eight built none of it. That is either
|
||||
alienating or the best first impression in the game, depending entirely on whether the city records
|
||||
*who* built it — names on the aqueduct, a monument per tier listing that season's contributors. New
|
||||
players then walk through evidence that real people made this place, and long-term players get
|
||||
something permanent, which pairs with the titles that already survive wipes.
|
||||
|
||||
### Open
|
||||
|
||||
- Is the tier threshold a fixed number, or does it scale with the active population? A fixed number
|
||||
stalls forever with a small playerbase and is cleared in a week with a large one. *(My
|
||||
recommendation: target one tier per season and retune the threshold at each season boundary, which
|
||||
is a moment that already exists for other reasons.)*
|
||||
- Where housing sits relative to the city.
|
||||
|
||||
---
|
||||
|
||||
## 6. The depths
|
||||
|
||||
### Decided
|
||||
|
||||
Bounded, but **gigantic**. Not infinite — that choice was made deliberately, and it buys a great deal.
|
||||
|
||||
**No bottom.** The descent does not end.
|
||||
|
||||
**Boss strates roughly every five levels**, and they are **intermediary, not terminal** — gates that
|
||||
punctuate the descent rather than finish it. A boss is where the transport network's next stop opens.
|
||||
|
||||
**Regenerated every season** from a single seed shared by every player.
|
||||
|
||||
Some strates are open, cathedral-sized spaces lit from within that read as outdoors. There is no sun
|
||||
anywhere in the depths.
|
||||
|
||||
### What bounding buys
|
||||
|
||||
Because the depths are finite, the generator can examine an entire strate rather than sampling it. That
|
||||
turns the entrance-and-exit guarantee into a **proof**, and it makes "the community has explored 34% of
|
||||
this season" a real number rather than a slogan.
|
||||
|
||||
---
|
||||
|
||||
## 7. A strate
|
||||
|
||||
### Decided
|
||||
|
||||
**The primordial law: every strate has an entrance and an exit, exposed to air, and findable.** This is
|
||||
the one invariant that may never fail.
|
||||
|
||||
**Danger is depth times a per-strate multiplier**, with a rare chance of an easier strate appearing
|
||||
deep. Danger drives which creatures live there, which environmental hazards exist, and what loot drops.
|
||||
It does **not** drive what *kind* of place a strate is.
|
||||
|
||||
**A strate's identity is unrelated to its depth.** Two seeds differ in everything — not the same
|
||||
strates spawn, nor the same details. You could meet anything at any depth.
|
||||
|
||||
**Anomalous strates are rare.** The example Jahni gave is "dreamcore" — repetitive patterns, a strange
|
||||
sense of emptiness. They should be rare enough that finding one is *puzzling*, because it does not have
|
||||
its place there. They deliberately do **not** blend with their neighbours; the wrongness lives in the
|
||||
seam.
|
||||
|
||||
**Size varies enormously** — from a crawlspace to a two-kilometre cathedral.
|
||||
|
||||
### Design rules
|
||||
|
||||
**Strangeness needs ordinary to be strange against.** If every strate is peculiar, peculiar becomes the
|
||||
baseline and nothing reads as uncanny. A world should guarantee a share of grounded, plausible strates
|
||||
so the strange ones land.
|
||||
|
||||
**A rare easy strate should be a designed type, not a low roll** — visibly calm, few creatures, a place
|
||||
a descent exhales. The natural home for a safe camp or a merchant.
|
||||
|
||||
**The mirror of that is stronger: a rare danger spike near the surface.** A place at −300 that is
|
||||
absolutely not for you yet. You find it early, it kills you, and it sits in your head for forty hours.
|
||||
In a single shared world that becomes season-wide news.
|
||||
|
||||
**Every anomaly must be legible.** Unsignalled variance reads as bad tuning, not design. And danger in
|
||||
particular must be **readable at a strate's entrance, before the player commits** — warned-and-ignored
|
||||
is the best death in games; unwarned is the worst.
|
||||
|
||||
### Open
|
||||
|
||||
- Adjacency rules: what may sit next to what. Confirmed as wanted, never specified.
|
||||
|
||||
---
|
||||
|
||||
## 8. Descending
|
||||
|
||||
### Decided
|
||||
|
||||
**A party of up to six.** The descent is done together.
|
||||
|
||||
**Instanced per group.** Different groups never meet in the depths. Today this runs on a local server —
|
||||
one player's machine or a dedicated one — fed by an external database for community information.
|
||||
|
||||
**Two ways back up.** Walking back through the tunnels is content, not a chore. Alternatively, once a
|
||||
strate has been visited it opens a passage onward, so transport — an elevator or similar — can carry
|
||||
you down, and you choose which strate to travel to.
|
||||
|
||||
**Transport unlocks are per-player for now.** A world-level unlock, where the first group through opens
|
||||
the way for everyone, is a possible future — gated on strates being large enough that finding the next
|
||||
passage is not a ten-minute errand. That is a decision to make from data, not in advance.
|
||||
|
||||
### Open
|
||||
|
||||
- **How long is a run?** Still unknown, and it constrains a great deal: strate size, how dense the
|
||||
transport stops need to be, how deep a season can plausibly reach.
|
||||
- Does danger scale with party size, or are strates tuned for a nominal group?
|
||||
|
||||
---
|
||||
|
||||
## 9. Progression and the economy
|
||||
|
||||
### Decided
|
||||
|
||||
**Gear stats have a ceiling — somewhere around depth 30.** Loot found deeper has the *same* stats.
|
||||
|
||||
**What depth buys instead:** better natural rolls, so an item costs less material to level up; and
|
||||
materials, enchantments and effects that exist nowhere else.
|
||||
|
||||
**Beyond the ceiling, going deeper is about pride, challenge, and seeing new things.** That is the
|
||||
intent, not an accident.
|
||||
|
||||
**Depth-locked materials are tagged by difficulty tier, not by strate.** Their placement stays random,
|
||||
so *where* a material is has to be rediscovered every season — which in a one-seed shared world turns
|
||||
into a community question with a fresh answer each time. A fixed depth would be solved once and stay
|
||||
solved forever.
|
||||
|
||||
**Trading will exist**, lightly at first, and becomes much better once the city is a real social hub.
|
||||
|
||||
### Design rules
|
||||
|
||||
**Buy access and time, never numbers.** Applied to both depth and city tier, which means the economy has
|
||||
one principle rather than two competing ones.
|
||||
|
||||
**Guarantee what recipes depend on; leave the rest to chance.** A material a progression recipe needs
|
||||
must be placed within reach, or a season ships a dead end that reads as a bug. A purely optional
|
||||
material can be absent, and a season lacking one gains an identity.
|
||||
|
||||
**Since the depths are gigantic, "guaranteed" must mean a density, not a location.** Somewhere in an
|
||||
enormous strate is unfindable. Near enough to the spine and common enough to actually meet is not.
|
||||
|
||||
### Open
|
||||
|
||||
- Where exactly the gear ceiling sits, and how fast danger climbs past it — that single rate is the
|
||||
entire endgame difficulty curve.
|
||||
|
||||
---
|
||||
|
||||
## 10. Death
|
||||
|
||||
### Decided
|
||||
|
||||
Dying in the depths leaves your **corpse, possessed**. You fight it to get your cargo back.
|
||||
|
||||
**The corpse copies your weapons, and you keep yours.** It is a mirror match, not a disarming — you
|
||||
never return to a fight underequipped.
|
||||
|
||||
**Only you can claim your loot.** No player can take it.
|
||||
|
||||
**A corpse run is a detour, not an evening — ten minutes at worst.**
|
||||
|
||||
**An NPC tells you where your corpse was last seen**, for a very small fee.
|
||||
|
||||
**Possible later:** dead players appear in other groups' instances, and killing such a corpse earns
|
||||
gold from a retrieving guild — never that player's loot.
|
||||
|
||||
### Design rules
|
||||
|
||||
**Place the corpse; do not make it walk to you.** On death it should appear at a landmark near the
|
||||
nearest passage or transport stop, and wander only within that neighbourhood. That turns the ten-minute
|
||||
cap from a hope into a guarantee regardless of where you actually died.
|
||||
|
||||
**Charge for precision, not for existence.** The general area — "last seen near the Sink" — should be
|
||||
free. Pay only for the exact position or live tracking. Otherwise the fee is not a decision, and worse,
|
||||
a broke player after a bad run cannot recover their cargo at all.
|
||||
|
||||
**Copied weapons must never be lootable.** If the corpse drops a copy of your sword you have built an
|
||||
item duplicator and it will be found within a week. Copied gear vanishes with the corpse; only cargo
|
||||
drops.
|
||||
|
||||
**The corpse should be a combat-shaped version of you, not a literal copy.** A support build's literal
|
||||
mirror heals itself and cannot be beaten, for reasons that have nothing to do with skill.
|
||||
|
||||
**Give the player a deliberate edge.** A perfect mirror is a coin flip, and you often arrive having
|
||||
already spent your consumables. You have potions and can retreat; it does not and cannot. That is what
|
||||
makes the fight winnable through preparation rather than luck.
|
||||
|
||||
**The bounty hunter and the player retrieval guild are one system with two front ends** — players are
|
||||
the fast, uncertain path; the NPC is the slow, guaranteed one. Either way the cargo returns to you and
|
||||
the retriever is paid by the guild, not out of your pocket.
|
||||
|
||||
### Open
|
||||
|
||||
- What happens if you die while fighting your corpse — one corpse that absorbs the last, or do they
|
||||
stack?
|
||||
|
||||
---
|
||||
|
||||
## 11. Housing and farming
|
||||
|
||||
### Decided
|
||||
|
||||
**Housing is an instanced personal level** — your own small world, never wiped, never re-placed.
|
||||
|
||||
**Its ground is a snapshot of land you chose** in the depths and claimed. Your home is *from* somewhere.
|
||||
After the season wipe it is the last surviving fragment of a world that no longer exists.
|
||||
|
||||
**One template level populated from save data** — a stored terrain region plus a list of placed
|
||||
structures. Not one map per player.
|
||||
|
||||
**Invites**, because houses can be shared and visited.
|
||||
|
||||
**Anything that grows is computed from elapsed real time on entry**, not ticked — the instance is not
|
||||
loaded while you are away.
|
||||
|
||||
### Open
|
||||
|
||||
- Whether players can build underground in the depths proper. The idea is alive, undecided.
|
||||
- Whether the surface offers natural buildable space, or whether terrain-editing tools cover it.
|
||||
|
||||
---
|
||||
|
||||
## 12. The verbs
|
||||
|
||||
### Digging — decided
|
||||
|
||||
Digging is for **breaking walls that hide something behind them**, **uncovering half-buried ruins**,
|
||||
**small shortcuts within a strate**, and **terraforming for builds**. It is a reveal-and-shape verb.
|
||||
|
||||
It is **not** a movement verb, and it is not the primary way through the world.
|
||||
|
||||
Descending by digging is possible but effortful: each strate's floor and ceiling are sealed, and the
|
||||
seal must be dug through. Passages and transport are the fast path, by design.
|
||||
|
||||
### Combat — decided
|
||||
|
||||
MMORPG-like and dynamic: ranged and melee, magical and physical, stuns, roots, mutes, area effects,
|
||||
targeted area effects, damage over time.
|
||||
|
||||
**Party of six. No friendly fire** — an area effect in a tunnel is simply strong, and that is accepted.
|
||||
|
||||
**First person must remain possible.**
|
||||
|
||||
### Design rules
|
||||
|
||||
**Telegraph the source, not the floor.** A circle painted on the ground only works if you can see the
|
||||
ground, which is exactly what first person removes — you cannot see a marker under your own feet or
|
||||
behind you. If the enemy visibly winds up instead, the threat is read off the creature. That works in
|
||||
both perspectives, it survives lumpy voxel terrain where ground decals project badly, and reading an
|
||||
animation is a better thing to ask of a player than reading a decal.
|
||||
|
||||
**Supporting both perspectives means owing both sets of constraints** — full third-person camera
|
||||
clearance *and* source-readable telegraphs.
|
||||
|
||||
**Fights need space, and the generator must guarantee it.** Cramped tunnels are the worst possible
|
||||
arena: no room to dodge a telegraph, no sightlines, no retreat, and with six players' effects in an
|
||||
eight-metre corridor nobody can see anything. Rough targets, in the generator's own units of 8 m
|
||||
chunks: a boss arena wants 3 to 4 chunks across, an ordinary fight space about 2, and roughly 1 chunk
|
||||
of headroom for the camera.
|
||||
|
||||
**Verticality is what caves can offer that a flat arena cannot.** MMO fights are overwhelmingly flat. A
|
||||
fight across ledges and drops is genuinely different, and it is the reason to fight underground at all.
|
||||
|
||||
### Open
|
||||
|
||||
- Creatures. Nothing decided. Danger and theme tags will select them, but *what they are* is unwritten,
|
||||
and they are most of what makes a strate feel like a place rather than a shape.
|
||||
|
||||
---
|
||||
|
||||
## 13. The season
|
||||
|
||||
### Decided
|
||||
|
||||
The depths are wiped and regenerated roughly every six to twelve months.
|
||||
|
||||
**Survives a wipe:** the city and its tier, account progression, titles, personal blueprints, housing
|
||||
instances.
|
||||
|
||||
**Dies with the wipe:** the depths, the shared build pool, the transport unlock network.
|
||||
|
||||
### Why this is good rather than merely tolerable
|
||||
|
||||
The world is impermanent and the civilization is not. *We lost our maps, but we built a town.* Season
|
||||
eight arrives in a place season three made, which gives a season's end a meaning beyond freshness.
|
||||
|
||||
It also solves problems that would otherwise be permanent. Every long-lived exploration game eventually
|
||||
dies of its own wiki — everything is solved, every secret is mapped, nothing is left to find. A wipe
|
||||
resets that, and the whole community re-explores together. It equally means a datamined map is worthless
|
||||
one season later, so there is nothing to defend against.
|
||||
|
||||
And it frees the generator: because generation is only frozen *within* a season, it can be changed at
|
||||
every boundary. The system never has to be finished before it meets players.
|
||||
|
||||
---
|
||||
|
||||
## 14. The community layer
|
||||
|
||||
### Decided
|
||||
|
||||
**One seed for everyone.** Every player's depths are the same depths, which is what makes shared
|
||||
knowledge — wikis, guides, "the crystalline lake is in strate four" — possible at all.
|
||||
|
||||
**Players share builds**, as defined blueprint structures rather than raw terrain edits. Votes rank
|
||||
them; well-liked builds appear in more players' worlds; disliked ones fall to the back of the queue and
|
||||
sufficiently disliked ones are never shown again. **New and unvoted builds must be deliberately rolled
|
||||
in**, or nothing new is ever seen and the system dies quietly.
|
||||
|
||||
**The build pool does not survive a season change** — nothing would be placed correctly. Top
|
||||
contributors are rewarded with titles, tiered by population.
|
||||
|
||||
**Landmarks have names**, generated so that every player sees the same name for the same place before
|
||||
anyone has ever visited it.
|
||||
|
||||
### Design rules
|
||||
|
||||
**The community layer must degrade gracefully.** The depths are generated from a seed and need no
|
||||
database at all. If the database is unreachable, procedural names stand in for community ones, shared
|
||||
builds simply do not appear, and votes queue locally. That is cheap now and painful to retrofit once a
|
||||
hundred call sites assume the fetch succeeded.
|
||||
|
||||
**A build placed anywhere is yours; a build placed in a spot the world advertised is eligible to
|
||||
spread.** That keeps building free while keeping automatic distribution safe.
|
||||
|
||||
**Names must survive contact with a wiki** — short, pronounceable, typeable. A name nobody can spell
|
||||
cannot become community knowledge, which is the only reason names exist here.
|
||||
|
||||
**Auto-distributing player content needs a report, reject and rollback path**, and shared builds must be
|
||||
individually removable by the player receiving them.
|
||||
|
||||
---
|
||||
|
||||
## 15. The opening
|
||||
|
||||
### Decided
|
||||
|
||||
A cinematic: civilization emerged from the depths. The first artificial strate holds crystals with
|
||||
humans inside. One of them is un-crystallized — you.
|
||||
|
||||
You discover a **base camp**, and are told you could be an adventurer who braves the depths.
|
||||
|
||||
Then you descend to **strate 2 — the first generated strate.**
|
||||
|
||||
### Why this is worth more than it looks
|
||||
|
||||
The first strate is hand-made, and the rest are generated. That is normally a production compromise
|
||||
players must be prevented from noticing. Here the hand-made strate is **hand-made in the fiction too** —
|
||||
artificial because someone built it, with the natural world lying beneath. A player who senses that
|
||||
strate 1 feels different from strate 2 is reading the story correctly rather than spotting the seam.
|
||||
|
||||
That is rare, and it is worth protecting in every later decision.
|
||||
|
||||
---
|
||||
|
||||
## 16. Open questions, collected
|
||||
|
||||
Ordered roughly by how much they block other work.
|
||||
|
||||
1. **Is the descent fun?** Unverified, and undesignable — it needs a crude build and an evening of
|
||||
playing it. Everything in this document assumes the answer is yes.
|
||||
2. **How long is a run?** Constrains strate size, transport density, and how deep a season reaches.
|
||||
3. **Creatures.** Nothing decided. Blocks content, not the generator.
|
||||
4. **The backend.** Local server plus an external database, per-group instances, moderation, shared
|
||||
builds. Entirely unspecified and carrying a lot of weight.
|
||||
5. **Does danger scale with party size**, or are strates tuned for a nominal group?
|
||||
6. **The tier threshold** — fixed, or scaled to population?
|
||||
7. **Adjacency rules** between strates. Wanted, never specified.
|
||||
8. **Underground building** — undecided.
|
||||
9. **Where the gear ceiling sits**, and how fast danger climbs past it.
|
||||
|
||||
## 17. Deliberately deferred
|
||||
|
||||
- **Digging noise attracting creatures.** Liked, parked. Still the cheapest way to price digging in
|
||||
safety rather than in resources, if it is ever wanted.
|
||||
- **Naming languages varying by depth** — older, stranger tongues deeper down. Liked, parked as still
|
||||
being a gradient.
|
||||
- **World-level transport unlocks**, where the community opens the descent together. Gated on strates
|
||||
being big enough for the discovery to be worth something.
|
||||
- **Shards** — more than one seed at a time. Only if the game grows enough to need them.
|
||||
- **Singleplayer.** Never the intent; may become an option.
|
||||
|
||||
---
|
||||
|
||||
## Where the rest lives
|
||||
|
||||
- **`COMPOSER-NOTES.md`** — the working transcript this was distilled from, including the generator
|
||||
design (the composer, the world manifest, the measurement pass, stack assembly) and the reasoning
|
||||
behind everything above.
|
||||
- **`ARCHITECTURE.md`**, **`CODEMAP.md`** — how VoxelForge actually works.
|
||||
- **`fable-idea.md`** — the older ranked feature and performance roadmap. Predates most of this.
|
||||
@@ -0,0 +1,783 @@
|
||||
# Astra (gpt-6-astra, max): how else could we do it? — 2026-10-02
|
||||
|
||||
Prompt: scratchpad astra-think.md (free thinking, nothing off the table). Answer verbatim below.
|
||||
|
||||
**I would bet on your coarse-structure/fine-surface idea, extended in two ways: generate surfaces directly wherever possible, and make edits immediately queryable without waiting for replacement meshes.**
|
||||
|
||||
The largest opportunity is to make the ordinary cost scale with **surface area, visible detail, and recent changes**. Dense volumetric work should be reserved for places where complicated three-dimensional intersections actually require it.
|
||||
|
||||
I read the [research report](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/RESEARCH-SPEED-2026-09.md>) and use the newer timings in your prompt below. All performance estimates here are arithmetic or architectural estimates, not measurements of these proposed designs.
|
||||
|
||||
At 100 ns per point, 140 million points require about **2.3 seconds on six perfectly scaling workers**, before anything else happens. That makes the central problem clear: further local optimization cannot carry the full burden.
|
||||
|
||||
There is also an important distinction inside “detail”:
|
||||
|
||||
| What the player perceives | What must represent it |
|
||||
|---|---|
|
||||
| A passage, hole, thin wall, or floating island | Actual solid/air topology |
|
||||
| A foothold, climbable edge, or projectile obstruction | Gameplay geometry |
|
||||
| Rock relief that changes silhouettes and stereo depth | Actual rendered surface displacement or geometry |
|
||||
| Grain, mineral variation, tiny scratches, wetness | Shading and material information |
|
||||
|
||||
A finer lattice spends more on all four together. Your game can give each its own representation and still present one coherent world.
|
||||
|
||||
The difficult negotiation is whether every visible grain must also be an independently collidable, destructible object. If that is required at arbitrarily increasing resolution, the physical complexity must eventually be paid for. If the requirement is increasingly fine **visible rock**, there is much more room.
|
||||
|
||||
**1. Give fine detail a surface representation. This is the strongest immediate direction.**
|
||||
|
||||
A cave wall can have a relatively simple structural surface and an extremely detailed skin.
|
||||
|
||||
A useful version would contain:
|
||||
|
||||
- Structural geometry for chambers, passages, ledges, holes, and substantial protrusions.
|
||||
- A bounded surface displacement for shallow fractures, erosion, and rock relief.
|
||||
- Normal, roughness, colour, and wetness variation below that.
|
||||
- Explicit small geometry for features such as thin stalactites that cannot be represented by shallow displacement.
|
||||
|
||||
This follows the useful idea behind film-style micropolygon rendering: keep a compact description of a surface, then subdivide and shade it according to what the image needs. Pixar’s original [Reyes architecture](https://graphics.pixar.com/library/Reyes/paper.pdf) is a foundational example.
|
||||
|
||||
The crucial improvement over “coarse geometry plus nicer materials” is that the surface layer should include **real geometric relief where stereo or silhouette makes it necessary**. Normal mapping alone will fail the owner’s close inspection. Parallax mapping has useful applications, but I would not make it responsible for rock silhouettes or convincing near-hand VR geometry.
|
||||
|
||||
There are two traps.
|
||||
|
||||
First, tessellating a coarse, faceted mesh does not automatically recover a smooth underlying shape. The refined vertices need a meaningful curved surface to follow: a procedural patch, a suitable subdivision surface, or projection onto a local structural field. Otherwise, finely tessellated facets remain facets.
|
||||
|
||||
Second, displacement cannot create arbitrary new topology. A six-centimetre hole through a wall needs an explicit hole or a locally finer volumetric representation. It cannot be recovered by adding surface roughness to a mesh that sealed it.
|
||||
|
||||
For your reference image, that distinction is favourable. Broad ledges, columns, hanging formations, and fractured slabs supply the geometry. Much of the apparent richness comes from correlated relief, material, moisture, lighting, and water.
|
||||
|
||||
**What it gives:** returning the structural lattice from 12.5 cm to 25 cm reduces the dense sample count by approximately **8×**, while the rendered surface can become finer than either lattice. This is a sample-count reduction, not an 8× whole-game guarantee.
|
||||
|
||||
**What it costs:** continuous GPU work, surface refinement, coherent material coordinates, and careful boundaries between patches. At 90 Hz, the entire frame has approximately **11.1 ms**; displacement and its shadows must share that budget.
|
||||
|
||||
**What it takes away:** automatic physical significance for every visible bump. You need an explicit displacement envelope and contact tolerance. Where precise hand placement or tool contact matters, the CPU can evaluate the same relief locally, rather than generating that detail throughout a 64 m cube. Robust contact queries still require more than checking a few points.
|
||||
|
||||
**Determinism:** retain canonical structural geometry and deterministic surface parameters. Any relief used for gameplay must have a canonical evaluator too. Lighting and final pixel calculations can remain device-dependent without changing the world’s gameplay answers.
|
||||
|
||||
A signal-processing lesson matters here: **preserve the detailed surface definition, but filter its image**. Trying to display every tiny frequency at every distance causes shimmer rather than fidelity. Procedural noise can be designed with controlled frequency content and filtering; [Gabor noise research](https://graphics.cs.kuleuven.be/publications/LLDD09PNSGC/) is one useful example. Finer detail can be inexpensive when it comes from a reusable filtered texture or compact surface function. Additional geometry, shadowing, and contact complexity are never literally free.
|
||||
|
||||
There is an Unreal-specific qualification. UE 5.7 documents Nanite tessellation as experimental, limited to Nanite meshes, and subject to displacement cracks at discontinuous attributes. Its standard `UDynamicMeshComponent` does not support Nanite. Nanite/Lumen XR support is also experimental and requires PC deferred rendering with DX12. Your PC-linked Quest fits the PC category, but this is an integration and performance risk, not a checkbox solution. [Nanite tessellation](https://dev.epicgames.com/documentation/en-us/unreal-engine/working-with-naniteenabled-content?application_version=5.7), [Dynamic Mesh limitations](https://dev.epicgames.com/documentation/en-us/unreal-engine/geometry-scripting-users-guide-in-unreal-engine?application_version=5.7), [XR support](https://dev.epicgames.com/documentation/en-us/unreal-engine/nanite-and-lumen-for-xr-in-unreal-engine?application_version=5.7).
|
||||
|
||||
I would validate the surface concept independently of whether Nanite becomes its eventual implementation.
|
||||
|
||||
**2. The radical direction: make the generator describe the cave boundary directly.**
|
||||
|
||||
Today, the generator knows how rooms and tunnels were constructed, but the mesher largely rediscovers their boundaries by asking millions of questions about space.
|
||||
|
||||
I would investigate preserving that construction knowledge all the way to geometry.
|
||||
|
||||
A chamber could generate floor patches, ceiling patches, and connecting wall patches. A tunnel could generate a swept cross-section along a curve. Columns and stalactites could generate tapered, deformed surfaces. Ledges could follow bedding planes. Floating masses could be closed procedural objects.
|
||||
|
||||
These are local two-dimensional surfaces embedded in three dimensions. Multiple overlapping levels and arbitrarily oriented patches provide overhangs and stacked caves; this does not require a globally single-valued heightfield.
|
||||
|
||||
At complicated junctions, blends, and heavily edited regions, retain your volumetric system. Those regions become exceptions with explicit boundaries.
|
||||
|
||||
The result is a world recipe containing:
|
||||
|
||||
- A graph describing connections and traversable routes.
|
||||
- Surface-producing features with conservative bounds.
|
||||
- Local volumetric regions for difficult topology.
|
||||
- Surface detail parameters and player edits.
|
||||
|
||||
This changes the generator’s responsibility. A feature should be able to supply its boundary, its bounds, and its relevant gameplay queries—not just a density value at an arbitrary position.
|
||||
|
||||
There is an artistic reason to consider this too. Geology produces strong correlations: bedding, fracture directions, drainage paths, erosion, and deposition. Those correlations are an excellent form of compression. One bedding description can explain thousands of related ledges and fractures.
|
||||
|
||||
Research on [geologically coherent cave networks](https://perso.liris.cnrs.fr/apeytavi/website/publication/hal-03331697/) demonstrates useful controls through faults, horizons, permeability, and network skeletons. That paper ultimately constructs an SDF; my proposal is to retain additional boundary knowledge through the rendering pipeline.
|
||||
|
||||
Here is the potential dimensional advantage.
|
||||
|
||||
**Illustrative estimate:** suppose the nearby region contains 20,000 m² of rock surface. The actual area in your game must be measured.
|
||||
|
||||
| Representation at 12.5 cm spacing | Approximate sample locations |
|
||||
|---|---:|
|
||||
| Your full cube, including its sampling overhead | 140 million |
|
||||
| A surface band with total thickness of four cells | 5.1 million |
|
||||
| Direct surface sampling, roughly one location per \(h^2\) | 1.3 million |
|
||||
|
||||
The narrow-band estimate is:
|
||||
|
||||
\[
|
||||
N \approx \frac{A(4h)}{h^3}=\frac{4A}{h^2}.
|
||||
\]
|
||||
|
||||
For fixed surface area and complexity, halving spacing makes surface sampling roughly **4×** larger; dense volume sampling becomes **8×** larger.
|
||||
|
||||
These are counts, not interchangeable operation costs or promised speedups. Surface intersections, charts, normals, transitions, and acceleration structures all add work.
|
||||
|
||||
Most importantly, **your existing uniform-brick skipping already captures some of this benefit**. The table cannot be claimed as an additional 27× improvement over that implementation.
|
||||
|
||||
The new opportunity is to enumerate likely surface regions directly, or emit surface geometry directly, so that you avoid both expensive field evaluations and a large search through candidate bricks. If a proposed hierarchy still performs essentially the same uncertain-brick evaluations, it has not changed your problem.
|
||||
|
||||
For the volumetric exceptions, narrow-band storage is well established; [VDB](https://museth.org/Ken/Publications_files/Museth_TOG13.pdf) provides relevant techniques. Its data structure does not, by itself, make your procedural field cheaper. Your report’s failed generic interval experiment is a good reason to insist on cheap, construction-specific bounds.
|
||||
|
||||
A useful intermediate technique preserves fine detail exactly: give the coarse description a **conservative envelope containing the full detailed boundary**, then evaluate the complete fine function only inside that envelope. This can retain the original detail where it matters. It works only if the envelope is cheap and sufficiently tight. Sampling coarse corners alone cannot certify that a thin feature is absent.
|
||||
|
||||
**What it gives:** potentially the largest lasting reduction in generation work, faster distant representations, better artistic controls, and a natural place to construct guaranteed routes.
|
||||
|
||||
**What it costs:** a substantial generator redesign, patch stitching, solid/interior classification, and maintaining a volumetric fallback. Junctions are the hard part.
|
||||
|
||||
**What it takes away:** the convenience of expressing every shape as unrestricted noise and arbitrary field operators. You can preserve output freedom through exceptions, but a world composed almost entirely of tangled exceptions will recover the expensive behaviour.
|
||||
|
||||
**Determinism:** replicate feature IDs, parameters, graph/layout, content version, and edits. Use canonical rules for patch boundaries and gameplay queries. Each client can build different temporary caches while deriving the same defined geometry.
|
||||
|
||||
This also offers a better target for precomputation. Bake a reusable library of rock profiles, fracture relief, and formation families. For scale, **256 relief tiles at 256² texels and four bytes per texel occupy 64 MiB before mipmaps**. That is an illustrative shared asset library, not a storage estimate for your whole art set. It can be reused across every seed and season.
|
||||
|
||||
**3. Make a crater immediately evaluable, then rebuild its acceleration structures later.**
|
||||
|
||||
There is a useful geometric fact behind excavation.
|
||||
|
||||
After subtracting a volume from rock, the resulting surface consists of:
|
||||
|
||||
- Surviving parts of the old rock surface.
|
||||
- Parts of the excavation boundary that lie inside the old rock.
|
||||
|
||||
For a spherical blast, the new interior surface comes from the sphere boundary. The volume in the middle becomes known air.
|
||||
|
||||
That suggests treating a spell as an immediately active geometric operation, with meshes serving as cached representations of its result.
|
||||
|
||||
For scale, a 10 m radius sphere has approximately **1,257 m²** of surface. At 12.5 cm spacing, that corresponds to roughly **80,000 surface locations**, before intersections and other overhead. Its 20 m bounding cube contains approximately **4.17 million lattice locations**.
|
||||
|
||||
This is not an 80,000-sample implementation promise. It explains why a boundary-oriented edit algorithm has a fundamentally different opportunity from rebuilding the affected volume.
|
||||
|
||||
A possible immediate path is:
|
||||
|
||||
1. Commit one canonical excavation command.
|
||||
2. Make movement and terrain queries observe it immediately.
|
||||
3. Clip affected old surfaces and render the newly exposed boundary through a local CSG or implicit rendering path.
|
||||
4. Build the enduring mesh and collision caches in the background.
|
||||
5. Replace the temporary rendering only when the replacement is complete and equivalent.
|
||||
|
||||
Image-based constructive solid geometry is an established family of techniques: [OpenCSG](https://opencsg.org/) renders Boolean combinations without first constructing their final mesh boundary. That establishes the possibility; it does not supply a ready-made Unreal terrain solution.
|
||||
|
||||
The hard parts are substantial:
|
||||
|
||||
- The new crater surface must be clipped correctly against existing caves and earlier excavations.
|
||||
- Removing the first visible wall must reveal whatever is behind it.
|
||||
- Depth, shading, shadows, and both eyes must agree.
|
||||
- Independent cap and terrain tessellation need consistent intersection boundaries.
|
||||
- The temporary and cached representations must describe the same surface closely enough to avoid a visible changeover.
|
||||
|
||||
A single regional implicit rendering path can avoid some mesh-boundary problems, but then it needs robust field traversal. Your signed density cannot automatically be treated as a safe sphere-tracing distance.
|
||||
|
||||
For overlapping spells, render the composed result. Independently displaying several crater shells produces internal surfaces that should not exist.
|
||||
|
||||
This is also where the database analogy helps. The seed/layout and ordered edits are the authoritative record. Meshes, collision bodies, and GPU pages are derived indexes. An edit can become semantically active before every index is rebuilt, provided queries correctly combine the old index with pending changes.
|
||||
|
||||
**What it gives:** a credible architecture for same-frame excavation whose latency does not depend on draining dozens of component updates.
|
||||
|
||||
**What it costs:** custom rendering and terrain-query integration, plus management of overlapping edits and temporary representations.
|
||||
|
||||
**What it takes away:** a cheap universal implementation for arbitrarily complicated edit operators. Spheres, capsules, and bounded procedural blasts are favourable. Unbounded numbers of overlapping edits still require work; the supported spell load needs a defined budget.
|
||||
|
||||
**Determinism:** distribute canonical edit parameters and ordering. Immediate queries and later meshes must implement the same composition rules. A CPU-produced canonical lattice or a specified integer GPU evaluator can provide agreement where the rendering path needs authoritative signs.
|
||||
|
||||
“Same frame” should mean the first submitted frame containing the accepted command. Remote machines necessarily see commands after network delivery; local prediction can hide that latency. Within each machine, the whole affected region must use the same edit state.
|
||||
|
||||
I would regard this as a complement to your surface-detail idea. Faster generation alone does not create atomic visual publication.
|
||||
|
||||
**4. Reopen GPU generation specifically through defined integer arithmetic.**
|
||||
|
||||
Rejecting ordinary cross-vendor floating-point generation was reasonable. There is still a separate option: a deliberately restricted, integer-defined world evaluator running on both CPU and GPU.
|
||||
|
||||
The reason to revisit fixed point would be access to GPU parallelism while preserving the numeric contract. It would not be an assumption that integers are inherently faster than your optimized CPU floats.
|
||||
|
||||
The contract needs explicit widths, rounding, overflow behaviour, lookup tables, coordinate decomposition, and operation ordering. Canonical decisions cannot quietly pass through vendor-dependent floating-point functions or texture filtering. Microsoft’s [DXIL specification](https://github.com/microsoft/DirectXShaderCompiler/blob/main/docs/DXIL.rst) defines relevant integer operations, including full multiplication results and unfiltered data loads.
|
||||
|
||||
Quantizing an ordinary float result at the end is insufficient: two computations can land on opposite sides of a quantization boundary.
|
||||
|
||||
A practical division would be:
|
||||
|
||||
- GPU evaluates bulk geometry or surface refinement.
|
||||
- CPU evaluates the small subset required for immediate gameplay.
|
||||
- Both implement the same defined rules.
|
||||
- Gameplay avoids waiting for GPU readback.
|
||||
|
||||
**What it gives:** potentially large throughput while retaining seed-and-edits networking.
|
||||
|
||||
**What it costs:** a numerical implementation project, validation across supported vendors, GPU scheduling, and less freedom in procedural operators. Integer divisions or wide arithmetic can be expensive.
|
||||
|
||||
**What it takes away:** convenient use of arbitrary floating-point shader math for authoritative geometry. It also consumes the GPU budget needed for stereo rendering.
|
||||
|
||||
**Determinism:** matching canonical outputs is a release requirement, tested across CPU and GPU implementations and supported devices. It is achievable through a carefully specified implementation; the word “integer” alone does not prove it.
|
||||
|
||||
For scale, 140 million 16-bit samples occupy approximately **280 MB** before halos and other buffers. Storage alone is manageable on your target GPU. Producing and using those values quickly enough remains the actual question.
|
||||
|
||||
I would pursue this after reducing the amount of work. Otherwise, every future resolution increase retains the same cubic growth.
|
||||
|
||||
There are three requirements that deserve independent treatment regardless of the representation.
|
||||
|
||||
**Player readiness should depend on the player’s reachable surroundings.** A 4 m cube at 12.5 cm spacing contains about **36,000 lattice points**. At your stated evaluator rate, that is roughly **3–5 ms of aggregate evaluation work**, excluding setup, meshing, and collision installation. It is an illustration of the scale difference, not a proposed universal collision neighbourhood.
|
||||
|
||||
A moving or falling player needs a region covering the swept capsule and its near-future motion, including walls and ceilings. A cold teleport needs a valid destination and immediate terrain queries or a small ready collision patch. None of that inherently requires the complete 64 m visual neighbourhood.
|
||||
|
||||
The engineering challenge is reliable swept contact and edit-aware support. Your measured cook queue problem reinforces the need to keep that deadline independent. Retaining an obsolete floor after excavation is as incorrect as temporarily losing a floor that still exists.
|
||||
|
||||
**Kilometres of visibility should be served by compact distant surfaces and conservative visibility.** Underground occlusion can eliminate large amounts of rendering work, while a vast open chamber still needs a cheap distant representation.
|
||||
|
||||
However, generating only currently visible surfaces is incompatible with guaranteed immediate revelation by blasting. Keep inexpensive distant representations available and detailed geometry within the region that nearby edits can expose. Destruction also needs to update any portal or occlusion assumptions.
|
||||
|
||||
**Walking connectivity must include support.** A connected graph of air spaces is insufficient. Construct at least one entrance-to-exit route with constrained slope, step height, capsule clearance, and continuous supporting floor. Bound later geometric decoration against that route.
|
||||
|
||||
If this guarantee must survive arbitrary player edits, there is a real design conflict: excavation can remove the only bridge while increasing air connectivity. Maintaining the guarantee then requires protected support, alternative routes, or repair. That is a gameplay rule that no representation can avoid.
|
||||
|
||||
I would spend the next experimental effort on **a deterministic structural world with genuinely detailed surface refinement and an immediate edit representation**.
|
||||
|
||||
The first prototype could retain your current generator at 25 cm for ordinary structure, preserve finer volumetric regions for small holes and thin features, and add the surface layer. Your measured 25 cm result makes this a much more grounded starting point than a complete renderer replacement.
|
||||
|
||||
I would judge it through four demonstrations:
|
||||
|
||||
- Close VR inspection: silhouettes, grazing views, near-hand relief, and patch boundaries meet the owner’s quality bar.
|
||||
- A cold teleport: valid support becomes available independently of the full visual region.
|
||||
- A 10 m blast: the complete result appears together, including an underfoot edit and an opening into another cave.
|
||||
- Cross-machine comparison: canonical geometry and terrain-query results agree.
|
||||
|
||||
If the surface experiment fails because the required detail is truly topological or physically interactive, preserve that fine geometry and move harder toward direct surface generation and narrow volumetric exceptions.
|
||||
|
||||
My longer-term bet is the second direction: **a generator that retains explicit knowledge of its surfaces**. It fits the geological structure of your target, gives walkability a natural foundation, and offers a route to increasing detail without repeatedly multiplying the work throughout every cubic metre of the world.
|
||||
|
||||
---
|
||||
|
||||
# Round 2 (fork): shorelines, fluid-looking rock, invention — 2026-10-02
|
||||
|
||||
Prompt: scratchpad astra-think-2.md. Answer verbatim below.
|
||||
|
||||
**I would now give important contacts their own geometry, and give moving rock a stable “material space” in which cuts persist.** Those two additions address the new problems and strengthen the earlier surface-based direction.
|
||||
|
||||
The shoreline exposes a weakness in treating independently generated representations as if they will automatically agree. Moving rock exposes a different weakness: defining a world only by where its surface happens to be this frame.
|
||||
|
||||
Below, “proposed” means an architectural suggestion that needs testing. “Speculative” means I see a plausible mechanism, but would not plan production around it yet. Numerical estimates are arithmetic, not measured Unreal performance.
|
||||
|
||||
**1. Make the shoreline a shared geometric boundary. Established geometry techniques; proposed terrain integration.**
|
||||
|
||||
A flat water surface is a reasonable representation of a still cave pool. It is also an unusually effective instrument for revealing geometric error.
|
||||
|
||||
Consider a shallow bank whose height rises by 2 cm per horizontal metre. Locally,
|
||||
|
||||
\[
|
||||
\text{shoreline position error}\approx
|
||||
\frac{\text{terrain height error}}{\text{bank slope}}.
|
||||
\]
|
||||
|
||||
A **1 cm height error on that 2% slope can move the shoreline by approximately 50 cm**. This explains why terrain that looks acceptable elsewhere can look conspicuously wrong at water level.
|
||||
|
||||
Marching cubes produces piecewise planar surfaces, so its intersection with a plane is piecewise linear. Additional stair-like appearance can come from density quantization, displacement, LOD transitions, or a water mask derived from another representation. Smooth normals can improve lighting, but cannot repair the geometric outline.
|
||||
|
||||
My proposal is to make the intersection an explicit curve shared by both surfaces.
|
||||
|
||||
For a flat pool at height \(H\), find the curve where the intended rock surface meets \(z=H\). Then:
|
||||
|
||||
- Make the nearby rock triangles conform to that curve.
|
||||
- Make the water boundary use the same curve.
|
||||
- Derive wetness, deposits, and near-bank effects from it.
|
||||
- Refine the rock in a narrow surrounding strip to preserve the correct shape and normals.
|
||||
|
||||
The “intended rock surface” must include any displacement that materially changes the contact. Computing the curve from coarse collision geometry while rendering a substantially displaced bank simply introduces another disagreement.
|
||||
|
||||
This is related to boundary representation in CAD: intersections are first-class geometry. Guenter and Gavriliu’s [Generative CSG Models for Real Time Graphics](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-2005-43.pdf) explicitly represents intersection curves and uses them for adaptive triangulation. Its expensive preprocessing makes it a conceptual precedent, not evidence that arbitrary cave intersections can be reconstructed instantly.
|
||||
|
||||
The favourable part is dimensionality. **A shoreline is primarily a one-dimensional problem.**
|
||||
|
||||
For illustration, a 100 m shoreline sampled every 2 cm has approximately **5,000 curve locations**. A surrounding strip 40 cm wide, sampled at the same spacing, contains roughly **100,000 surface locations**. Root finding, transitions, normals, and topology add work, but this is a very different expenditure from refining a volume.
|
||||
|
||||
There is a useful rendering refinement here too: maintain separate tolerances for **position and normal direction**. Accurate contact positions fix the outline; accurate surface normals fix the faceted reflection. One tolerance does not guarantee the other.
|
||||
|
||||
- **Cost and loss:** substantial meshing work around intersections, plus handling curve splitting and merging after edits. Near tangencies, an intersection can appear or disappear, so tracing one existing curve is insufficient to discover every component.
|
||||
- **Multiplayer:** derive the curve from the canonical shape, water level, and edit version using specified numeric rules. Different clients can tessellate between shared boundary constraints differently for rendering. Gameplay-relevant bank geometry remains canonical.
|
||||
- **First cheap experiment:** use one analytically defined bowl with a ledge, intersected by flat water. Render unlit colours first, then reflective materials. Shift the sampling-grid origin while holding the underlying bowl fixed. Add a conforming shoreline strip and check whether the contact becomes stable without refining the whole bowl. Also raise the water through the ledge to expose topology failures.
|
||||
|
||||
**2. Generate pools from their shorelines and geological history. Proposed construction method, with strong natural precedents.**
|
||||
|
||||
For generated pools, there is an even cheaper possibility: **choose the shoreline before constructing the surrounding bank**.
|
||||
|
||||
The recipe could start with:
|
||||
|
||||
- Water elevation and an irregular closed boundary.
|
||||
- A bank profile along that boundary: steep fracture, shallow shelf, undercut, sediment apron, or mineral lip.
|
||||
- A basin interior.
|
||||
- A transition into the surrounding cave surfaces.
|
||||
|
||||
The rock is then constructed to meet the water deliberately. Noise modifies that construction within limits that preserve the intended contact.
|
||||
|
||||
This is particularly suitable for your visual target because natural caves already provide expressive shoreline structures. Flowstone forms through mineral deposition by flowing films; rimstone dams hold pools and grow through deposition where water passes over them. The [National Park Service’s cave geology account](https://www.nps.gov/grba/planyourvisit/cave-geology-in-depth.htm) describes these processes and formations.
|
||||
|
||||
A pool could therefore have a fine scalloped mineral lip, a darker wet band, submerged sediment, and occasional fractured interruptions. Those features explain the contact visually and give the generator meaningful parameters.
|
||||
|
||||
Recent research also supports treating contours as primary terrain descriptions. [Huftier and colleagues’ 2026 contour-based terrain work](https://perso.liris.cnrs.fr/apeytavi/website/publication/hal-05608822/) generates and edits terrain through elevation contours. It addresses heightfield terrain, so extending it to cave overhangs and intersecting chambers remains a separate problem. I would apply the idea locally around pools.
|
||||
|
||||
The water itself can contribute appropriate detail:
|
||||
|
||||
- Small waves whose behaviour changes near banks.
|
||||
- A restrained meniscus.
|
||||
- Wetness and sediment colour gradients.
|
||||
- Refraction that reveals the submerged bank.
|
||||
- Foam where there is actual agitation.
|
||||
|
||||
However, a meniscus is a small-scale effect. Water’s capillary length is approximately **2.7 mm**, so it should not be expected to conceal decimetre-scale geometric problems in a broad pool. [MIT’s capillarity notes](https://web.mit.edu/nnf/education/wettability/gravity.html) give that scale. Similarly, a wide foam outline around every still cave pool would become its own procedural signature.
|
||||
|
||||
I would distinguish **current contact** from **historical deposits**. Lowering the water should reveal old mineral marks. Blasting a rim should remove it; the generator should not immediately grow a replacement decorative lip around the crater.
|
||||
|
||||
- **Cost and loss:** more authoring logic and a preference for pools with intentional bank structure. Freely changing water levels still require fresh intersection handling outside the originally constructed contact.
|
||||
- **Multiplayer:** pool outlines, profiles, historical water levels, and structural deposits belong to the seeded recipe. Current water levels must be authoritative wherever swimming, drainage, or access depends on them. Tiny ripples and wet highlights can be cosmetic.
|
||||
- **First cheap experiment:** generate one pool with three bank styles from a shared curve. Lower its water, then cut through its rim. The experiment succeeds only if the bank remains convincing, the old watermark stays put, and the excavation does not trigger decorative “healing.”
|
||||
|
||||
**3. Give stone the visual language of flowing material before moving its bulk. Established rendering techniques; artistic hypothesis.**
|
||||
|
||||
There are several distinct ways for rock to look fluid:
|
||||
|
||||
| Appearance | What actually changes |
|
||||
|---|---|
|
||||
| Frozen waves, folds, or drapery | Static geometry |
|
||||
| Veins and surface patterns flowing over stone | Material coordinates and shading |
|
||||
| Ripples crossing a wall | Shading plus shallow displacement |
|
||||
| A wall visibly bulging into a passage | Gameplay geometry, if reachable |
|
||||
| Stone pouring, splitting, or merging | Material transport and topology |
|
||||
|
||||
The first three can provide a large part of the desired effect at much lower cost than the last two.
|
||||
|
||||
For a living wall, I would try a combination of stationary large fractures and slowly travelling mineral ridges. The motion would have a direction and coherent scale: sweeping upward, curling around columns, spreading from impacts, or descending into basins.
|
||||
|
||||
A flow map supplies local direction and speed. Valve demonstrated this approach for animated normal and colour textures in [Water Flow in Portal 2](https://advances.realtimerendering.com/s2010/index.html#Water%20Flow%20in%20Portal%202). Applied to stone, it could transport veins, roughness variation, and shallow relief.
|
||||
|
||||
For more organic motion, [curl noise](https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph2007-curlnoise.pdf) supplies procedural fluid-like velocity fields with mechanisms for respecting boundaries. It does not require running a full fluid simulation. Transporting textures or particles through that field still needs an appropriate animation scheme; an attractive velocity field alone does not solve stretching, looping, or temporal consistency.
|
||||
|
||||
My artistic hypothesis is that **coherent folds and motion at a few scales will read as flowing stone more convincingly than many animated noise octaves**. Keep the reflectance recognizably mineral unless the intention is explicitly wet or molten material.
|
||||
|
||||
Frozen waves deserve their own inexpensive approach: run an expensive viscous simulation during asset creation, select compelling states, then turn them into reusable surface motifs. Research on [viscous buckling, folding, and coiling](https://www.cs.ubc.ca/labs/imager/tr/2008/Batty_ViscousFluids/) demonstrates precisely the kinds of shapes that could become stone draperies and frozen cascades. Runtime generation would place and deform the resulting recipes or assets; it would not reproduce the fluid simulation.
|
||||
|
||||
- **Cost and loss:** shader sampling, texture transport, and possibly fine surface geometry. Flowing shading has limited power at silhouettes and under close VR inspection. Frozen motifs need enough variation to avoid repetition.
|
||||
- **Multiplayer:** fixed structural geometry remains the authority. Shared assets and seeds define frozen forms. Cosmetic motion can use a common phase without requiring identical pixels. Any displacement large enough to alter cover, contact, or digging must move into the authoritative category.
|
||||
- **First cheap experiment:** use one static wall and compare three versions: flowing material only, material plus shallow relief, and a frozen folded surface. Inspect them in VR from the side and with a stationary hand or tool nearby. This quickly reveals how much physical movement the desired effect actually needs.
|
||||
|
||||
**4. Animate a few controlled deformation patterns. Established animation mathematics; proposed gameplay integration.**
|
||||
|
||||
For actual breathing geometry, I would begin with a small set of motion patterns:
|
||||
|
||||
\[
|
||||
S(u,v,t)=S_0(u,v)+\sum_k a_k(t)\,B_k(u,v).
|
||||
\]
|
||||
|
||||
Here, \(S_0\) is the resting surface. Each \(B_k\) is a pattern such as a bulge, travelling ridge, or twist. The coefficient \(a_k(t)\) controls its strength.
|
||||
|
||||
The useful property is that the shape can be evaluated directly at a requested time. A client arriving late does not have to simulate every previous frame.
|
||||
|
||||
Patterns can be designed to vanish along protected boundaries: the floor, a doorway, a pool edge, or the edges of an animated region. Where reflections reveal changes in slope, make the deformation’s first derivative vanish there too.
|
||||
|
||||
Impact-driven motion has a particularly relevant precedent: [Dynamic Kelvinlets](https://graphics.stanford.edu/~djames/publication/dynamic-kelvinlets-secondary-motions-based-on-fundamental-solutions-of-elastodynamics/). These provide analytical elastic responses such as ripples and waves from localized forces, avoiding a conventional volumetric simulation. They are models of elastic secondary motion, not complete fluid or fracture simulation.
|
||||
|
||||
A spell could therefore produce two effects:
|
||||
|
||||
1. A permanent excavation command.
|
||||
2. A temporary elastic wave, defined by its origin, start time, strength, and decay.
|
||||
|
||||
For an initial version, I would keep the walkable floor stationary while nearby walls respond. Later, genuinely moving floors can use the same evaluator for geometry and terrain contacts.
|
||||
|
||||
**Motion speed and acceleration matter as much as displacement amplitude.** For a sinusoidal movement:
|
||||
|
||||
- An amplitude of 3 cm at 0.5 Hz gives peak acceleration of approximately **0.30 m/s²**.
|
||||
- An amplitude of 20 cm at 2 Hz gives approximately **32 m/s²**, over three times gravity.
|
||||
|
||||
The latter is not a gently breathing floor. Correct physics would make it a powerful moving platform.
|
||||
|
||||
A rough work estimate: a 10 m square patch sampled every 5 cm has about **40,000 vertices**. Eight patterns require approximately **320,000 pattern contributions per geometry pass**. Stereo, shadows, normals, and shading increase that work. This is an operation count, not a frame-time prediction, but it shows why surface animation can be much cheaper than regenerating a density volume every frame.
|
||||
|
||||
- **Cost and loss:** deformation evaluation, updated normals and bounds, correct shadows and previous-frame positions, and custom terrain contact handling for substantial movement. Ordinary shader vertex displacement does not automatically update collision. The result has a controlled motion vocabulary rather than unrestricted fluid behaviour.
|
||||
- **Multiplayer:** use authoritative ticks and a versioned evaluator. Evaluate coefficients from time and compact events, avoiding frame-by-frame accumulation. Gameplay-relevant arithmetic needs the same determinism discipline as terrain generation; a shared sine expression alone is insufficient.
|
||||
- **First cheap experiment:** animate one wall with four patterns, pin its lower edge, and trigger one impact wave. Evaluate the same timestamps after different frame-rate histories and a late join. Then make a small platform move and test whether contact velocity is handled correctly. Stop if it requires recooking terrain collision every frame.
|
||||
|
||||
A deformation also needs limits on spatial variation. Small amplitude does not prevent very short waves from folding a surface over itself.
|
||||
|
||||
**5. Let moving stone remember cuts in material coordinates. Speculative architecture; established invertible-map mathematics.**
|
||||
|
||||
This is the direction I find most interesting.
|
||||
|
||||
Imagine every piece of rock carries permanent coordinates, like markings embedded throughout a block of clay. The clay can deform, but those coordinates stay attached to its material.
|
||||
|
||||
Define the rock and its excavations in those coordinates. A time-dependent mapping moves the material into the visible world:
|
||||
|
||||
\[
|
||||
x=T_t(q),
|
||||
\qquad
|
||||
F_t(x)=F_{\text{edited}}\!\left(T_t^{-1}(x)\right).
|
||||
\]
|
||||
|
||||
In plain terms:
|
||||
|
||||
- To render a point of rock, move it forward through the deformation.
|
||||
- To query the rock at a world position, undo the deformation and query the stored shape.
|
||||
- A hole follows the material because it is stored with the material.
|
||||
|
||||
Between edits, the reference geometry can remain cached. Animation changes its positions rather than repeatedly rediscovering its topology.
|
||||
|
||||
The difficult requirement is a cheap, reliable inverse.
|
||||
|
||||
One construction uses successive nonlinear shears. Shift \(x\) by an amount depending on \(y,z\); then shift \(y\) by an amount depending on the updated \(x,z\). Each step can be undone by subtraction in reverse order. Smooth additive shears preserve volume individually and remain invertible. The same mathematical construction appears as additive coupling layers in [NICE](https://arxiv.org/html/1410.8516v6); no neural network or training is required for this use.
|
||||
|
||||
My speculative application is a collection of such maps producing slow eddies, waves, and folds inside selected rock regions.
|
||||
|
||||
An additional experiment would be:
|
||||
|
||||
> Apply an x-shear, then a y-shear, then undo the x-shear, then undo the y-shear.
|
||||
|
||||
Because the operations act on already changed coordinates, they generally leave a deformation inside their interaction region. With appropriately localized shear functions, their effects can cancel outside a bounded surrounding region. That suggests a localized, invertible “stone eddy.” The support bounds and resulting distortion would need to be verified.
|
||||
|
||||
The digging rule is essential.
|
||||
|
||||
If a spherical blast occurs while the rock is bent, **pull the entire blast shape back into material coordinates**. Transforming only the sphere’s centre and keeping its radius would usually be wrong: a nonlinear deformation turns its inverse image into a more complicated shape.
|
||||
|
||||
A compact edit can retain the blast parameters and the frozen deformation parameters from the instant of impact. The cut then remains attached to the removed material as the rock continues moving.
|
||||
|
||||
A permanently world-fixed exclusion volume is another possible game mechanic, but it has different semantics. It continually clips whatever material enters it. I would make that distinction explicit rather than accidentally letting craters slide or appear to heal.
|
||||
|
||||
There are several important limits:
|
||||
|
||||
- Invertibility preserves topology during deformation; it does not preserve walkable slope or clearance.
|
||||
- A deformed signed field is generally no longer a true distance field.
|
||||
- Inverse-transforming a capsule does not leave an ordinary capsule under a nonlinear map.
|
||||
- The exact smooth surface may remain valid while an insufficiently subdivided triangle approximation develops artifacts.
|
||||
|
||||
Consequently, this does not make collision trivial. It makes the shape and edit history compact and queryable.
|
||||
|
||||
- **Cost and loss:** a significant research and physics-integration effort, inverse mappings on queries, distortion bounds, and material-space edit evaluation. Deformation alone cannot reproduce fluid breakup or merging; those require separate topology operations.
|
||||
- **Multiplayer:** canonical mapping parameters, simulation time, and material-space edits define the same world. Carefully defined fixed-point shears can even have exactly reversible coordinate updates when overflow is excluded, although that does not by itself prove the whole collision system correct.
|
||||
- **First cheap experiment:** one deforming slab, one spherical excavation, and a few ray/contact queries. Cut it at maximum deformation, return to rest, then deform it again. Check that the same material stays missing. Replay at different frame rates and test forward/inverse round trips. This should be settled before attempting a whole animated chamber.
|
||||
|
||||
**6. Make contacts and protected routes part of the generator’s input. Speculative unifying design.**
|
||||
|
||||
The broader architectural change I would now investigate is to give a region an explicit collection of important geometric constraints:
|
||||
|
||||
- A pool’s contact boundary.
|
||||
- A doorway’s edge.
|
||||
- A continuously supported walking route.
|
||||
- The attachment of a column.
|
||||
- The boundary between stationary and moving rock.
|
||||
- The rim created by a recent excavation.
|
||||
|
||||
Surface patches are then generated and refined around those constraints. Detail and motion have budgets relative to them.
|
||||
|
||||
For example, a chamber could contain a still pool and a stationary path beside it. The rock higher on the wall could ripple strongly. Its deformation would fade smoothly to zero at the protected boundaries. Fine mineral patterns could continue flowing through those zones because they do not alter support.
|
||||
|
||||
That permits visibly dramatic motion without moving every gameplay-critical surface.
|
||||
|
||||
It also suggests a better refinement policy. Allocate geometry according to the error that matters locally:
|
||||
|
||||
| Location | Important error |
|
||||
|---|---|
|
||||
| Water contact | Position of the intersection |
|
||||
| Reflective rock | Surface-normal direction |
|
||||
| Narrow passage | Minimum capsule clearance |
|
||||
| Animated floor | Contact position and velocity |
|
||||
| Ordinary distant wall | Projected image error |
|
||||
|
||||
A uniform spatial resolution is a poor proxy for these different requirements.
|
||||
|
||||
Motion envelopes help here. A generator can prove that a wall moving within a known envelope cannot enter a protected corridor. But that envelope is a clearance or broad-phase tool: it cannot replace the actual surface for close player contact without creating visible collision discrepancies.
|
||||
|
||||
I would also keep these constraints independent of each client’s camera. Freezing a wall only when the local player approaches is liable to create conflicting worlds. Use fixed protected zones or an explicit authoritative state change.
|
||||
|
||||
- **Cost and loss:** more information in the world recipe, coupled generation rules, and resolving conflicting constraints. Some combinations of motion, thin walls, and route clearance must be rejected or altered.
|
||||
- **Multiplayer:** the constraints and motion limits belong to the canonical layout. Rendering can vary its subdivision while respecting the same boundaries and gameplay surface.
|
||||
- **First cheap experiment:** construct one chamber containing a pool, a door, a walking strip, and an animated wall. Keep those contacts stable while varying the surrounding shape and motion. If this requires special-case fixes at every junction, the representation needs simplifying before expansion.
|
||||
|
||||
The division I would use for moving rock is therefore:
|
||||
|
||||
| Feature | May be cosmetic? |
|
||||
|---|---|
|
||||
| Colour, roughness, veins, fine normal variation | Yes, unless used to communicate gameplay state |
|
||||
| Very small relief | Yes, within a tested visual/contact tolerance |
|
||||
| Large movement in an unreachable region | Potentially; still needs appropriate bounds and visual interactions |
|
||||
| Reachable bulges, footholds, cover, passage clearance | Requires authoritative geometry and queries |
|
||||
| Excavation and material removal | Requires authoritative edit semantics |
|
||||
|
||||
For implementation research, **the shared shoreline curve is my strongest near-term bet**. It concentrates precision where the image is unusually sensitive and can be tested without replacing the whole terrain system.
|
||||
|
||||
For invention, **the invertible material-space wall is the experiment I would fund**. It offers a plausible way to combine motion, persistent digging, compact networking, and cached geometry. Its first success criterion should be modest and strict: one moving slab whose hole, visible surface, and contact queries continue to agree.
|
||||
|
||||
I would revise the earlier surface-first recommendation accordingly: retain explicit knowledge of the surfaces, their important intersections, and the material coordinates that survive animation. Those are useful facts the generator can provide once and reuse throughout rendering, editing, and gameplay.
|
||||
|
||||
---
|
||||
|
||||
# Round 3 (fork): performance only, unrealistic goal (whole view < 1 s) — 2026-10-02
|
||||
|
||||
Prompt: scratchpad astra-think-3.md. Answer verbatim below.
|
||||
|
||||
**I would bet on a generator that emits boundaries and solid intervals directly.** The distant world would come from a hierarchy of surface pieces. Nearby Boolean operations—joining caves, digging, blasting—would operate on intervals and packed bits. A density function could remain available for queries, but generating a view would no longer mean filling its volume with density samples.
|
||||
|
||||
I can construct a **900 ms engineering budget** for this design. That is a target to test, not a measured prediction. Its essential condition is that the world’s composition limits the amount of surface work required. An arbitrary field can contain arbitrarily much geometry; no representation can guarantee a second without some complexity contract.
|
||||
|
||||
The proposed architecture combines established techniques. The particular composition rules, division of work, and budget below are my proposal.
|
||||
|
||||
**There is a larger opportunity in your current far field than the word “clipmap” suggests.**
|
||||
|
||||
Your source supports expanding the outermost ring at a fixed maximum LOD. The checked-in settings use `RenderDistanceChunks=1536` and `MaxClipLevel=5`: that gives an outer radius of 48 tiles, approximately **97 × 97 horizontal positions per vertical slice**, before exclusions. Those are defaults, not a census of your running world. Nevertheless, this can turn the horizon into a large fixed-resolution grid. The settings explain why: coarser volume sampling can miss thin strata. [Settings](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelSettings.h:166>), [outer-ring calculation](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelClipmapDesiredTiles.h:46>).
|
||||
|
||||
That is a representation problem with a performance consequence. **A distant floor should survive because it is explicitly a floor, rather than because a coarse lattice happens to intersect it.**
|
||||
|
||||
There is a similarly useful distinction nearby: your column evaluator already shares setup across heights, but still loops through every Z sample. The opportunity below is to remove that loop by solving where the column enters and leaves rock. [Column evaluator](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp:2851>).
|
||||
|
||||
**First, make each world piece know how to produce its surface.**
|
||||
|
||||
A room, tunnel segment, shelf, shaft, pillar or floating mass would carry a small description with:
|
||||
|
||||
- Its bounds and connections.
|
||||
- A way to emit its surface at several resolutions.
|
||||
- A way to intersect an axis-aligned line with its solid.
|
||||
- Shared boundary curves for joining neighbouring pieces.
|
||||
- A limit on how much refinement and Boolean work it can require.
|
||||
|
||||
An untouched tunnel would emit a swept surface. A chamber would emit patches with explicitly constructed openings. A floating mass would emit a closed surface. These operations evaluate positions **on the surface**, without searching a surrounding volume for it.
|
||||
|
||||
The difficult part is junctions. Drawing overlapping closed room and tunnel meshes leaves unwanted internal walls. I would begin with a bounded family of junction constructions whose openings, trimming and transitions are already defined. More unusual intersections would use the local interval compiler described below.
|
||||
|
||||
This is a substantial generator redesign. Its advantage is that it makes the cost of a feature predictable.
|
||||
|
||||
The hierarchy must also be **constructed from coarse to fine**. A parent knows the major chamber boundaries, floor sheets and passages before its children exist. Children add bounded shape variation and smaller features. A distant parent can therefore be rendered without generating and simplifying all its descendants.
|
||||
|
||||
That does **not** make a coarse mesh an exact copy of every fine surface. What can be exact is the parent’s enclosure, connection information and refinement contract. The visible surface remains an approximation with a specified error.
|
||||
|
||||
For deterministic connections, neighbouring pieces can derive their shared opening from the same integer face key. Each side then receives identical opening geometry. Matching-boundary composition has a precedent in Wang tiles; extending it to a hierarchical cave grammar with guaranteed connectors is my proposed adaptation. It needs a constructive connector for every allowed boundary configuration, otherwise a search problem returns. [Cohen et al., *Wang Tiles for Image and Texture Generation*](https://graphics.uni-konstanz.de/publikationen/Cohen2003WangTilesImage/index.html).
|
||||
|
||||
Build the initial walkable route into this hierarchy, with enforced clearance and floor slope. Its existence then follows from construction; discovering it does not require generating the whole cave and running navigation over it.
|
||||
|
||||
**The far field would contain selected surface patches, including explicit thin strata. It would contain no density tiles.**
|
||||
|
||||
All selected LODs must be ready by the deadline. Showing a coarse preview while the requested near surface is still missing would not count.
|
||||
|
||||
**Second, replace point evaluation with interval algebra wherever Boolean composition is necessary.**
|
||||
|
||||
Consider a spherical cavity. At a fixed horizontal position \((x,y)\), define
|
||||
|
||||
\[
|
||||
d=r^2-(x-c_x)^2-(y-c_y)^2.
|
||||
\]
|
||||
|
||||
If \(d<0\), that column misses the cavity. Otherwise its entire air interval is
|
||||
|
||||
\[
|
||||
[c_z-\sqrt d,\;c_z+\sqrt d].
|
||||
\]
|
||||
|
||||
One calculation has answered every height in that column.
|
||||
|
||||
Multiple rooms and tunnels produce lists of intervals. Joining cavities becomes interval union. Adding a pillar removes an interval from air. A crater adds an air interval. There is no need to evaluate the composed function at every Z coordinate.
|
||||
|
||||
This is related to **dexel representations**, used in machining and virtual sculpting: solids are represented by entry and exit depths along parallel lines, and cutting becomes interval operations. This is established machinery, not a new claim about solid modelling. [Peng et al., *Surface Reconstruction from Dexel Data for Virtual Sculpting*](https://scholarsmine.mst.edu/mec_aereng_facwork/1226/).
|
||||
|
||||
For your world, I would use:
|
||||
|
||||
- Quadrics for ellipsoids, cones and suitable chamber shapes.
|
||||
- Piecewise analytic segments for tunnels.
|
||||
- Explicit floor/ceiling intervals for shelves and room interiors.
|
||||
- Bounded local exceptions for shapes outside that family.
|
||||
|
||||
Using line families along **all three axes** provides boundary intersections in each direction, including on vertical walls. A 256-cell cube has approximately **198,000 lattice lines across the three directions**, versus roughly 17 million lattice points. If each line encounters an average of 12 candidate primitives, that is about **2.4 million primitive-line tests**.
|
||||
|
||||
That is an operation-count estimate, not a speedup measurement: root solving and interval merging cost more than a single comparison. However, each test can replace many expensive calls to your complete density evaluator.
|
||||
|
||||
This method becomes unattractive if every line crosses hundreds of tiny features. The generator must keep that complexity bounded, or represent those features directly as surfaces.
|
||||
|
||||
**There are two useful ways to exploit 64-bit words here.**
|
||||
|
||||
The first is to represent which primitives are active while sweeping through sorted interval endpoints. With at most 64 local primitives, their membership fits in one word. At a crossing, update the corresponding bit. Between crossings, membership is constant, so the local Boolean expression has one answer for the entire span.
|
||||
|
||||
For a restricted construction such as “rock minus rooms and tunnels, plus pillars, minus craters,” the answer needs only a few tests of those membership masks. Arbitrary CSG trees require more work; the simple expression is a deliberate composition rule.
|
||||
|
||||
The second is to store **64 consecutive rock/air signs in one word**:
|
||||
|
||||
- Union, intersection and subtraction become OR, AND and AND-NOT.
|
||||
- A long uniform span writes whole words.
|
||||
- XOR with a neighbouring column identifies changed signs along X or Y.
|
||||
- XOR with a shifted word identifies changed signs along Z.
|
||||
- Bit scans enumerate only the interesting locations.
|
||||
|
||||
For marching cubes, align the eight corner-sign words for 64 cells. Then
|
||||
|
||||
\[
|
||||
\text{active}=\operatorname{OR}(\text{eight words})
|
||||
\;\land\;
|
||||
\neg\operatorname{AND}(\text{eight words}).
|
||||
\]
|
||||
|
||||
That identifies 64 potentially intersected cells using word operations. `std::countr_zero` and clearing the lowest set bit can enumerate the survivors. Binary greedy meshers demonstrate the usefulness of processing occupancy and face masks this way. Their block-shaped output is not what I propose using for cave surfaces. [Binary Greedy Meshing](https://github.com/cgerikj/binary-greedy-meshing).
|
||||
|
||||
The smooth surface comes from the **actual boundary crossings**, with quantized positions and normals, not from placing vertices halfway between binary samples.
|
||||
|
||||
Two qualifications matter:
|
||||
|
||||
1. Packing an arbitrary field into bits does not make generating those bits cheap. The interval construction is what removes the evaluations.
|
||||
2. Equal corner signs can hide a thin feature entirely inside a cell. Such features need explicit primitive-intersection tags, refinement, or direct surface geometry. Binary signs alone cannot preserve them.
|
||||
|
||||
A near ring with 17.4 million signs needs approximately **2.2 MB**, compared with approximately **69.6 MB for float densities**. Boundary data and meshes are additional, but the interior representation becomes small.
|
||||
|
||||
**There is a particularly interesting mathematical family to investigate: piecewise triquadratic fields.**
|
||||
|
||||
This is an optional extension, not something the first prototype must depend on.
|
||||
|
||||
A polynomial that is quadratic separately in X, Y and Z has 27 coefficients. It offers two useful properties:
|
||||
|
||||
- Restrict it to any axis-aligned line, and it becomes a quadratic: at most two isolated crossings unless the polynomial degenerates.
|
||||
- Express it in the Bernstein basis over a box, and its value lies between the smallest and largest coefficients.
|
||||
|
||||
Consequently, one small coefficient block supplies both cheap line intersections and conservative whole-box sign bounds. All coefficients positive means the entire box is air; all negative means rock. Subdivision can tighten the bounds.
|
||||
|
||||
Bernstein bounds and subdivision are established mathematics used in implicit-surface research. **My speculative proposal is to make this restricted polynomial family part of the cave’s native construction language**, rather than fit it to an already expensive noise field. [Bán and Valasek, *Generalized Lipschitz Tracing of Implicit Surfaces*](https://onlinelibrary.wiley.com/doi/full/10.1111/cgf.15202).
|
||||
|
||||
This distinction matters: fitting an arbitrary existing field would reintroduce sampling and approximation problems.
|
||||
|
||||
Bounded detail can be handled similarly. If a parent function has a known range \([L,U]\), and all omitted detail together is bounded by \(R\), then
|
||||
|
||||
\[
|
||||
F_{\text{complete}}\in[L-R,\;U+R].
|
||||
\]
|
||||
|
||||
Whenever that interval excludes zero, all descendants are irrelevant to occupancy there. The amplitude bound must come from the construction rules; estimating it from a few samples would not be a proof.
|
||||
|
||||
This approach spends a small, predictable amount on bounds supplied by each primitive. Your report’s failed generic interval experiment is good evidence against wrapping the existing entire evaluator in a more expensive arithmetic system.
|
||||
|
||||
**The most radical version would upload surface instructions instead of most vertices.**
|
||||
|
||||
An untouched surface patch can be described by its type, coefficients, transform, boundary identifiers and tessellation level. The GPU can draw a shared index pattern and calculate vertex positions from that description. This is ordinary rasterized geometry; it does not require raymarching the whole world each frame.
|
||||
|
||||
As an illustrative estimate:
|
||||
|
||||
- 20,000 patch descriptions × 128 bytes = **2.56 MB**.
|
||||
- A conventional mesh with 1.2 million vertices and 2.4 million triangles, using 24-byte vertices and 32-bit indices, is **57.6 MB**.
|
||||
|
||||
Both still produce geometry for the GPU to process. The saving is CPU generation, storage, transfer and object setup.
|
||||
|
||||
Edited regions would switch to explicit generated patches. Untouched regions could remain compact procedural instances indefinitely.
|
||||
|
||||
The costs are a custom rendering path, restricted patch families, careful transitions, and GPU arithmetic repeated in depth and shadow passes. Some patches should therefore be expanded once into GPU buffers and retained. This is a credible research direction, but I would **not require it to pass the first CPU experiment**.
|
||||
|
||||
**The rest of the pipeline needs to be designed around bulk work.**
|
||||
|
||||
Your recorded **639 mesh applies per second** is sufficient to make thousands of separate publications a problem even if generation becomes free. Your report also separates fast collision cooking from multi-second queue delays. Those are measured reasons to change the publication and support paths for this larger objective. [Recorded results](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/RESEARCH-SPEED-2026-09.md:35>).
|
||||
|
||||
I would use pooled geometry buffers and a small number of spatial render pages. Tiles or patches retain their own bounds and update ranges inside those pages. A small edit must update its ranges rather than rebuild a large page.
|
||||
|
||||
GPU culling and indirect drawing can reduce CPU submission work while retaining many independently culled pieces. Direct3D provides the necessary indirect command mechanism; integrating it into Unreal’s scene, material and shadow paths is real implementation work. [Microsoft, *Indirect Drawing*](https://learn.microsoft.com/en-us/windows/win32/direct3d12/indirect-drawing).
|
||||
|
||||
Only the player’s immediate support neighbourhood belongs on the initial collision deadline. Give it a priority path through generation, cooking and installation. Treat “physics state installed and a capsule sweep succeeds” as completion, rather than “cook task submitted.”
|
||||
|
||||
Boundary ownership must also be explicit. Adjacent patches use identical quantized border samples and defined transition strips. Edited replacement patches meet those shared borders. This work belongs in the meshing budget; overlapping skirts are not a general proof of a watertight result.
|
||||
|
||||
**Here is where I would allocate the second.**
|
||||
|
||||
These are **proposed acceptance budgets**, not benchmark results. The workload assumed is:
|
||||
|
||||
- A 64 m near region at roughly **25 cm structural resolution**, with finer appearance handled without a finer volume lattice.
|
||||
- All selected distant surfaces to the requested horizon, including stacked layers.
|
||||
- At most about 50,000 hierarchy records, 2–4 million nearby primitive-line tests, and **2.4 million output triangles**.
|
||||
- Six generation workers.
|
||||
- No seed-dependent geometry or field cache.
|
||||
|
||||
The structural resolution and triangle count are explicit concessions. This budget does not establish the same result for every geometric feature in the current 12.5 cm field.
|
||||
|
||||
| Stage | Work assumption | Principal data | Wall-time budget |
|
||||
|---|---|---:|---:|
|
||||
| Generate/select hierarchy and connections | Up to 50,000 records; order of 10–20 M simple operations | 3–8 MB | 30 ms |
|
||||
| Solve and compose nearby intervals | 2–4 M primitive-line tests; order of 100–300 M arithmetic/comparison operations, plus roots | Paged scratch, target ≤24 MB | 150 ms |
|
||||
| Produce sign masks and active cells | Up to roughly 1 M word groups; tens of millions of bit operations | 2–8 MB | 30 ms |
|
||||
| Emit all near/far meshes and transitions | 1.2 M vertices, 2.4 M triangles; order of 100–250 M geometry operations | 57.6 MB output | 220 ms |
|
||||
| Cook and install immediate support collision | Approximately the current small support neighbourhood | A few MB | 65 ms |
|
||||
| Bulk GPU upload and scene publication | At most roughly 64 page publications | Approximately 60 MB | 100 ms |
|
||||
| Final fence and first completed frame | Includes frame scheduling | — | 25 ms |
|
||||
| Allocation, imbalance and integration contingency | — | — | 280 ms |
|
||||
| **Total** | | | **900 ms** |
|
||||
|
||||
The budgets are added conservatively; I have not counted overlapping stages as an extra saving.
|
||||
|
||||
At an **assumed effective upload rate of 10 GB/s**, 60 MB takes approximately 6 ms of transfer time. That illustrates why publication, allocation and synchronization could matter more than raw bytes. It is not a measurement of your Unreal path.
|
||||
|
||||
Likewise, the operation counts do not automatically imply the listed milliseconds. Roots, unpredictable branches and cache misses differ enormously in cost. The experiment must measure them.
|
||||
|
||||
The largest unknown is the **required output complexity**. If a representative complete view needs 20 million triangles under the chosen surface error, this particular budget fails. The remedy would have to be better surface construction, instancing or approximation—not a claim that packed occupancy removed the output cost.
|
||||
|
||||
“Cold” here means the application is running, shaders are available, and the seed/layout/edit data have arrived, but no world-specific result exists. Starting Unreal, compiling shaders or downloading an unbounded edit history requires a separate budget.
|
||||
|
||||
**The 5800X3D makes a compact implementation particularly attractive.**
|
||||
|
||||
AMD specifies eight cores, sixteen threads, 96 MB of L3, AVX2 and dual-channel DDR4-3200 for this processor. Its theoretical dual-channel memory rate is 51.2 GB/s; actual application bandwidth must be measured. [AMD specifications](https://www.amd.com/en/products/processors/desktops/ryzen/5000-series/amd-ryzen-7-5800x3d.html).
|
||||
|
||||
I would organize the implementation as follows:
|
||||
|
||||
- **Keep repeatedly accessed data around 40–60 MB.** The 96 MB is shared with the rest of the application. Stream finished mesh output rather than keeping every intermediate alive.
|
||||
- **Process local pages with worker-owned scratch.** A 64³ sign block is only 32 KiB before borders. Finish its interval, boundary and mesh work while the relevant data are close.
|
||||
- **Use arrays of primitive coefficients grouped by type.** AVX2 should process similar intersections together, without virtual dispatch or scattered feature lookups inside the loop.
|
||||
- **Use AVX2 for batches, ordinary words for bitsets.** One vector handles four 64-bit masks. This is useful after the representation has removed most work; it is not a claimed 256-fold generator acceleration.
|
||||
- **Exploit forward differences.** Along an integer scanline, \((x+1)^2-x^2=2x+1\). Quadratic terms can advance with additions; hash and coefficient setup happen once per feature or row.
|
||||
- **Use Morton order between pages and simple linear order inside them.** This preserves spatial locality without expensive addressing in every inner iteration.
|
||||
- **Measure six versus eight physical workers before using extra SMT workers.** Leave capacity for the game thread, renderer and collision path. Sixteen hardware threads do not supply sixteen independent cores.
|
||||
|
||||
Avoid per-column allocation, shared append locks and global vertex hash tables. Count locally, assign deterministic output ranges, and write disjoint buffers.
|
||||
|
||||
**Multiplayer agreement would come from a small, explicit numerical contract.**
|
||||
|
||||
Use integer world keys, a versioned hash, fixed parameter quantization and stable feature identifiers. Define:
|
||||
|
||||
- Rounding and sign-at-zero rules.
|
||||
- Endpoint inclusion and coincident-crossing rules.
|
||||
- Boolean operation order and material priority.
|
||||
- Shared edge ownership and vertex quantization.
|
||||
- Arithmetic ranges, widening and overflow behaviour.
|
||||
|
||||
For quadrics, an integer square root or an exact predicate can determine the canonical crossing bracket. A floating-point estimate may accelerate the search only if exact arithmetic verifies and corrects the result.
|
||||
|
||||
GPU floating-point surface shading can vary without changing gameplay. If GPU-generated geometry itself makes authoritative decisions, its integer decisions must match the CPU reference. Merely changing the language’s numeric type does not establish determinism.
|
||||
|
||||
Clients independently generate the same descriptions and process the same ordered edits. Physics simulation need not become deterministic lockstep merely because world geometry is deterministic.
|
||||
|
||||
For a 10 m spherical crater at 25 cm spacing, its projected footprint covers approximately
|
||||
|
||||
\[
|
||||
\pi(10/0.25)^2 \approx 5{,}000
|
||||
\]
|
||||
|
||||
columns per axis. That is roughly 15,000 affected lines across three directions, rather than evaluating the whole 20 m bounding cube.
|
||||
|
||||
That makes immediate editing much more promising. It does **not** prove same-frame publication. The replacement surfaces must still be generated and switched together across every affected page and LOD. CPU support queries must observe the new edit immediately, even while replacement collision bodies are pending.
|
||||
|
||||
**What this design gives up is mathematical generality, rather than three-dimensionality.**
|
||||
|
||||
Overhangs, stacked chambers, floating masses, shafts and craters remain possible. The restrictions are on how they are composed:
|
||||
|
||||
| Restriction | Performance obtained |
|
||||
|---|---|
|
||||
| A bounded primitive and junction family | Predictable surface emission and intersection cost |
|
||||
| Limited overlap and crossings per local region | Short interval lists and cheap Boolean composition |
|
||||
| Coarse structure constrains later refinement | Far geometry exists without generating descendants |
|
||||
| Fine appearance does not all become collision topology | Surface work avoids growing with microscopic detail |
|
||||
| Edited regions become local exceptions | The untouched majority keeps its compact representation |
|
||||
| Finite supported scene and edit complexity | A meaningful loading-time target |
|
||||
|
||||
The long-term risk is that edits accumulate until most regions become expensive exceptions. Local edit histories therefore need deterministic pruning or compaction into a bounded replacement representation. An unlimited edit log cannot have a fixed cold-load guarantee.
|
||||
|
||||
A full-view raymarcher would remove mesh construction but move substantial work into every stereo frame. Global FFTs and tensor decompositions can be excellent for suitable restricted functions, but do not automatically survive arbitrary local cuts cheaply. I would pursue those after the boundary-producing construction has been tested.
|
||||
|
||||
**The first experiment should test the interval compiler and count the output—not attempt a new renderer.**
|
||||
|
||||
A one-day standalone C++ spike could answer the central question:
|
||||
|
||||
1. **Build a representative geometric corpus.** Use room and tunnel records resembling your actual parameter ranges, plus pillars, shelves, floating solids, thin strata, rotated shapes and intersecting craters. Include crowded junctions. A sparse collection of spheres is insufficient.
|
||||
|
||||
2. **Implement two versions of the same restricted world.** One evaluates occupancy point by point as a reference. The other generates intervals, composes them, fills sign words and extracts boundaries. They must describe the same new geometry; matching every detail of the old noise field is not the experiment.
|
||||
|
||||
3. **Produce real mesh buffers.** Include boundary positions, indices, borders and deterministic ownership. A fast occupancy-only result can conceal an expensive reconstruction stage.
|
||||
|
||||
4. **Exercise the far-world rule separately.** Generate the complete selected hierarchy and its coarse surface buffers without constructing fine descendants. Record nodes, patches, vertices and bytes. If implementing the full hierarchy exceeds the day, this part remains explicitly unproved.
|
||||
|
||||
5. **Measure fresh-process runs on the 5800X3D.** Include allocation and first-touch costs. Test one, six and eight workers; report median, p95 and worst across seeds. Validate outside the timed region against the pointwise reference, including negative coordinates, page borders, tangencies, thin features and coincident edits.
|
||||
|
||||
I would use these initial decision gates:
|
||||
|
||||
| Result | Decision |
|
||||
|---|---|
|
||||
| All CPU geometry fits approximately **430 ms**, with output around **60 MB** | Proceed to the Unreal publication and collision test |
|
||||
| Interval work fits its **150 ms** allocation, but meshing exceeds the budget | Investigate direct patches, reconstruction and output size |
|
||||
| Only the simple cases pass; representative junctions produce huge interval lists | Reject or restrict that composition family |
|
||||
| Far geometry requires fine descendants or dense strata sampling | The proposed hierarchy has not removed the principal cost |
|
||||
| Any missing thin feature or inconsistent shared boundary | Correctness fails regardless of speed |
|
||||
|
||||
The standalone test can establish whether the computational core deserves investment. The final proof is an Unreal run timed through **the first complete visible frame and successful player-support collision**, followed by a separate check that steady rendering meets the VR frame budget.
|
||||
|
||||
I would spend the first day on **analytic intervals plus direct far surfaces**. They attack both sources of unnecessary work: repeatedly discovering nearby boundaries, and reconstructing distant surfaces from a volume that the player will never interact with.
|
||||
@@ -0,0 +1,220 @@
|
||||
# VoxelForge speed research — 29 September 2026
|
||||
|
||||
## Consolidated decision
|
||||
|
||||
**Build the spatial/evaluation redesign first and keep the current precise float procedural math.** Compile the seed, layout, composer and operator stack once per region into a stable local program; bin rooms, tunnel segments and passages into 8³-cell bricks; compute cheap *final-field* solid/air certificates; share lattice samples across tiles and LODs; contour uncertain bricks. Use a versioned, explicitly rounded **16-bit lattice density** as the common mesh/edit/query cache. Keep the procedural float evaluator as the cache producer and on-demand *lattice-sample* fallback; player collision queries interpolate the same quantized lattice, including newly generated cells, so visual and collision signs cannot diverge through an off-grid float query. The quantization scale, zero tie and interpolation rule belong to the new world ABI. This changes the world version without requiring a full fixed-point rewrite of Perlin and every content operator. [Build flags](Source/VoxelForge/VoxelForge.Build.cs) already pin `FPSemanticsMode.Precise`; [noise](Source/VoxelForge/Public/VoxelNoise.h) has table-free Perlin and a scalar/SSE4.1 path written op for op; [geometry math](Source/VoxelForge/Public/VoxelTypes.h) uses `VoxelMath::DetSinCos`. This supports the current same-build CPU contract, although full-world hashes on the actual supported CPU/compiler combinations remain a release gate. MSVC documents both the ordering guarantees and limits of `/fp:precise`; it is not a GPU-vendor parity mechanism. [MSVC floating-point semantics](https://learn.microsoft.com/en-us/cpp/build/reference/fp-specify-floating-point-behavior?view=msvc-170), [MSVC cross-CPU cautions](https://learn.microsoft.com/en-us/cpp/porting/floating-point-migration-issues?view=msvc-170).
|
||||
|
||||
**For the player, provisionally choose field-aware movement against the versioned lattice, with cooked Chaos bodies as a compatible background path.** This is the route that can use the new edit epoch immediately when a dig removes support; a delayed cook cannot be authoritative in that frame. Before committing to Mover, instrument the existing cook pipeline and try reserving generation capacity. If a small set of player-support tiles cooks quickly *and* a CMC edit-time field override passes the same-frame floor/velocity test, retain CMC plus a prioritized-cook/field-overlay hybrid. A fast cook by itself does not solve an underfoot edit. RMC's [manager](../RealtimeMeshComponent/Source/RealtimeMeshComponent/Private/RealtimeMeshManaged.cpp) defers collision to end of frame, its [future helper](../RealtimeMeshComponent/Source/RealtimeMeshComponent/Public/Core/RealtimeMeshFuture.h) schedules work on `GThreadPool`, and its [data path](../RealtimeMeshComponent/Source/RealtimeMeshComponent/Private/Data/RealtimeMeshData.cpp) returns to the game thread to apply BodySetup/physics state; the observed 0.4–1.4 s is **submit-to-ready, not measured Chaos compute**. UE task priority does not preempt already running generation, and RMC's pool is a distinct queue whose threads still compete for CPU. [Epic UE Tasks](https://dev.epicgames.com/documentation/unreal-engine/tasks-systems-in-unreal-engine), [Epic BodySetup readiness](https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/UBodySetup/CreatePhysicsMeshesAsync).
|
||||
|
||||
**For rendering, measure the current RMC drain before grouping.** `MaxMeshAppliesPerFrame=4` and `MaxMeshApplyMilliseconds=2` are settings, not a hardware limit. The source comment's “~0.1 ms per 32³ apply” is an estimate, not a captured sustained upload rate; no surviving `[VoxelForgeApplyFrame]`/render-thread trace in the repo establishes one. With ~341 visible nonuniform tiles and a 0.35 s generation budget, the remaining 0.65 s requires **≥525 visible applies/s**, or **9/frame at 60 Hz** and **6/frame at 90 Hz** if results are ready together. At 4/frame the caps are 240/s and 360/s, respectively. If all 512 tiles are nonuniform, the 60 Hz requirement rises to **14/frame**. Raise the cap in an isolated saturation test and measure game-thread drain, RMC render enqueue, RHI completion and first visible frame. Group 2³ tiles into at most 64 spatial pages, retaining independently updateable sections, **only if** per-component or render-thread overhead misses that rate. The archived 128³ export emitted 7.435 MB of logical mesh output for 64 tiles, an average 116 kB/tile; extrapolating that mix to 341 visible tiles is ~39.6 MB of payload, not a measured GPU transfer or component cost. [Local attribution](Docs/archive/PERF-ATTRIBUTION.md).
|
||||
|
||||
**A large spell still needs a single visual transaction.** At radius 10 m the round-two probe found 714 touched 8³ bricks and up to 64 current tiles; its cached-field composition was 0.861–0.914 ms, excluding upload. Keep a GPU-resident copy of the visible quantized lattice and apply an integer edit command to the whole affected region in one render dependency, with a complete-region mesh or bounded raymarch fallback. Match CPU and GPU quantized sign, interpolation and zero ties; publish all touched tiles/LODs at one edit sequence. The GPU need only perform integer **edit composition and mesh decisions**. Replacing the entire procedural float generator with integer is conditional on a later full-GPU base-generation win. This retains cross-machine world authority on the CPU and avoids treating the round-two integer AVX2 probe as a production speedup.
|
||||
|
||||
### Separate the two proposed changes
|
||||
|
||||
| Change | Evidence and expected contribution | Decision |
|
||||
|---|---|---|
|
||||
| **(a) Region-local evaluation and sparse sampling** | At the supplied ~2 µs/sample, shared lattice alone cuts 21.952 M requests to 17.374 M and an ideal six-worker clock from **7.32 s to 5.79 s**: only **1.26×**. The proposed **0.35 s** worker budget requires a further **16.5× effective reduction** in cost or evaluated points after sharing: `(17.374 M × 2 µs)/(6 × 0.35 s)`. This is the required result, **not a measured gain**. The scalar synthetic probe's ~170 ns of aggregate worker CPU/sample (six workers × ~0.47 s / 16.8 M points) shows hardware headroom but uses a different, much cheaper graph. Brick certificates, candidate binning, removal of per-point routing and local compiled ops are the mechanisms to test; no number from another workload is assigned to them. | **Necessary first experiment.** Keep float while measuring it, so gains can be attributed. |
|
||||
| **(b) Full integer/fixed-point procedural generator** | The round-two explicit AVX2 kernel beat its *own integer scalar reference* **4.25–5.86×**; that comparison says nothing reliable about integer versus the plugin's optimized float/SSE noise or its feature-search cost. Merely changing numeric type leaves the ~30.7 counted candidate visits/sample and cache routing intact. There is **no measured production speed credit**, so assign **zero guaranteed speedup** toward the 16.5× goal. New world version permission removes compatibility cost, but not implementation or proof risk. | **Defer full conversion.** Use deterministic quantization at the lattice boundary and integer edit/mesh decisions on GPU. Convert base generation only if an exact CPU/GPU float-versus-integer production ablation proves a faster end-to-end path. |
|
||||
|
||||
The ~2 µs sample is an end-to-end `GetDensityAt` cost, not a Perlin operation. The available attribution closes **where** time goes but does **not** supply additive CPU shares for each nested operation. In the archived 128³ TunnelNetwork export, 590.923/606.128 ms (**97.49%**) was shared-grid fill; the following cell scan/mesh/merge was ~15.2 ms. Dividing 590.923 ms by its 2,248,091 unique samples yields only **263 ns/sample in that different workload/window**. It therefore cannot be substituted for the owner's current ~2 µs/sample or used as an additive timing breakdown of it. The export generated these *work counters*: **11.10 M room candidates / 1.73 M evaluated**, **28.95 M tunnel candidates / 3.46 M evaluated**, and **28.95 M tunnel-core candidates / 1.88 M evaluated**. That is about **4.94 + 12.88 + 12.88 = 30.7 candidate visits per sample** across the three reported counters, with about **3.15 actual evaluations/sample**. Candidate passes may revisit the same feature, and the counters are **not additive time buckets**. Binning local features once per brick attacks this work directly. [Local attribution](Docs/archive/PERF-ATTRIBUTION.md).
|
||||
|
||||
| Per-sample work visible in code | What the redesign removes; what it cannot claim |
|
||||
|---|---|
|
||||
| `GetDensityAt` recomputes chunk coordinates and checks manager/layout/lifetime, routing, tunnel cache, tile-window policy and diff version. The measured tunnel cache had **2,248,043 hits and 48 misses**: nearly every point pays lookup/key predicates despite few builds. `GetDensityWithParams` performs further box/fingerprint/terrain-op/tile-policy guards. | A prepared immutable region program moves these decisions to region/brick admission. This can remove repeated checks and indirection; their individual nanoseconds were not measured. These are multiple **key comparisons**, not a dozen expensive cache misses. |
|
||||
| Rooms, tunnel segments, passage checks and warped search boxes are queried inside field evaluation; the candidate counts above show repeated local traversal. | Spatially bin features with conservative warp/blend reach, then evaluate only the local list. A false-negative bin is a geometry bug; exact sign tests across stratum and passage boundaries are mandatory. |
|
||||
| The generic composer/operator path can visit roughly twenty ops, including locally inert ones. **But the archived primary TunnelNetwork run uses the already fused evaluator**, which bypasses `EvalMC`'s per-op dispatch. | Compiling away generic op visits helps custom/composer and other nonfused recipes; it **cannot** be credited for the primary fused TunnelNetwork timing. That route still contains room/tunnel candidate work, routing and the common tail. |
|
||||
| `GetDensityAt` follows the core with disturbance/structural/passage/landing-floor repairs, XY seal and diff application. The attribution lists structural-tail, density-post and passage-post *sampled nested* work, not wall shares. | A region program can pre-resolve which post passes can affect a brick and fuse their ordered math; the route/floor invariant must survive. Do not subtract the nested timings from 590.923 ms or assume the tail disappears entirely. |
|
||||
| The code enters per-sample profiling scopes. The owner's latest stack sample attributes **4.9% to the profiling leaf**; the archived same-argument profiled/unprofiled control differed by only **0.63%**. | Disable profiling for acceptance timing and retain a diagnostic build. The 4.9% sample share is not a demonstrated 4.9% clean-build gain; sampling and instrumentation mode differ. |
|
||||
|
||||
### Ordered build plan for the orchestrator and coding agent
|
||||
|
||||
The steps below describe **future plugin experiments**. This research pass makes no `Source/` change and does not run Unreal. Use the same machine, fixed seeds/layouts, cold world caches, and 20 spawn/teleport trials per UE gate; report the **maximum** as well as median because 20 trials are too few for a meaningful p99. Preserve gameplay while replacing one pipeline stage at a time.
|
||||
|
||||
1. **Day-one falsification: stage the existing collision and upload clocks.** Add timestamps for collision request, RMC end-of-frame dequeue, `GThreadPool` worker start, geometry extraction end, `CookComplexMesh` start/end, game-thread `ApplyCollisionUpdate` start/end, physics-state ready and first successful capsule sweep. Keep existing generation, mesh and gameplay behavior while profiling. Test one ~4,000-triangle tile and the pawn's current 3³=27-tile support neighborhood, idle at the current 14 generation tasks, and with the generation cap reduced to 12. If the cook's queue wait dominates, test a dedicated priority cook dispatched directly after player-support mesh extraction; compare its core cook and install times with RMC's route. In parallel, prequeue a 341-tile render corpus and run caps 4/8/16 with the same 2 ms drain guard; record visible applies/s, game/render/RHI clocks and first-visible frame. **Pass the cook shortcut only if** core cook p95 ≤10 ms, the 27-tile player support is ready ≤150 ms after meshes exist under load, and an edit-time field override can switch floor/support within the first simulated/rendered frame with zero invented velocity in 100 repeated underfoot digs. If any edit test fails, implement field-aware Mover as the player authority; if the shortcut passes all tests, use prioritized cooks for steady terrain plus field queries while an edit's cook is pending. **Pass the ungrouped upload path only if** it sustains ≥525 visible applies/s at 60 Hz with game-thread drain ≤2 ms/frame and no render/RHI backlog. These are decision thresholds, not existing measurements.
|
||||
**Step 1 results (2026-09-30, commit `41b001b`):** core cook **1.50 ms p50 / 2.28 ms p95** (passes); through RMC's collision route the same support bodies were ready **8.5–10.8 s** after their meshes existed (fails): the wait is RMC's path, not Chaos, so player-support collision must bypass it. CMC with a field override: **883 false-support frames in 100 underfoot digs** (fails): step 4 is **field-aware Mover**. Uploads: cap 16 = **639 applies/s**, drain p95 2.22 ms (misses 2 ms by 0.22 ms), no render/RHI backlog, so tune the drain and **do not group yet**. The "8× per-sample gap" was a denominator artefact: 263 ns was export *wall* time with 15 workers on a shared lattice (≈3.4 µs CPU per sample); the real per-sample CPU cost is ~2–3.4 µs in both paths, so the 12–16× reduction target stands.
|
||||
2. **Exact-float region-evaluator experiment.** Use an isolated, editor-only explore-commandlet path inside the plugin because the live composer, strate manager, generator and SDF cache depend on Unreal types. Keep gameplay default-off and compare at the same lattice points and operator order. The reference is the chosen **new-world float definition**, not old-season geometry. **Pass:** zero 16-bit quantized-value disagreements and zero route/capsule regression across at least five seeds; ≤**600 ms** for the full 259³ cold ring on six workers before certificates. Record candidate visits, cache checks and worker CPU. Failure means local compilation did not remove enough work; investigate before rewriting numeric math.
|
||||
|
||||
**Step 2 verdict (2026-09-30, attempts 2a/2b/2c, commits `41a9265`, `fd9f809`, `43c11cc`):** all exact (zero Q16/sign disagreements over five seeds, reach-shrink control caught, same hash on 1 and 6 workers), none fast. Best (2c): setup once per 32³ chunk, **1.595 µs/sample vs 1.678 live, 7.9 s vs 7.1 s wall**. Binning cut candidate *visits* 21→16 per sample, but room/tunnel/tunnel-core *evaluations* did not fall (48 M / 5 M / 38 M for 17.4 M points): the cost is the geometry evaluation itself, not the search; noise is ~2.6%. **600 ms is out of reach while every lattice point must reproduce today's density exactly.** That exactness bar was a comparison tool, not a requirement: the owner accepts a new world version, so the next step may *redefine* the evaluator (for example coarser room/tunnel geometry with interpolation, or skipping bricks proven uniform), judged on shape tolerance, walkability and cross-machine determinism instead of equality with the old path.
|
||||
**Step 2 result (2026-09-30, attempt C; pass bar not met, full compiler untested):** five-seed Q16 comparison and two-run raw-float determinism pass. The deliberate room-bin reach reduction to 0.5 is caught (18,107 Q16 differences, 735 sign differences), with no saturated samples. On the 17,373,979-point cold ring, the live `GetDensityAt` p50 is **6.776 s**, **1.679 µs worker CPU/sample**, and **29.164 worker CPU seconds**; the editor-only 8³ SDF-bin variant is **19.543 s**, **2.879 µs worker CPU/sample**, and **50.023 worker CPU seconds**. Both measured variants observed six workers. Sampled visits fall from **21.19 to 16.38/sample** (about 23%), but wall time is 2.88× slower and the 600 ms target is missed. The variant still calls `GetDensityAt` for every lattice point; its bins cover rooms, room-floor joins and whole tunnel chains, while segment loops, route/cache decisions, passages, landing posts, disturbances and player edits remain in the canonical per-sample path. This is a **candidate-bin ablation, not the planned compiled region program**; it falsifies per-sample 3D bin lookup as a sufficient optimization but does not falsify hoisting region decisions or compiling the full ordered program. Step 2 remains open until that full program is measured; do not start step 3's certificate work against this slower baseline. Full report: `Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/TestHarness/step2_region_eval_20260930_c_region-eval/region_eval.json`.
|
||||
|
||||
**Launch record:** attempt A's guarded UBT invocation logged `Result: Failed` (JSON writer and `ParallelFor` overload compile errors); it exited before launching a commandlet. Attempt B built successfully but `-regioneval=1` was ignored by Unreal argument parsing, so the ordinary explore commandlet exited successfully without the expected JSON. Attempt C used a bare `-regioneval`, rebuilt the editor module, and completed the intended experiment. No attempt timed out, crossed the 8 GiB private-memory limit, or produced a crash report; C peaked at 7,137,738,752 private bytes. The runner was corrected to detect UBT's logged failure even when its process exit code is zero.
|
||||
3. **Add certificates, shared lattice and contouring.** Prove final-sign bounds after all writers and edits; classify 8³ bricks, evaluate uncertain ones, share global corners, and mesh them with today's marching-cubes table. Keep the old tile route as a versioned reference while the new route is validated. **Pass:** zero false-uniform bricks and zero sign mismatch at all 17,373,979 ring lattice sites for the test corpus, cold worker generation ≤**350 ms**, and max render-visible/full ring ≤**900 ms** in the later UE gate. If certification consumes more time than it saves, fall back to evaluating uncertain regions without that proof; do not repeat the archived 0.588→2.057 s interval-control mistake.
|
||||
3b. **AVX2 check against the new norm (owner's request, 2026-09-29).** Once step 3's brick evaluator is the baseline, try an explicit 8-lane AVX2 version of its hot loop (the plugin's current noise path is 4-lane SSE4.1). **The owner does not require AVX2 to match the scalar path bit for bit** (clarified 2026-09-29): only the shipped product must be deterministic across players' machines. If adopted, the AVX2 path *is* the canonical world definition (FMA allowed, as long as it is always used), and every machine must run that same path: require AVX2 at startup rather than falling back to a scalar path that would produce a different world. **Keep it only if** it is faster on the step-3 corpus and the AVX2 path gives the same lattice hash on two runs and on two different AVX2 CPU models; otherwise record the number and drop it. The round-two integer AVX2 probe (4.25–5.86× over its own scalar) is the reference for what explicit vectorisation can buy.
|
||||
4. **Choose and implement player collision.** Use step 1 to select prioritized Chaos+edit overlay or field-aware Mover; keep ordinary collision with nonterrain actors and keep NPC/projectile field-query backup. Version floor/contact state by edit sequence. **Pass:** at every teleport the player can perform an actual blocked capsule movement on the intended terrain ≤**900 ms** after the request, 100 underfoot digs add **0** spurious velocity impulses, and 100 excavations with no cave below have **0** frames of false support or false air. The full visible ring must also meet the separate ≤1,000 ms hard deadline. A successful cook timing without the edit tests is insufficient.
|
||||
5. **Decide render batching from measured throughput.** Keep per-tile RMC if it sustains ≥525 visible applies/s and the first-visible gate ≤900 ms. Otherwise group 2³ tiles per page while retaining tile-addressable sections and the same materials/LOD transition topology; rerun the same corpus and require ≥**99 page applies/s** for at most 64 pages in 0.65 s, ≤2 ms/frame game-thread drain and no render-thread tail past the 1 s deadline. If grouping causes whole-page reupload on small edits or still misses, use a dedicated batched render proxy. Compare first-visible time against the same ≤900 ms gate.
|
||||
6. **Version the edit and GPU visual protocol.** Quantize float-produced lattice density with specified rounding/saturation; use fixed integer edit composition, stable per-cell triangle counts/prefix sums, global edge ownership and one edit sequence across touched pages/LODs. Keep CPU procedural generation, composer, route graph and ordered edit replication as the authority. Test AMD and NVIDIA output hashes against CPU reference; on a single machine, two full cold runs must yield the **same field/sign/mesh hash**. **Pass:** a radius-10 m crater (up to 714 bricks/64 tiles) appears throughout its region in the **first submitted render frame containing the edit**, no gap/ghost/ripple at borders or LOD seams, CPU/GPU sign disagreement **0**, and added GPU cost ≤**2 ms** within an 11.1 ms 90 Hz VR frame in 20 varied positions. If direct GPU remeshing exceeds that, test the complete-region raymarch overlay with the same visual and frame gates; do not publish a partial mesh.
|
||||
7. **Long-range and multiplayer gate.** Provide LOD1 first at 16 cells per tile, refine later, reuse coincident lattice points and keep full 3D geometry for caves/overhangs/islands. Replay ordered edit transactions, including crater commands, on two supported CPU models and compare world-versioned hashes. **Pass:** first LOD1 preview ≤**3 s** after cold teleport, an 8 m/s five-minute run with **0** missing player support or visible near-ring gaps, equal hashes for two runs and both CPU models, and a successful late join after replay/checkpoint. If float parity fails on a supported CPU/compiler pair, lock an exact scalar FP path or then promote a full integer procedural ABI.
|
||||
|
||||
**Still unproved:** production float region-program throughput, brick-proof efficiency on the full composer, the actual RMC sustained apply rate, the Chaos queue-versus-compute split, whether CMC can support an edit-time field override, field-aware Mover query cost, and same-frame GPU/VR crater rendering. Each has an explicit gate above. The standalone integer result proves only that its synthetic 3D kernel can run quickly and match its own scalar reference on this Ryzen. Neither a source paper's scene timing nor a comment's estimated per-tile apply time closes these gaps.
|
||||
|
||||
## Earlier evidence and alternatives
|
||||
|
||||
**The consolidated choice above supersedes the round-two integer-first and unconditional grouping suggestions.** The useful core remains a spatially pruned 8³-brick evaluator, shared lattice, conservative certificates and transaction-wide crater visual. Keep the 25 cm lattice, volumetric operators, walkable route graph and seed/layout/edit replication. Recent work on pruning SDF construction trees directly supports region-local programs, but its speedups on other scenes are not VoxelForge measurements. [Lipschitz Pruning, Eurographics 2025](https://wbrbr.org/publications/LipschitzPruning/documents/LipschitzPruning_submitted_to_EG25.pdf), [Accelerating Signed Distance Functions, 2025](https://perso.liris.cnrs.fr/eric.galin/Articles/2025-lod.pdf), [OpenVDB paper](https://museth.org/Ken/Publications_files/Museth_TOG13.pdf).
|
||||
|
||||
**The one-second target is plausible but not proved.** The round-two six-worker AVX2 probe processed a *synthetic* 256³ composition in **80.3–107.5 ms** across three three-trial runs with exact scalar parity; 17.374 M unique ring points at that measured throughput extrapolate to **83.2–111.3 ms**, leaving roughly 0.89 s for classification, mesh extraction, upload and visibility. This is an arithmetic extrapolation across different data and cache patterns, not a measured UE ring. The scalar probe took **430.3–496.2 ms**, a **4.25–5.86×** paired speedup from explicit AVX2. The recommendation is conditional on a production-stack parity/timing experiment and a UE upload/VR edit spike. The arithmetic is decisive about what must change:
|
||||
|
||||
| Quantity | Arithmetic or measurement | Consequence |
|
||||
|---|---:|---|
|
||||
| Current tile requests | 512 × 35³ = **21,952,000** samples | At the supplied ~2 µs/sample this is 43.9 CPU-seconds before collision/upload. |
|
||||
| Shared lattice for 64 m cube plus halo | 259³ = **17,373,979** unique points | Sharing alone removes 20.9% of requests, nowhere near enough. |
|
||||
| Required density rate if generation gets 0.5 s on six cores | 17.374 M / 0.5 / 6 = **5.79 M samples/core/s**, or **173 ns/sample** | Current ~2 µs/sample needs about **11.6×** improvement even after sharing if every point is evaluated. This ignores meshing and upload. |
|
||||
| Current render apply cap | ~341 nonuniform tiles if one third are uniform; 4 applies/frame = 85.3 frames, **1.42 s at 60 Hz** if all were queued together | This is the **default configured cap**, not measured sustained throughput. Raise it and measure game/render/RHI time first; group only if the actual rate misses the deadline. |
|
||||
| Current collision | Owner's 0.4–1.4 s submit-to-ready **per tile** | This interval includes end-of-frame deferral, pool queueing, extraction, actual Chaos cook and game-thread installation; **the cook's own CPU duration is unknown**. Reserve capacity and instrument the stages before choosing field-only versus prioritized-cook hybrid. [Epic BodySetup API](https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/UBodySetup/CreatePhysicsMeshesAsync), [RMC source](https://github.com/TriAxis-Games/RealtimeMeshComponent). |
|
||||
| Running at 8 m/s ([local design brief](Docs/archive/DESIGN-SOL-2026-09-15.md)) | One 8 m tile crossing/s brings one 8×8 face = **64 new tiles/s**; 64 × 35³ = **2.744 M requests/s** | At current ~2 µs/request, that is ~5.49 CPU-seconds/s, or ~0.915 s on six ideal workers before upload/collision. A shared 32-cell slab is 259² × 32 = 2.147 M new points, still ~0.716 s on six ideal workers. Field collision on demand is also the safety net for unexpected movement or stalls. |
|
||||
|
||||
**Measured standalone feasibility probe on this machine:** A synthetic 256³ 3D field with 48 integer sphere features and three octaves of integer lattice noise took **471–481 ms** for 16,777,216 samples using six threads. Conservatively classifying 32,768 8³ bricks took **15.46 ms**; evaluating only the 7,563 uncertain bricks then took **106–130 ms**, with **zero sign mismatches** against the full evaluation over all 16,777,216 points. An eight-thread repeat took 349–367 ms full or 86–110 ms classified plus 15.59 ms classification. This is a *kernel probe*, not VoxelForge or a measured 512-tile load. Its primitive list and noise are far simpler than the current composer stacks. It shows that the required rate is physically credible on this CPU when work is converted to bounded integer kernels; it does **not** establish that the full game, render upload, or Chaos field integration finishes in one second. Benchmark details and limitations are below.
|
||||
|
||||
No evidence here proves that UE can present a cold 64 m cube in under 1 s or a radius-10 m crater in the same VR frame. The ordered gates above decide those questions. If they fail, change the render/collision implementation or explicitly revise the target; do not silently infer success from a synthetic density benchmark.
|
||||
|
||||
## Local evidence and what it means
|
||||
|
||||
The code currently requests 35³ values for each 32-cell marching-cubes tile, and `GetDensityAt` builds/evaluates the stack in `VoxelGenerator.cpp`; `VoxelMarchingCubesMesher.cpp` spends most worker time filling the grid. The existing attribution measured **590.923 ms of 606.128 ms** (97.49%) for a shared-grid fill in a 128³ TunnelNetwork export, with only ~15.2 ms for later meshing/merge. It also measured an interval-classifier route that grew from **0.588 s to 2.057 s** instead of helping. Thus “add generic interval recursion” is already a failed local control; the new plan must compile *cheap, conservative bounds and feature lists per brick*, and fall back to evaluation if a bound is uncertain. [Local attribution](Docs/archive/PERF-ATTRIBUTION.md), [failed early-out control](Docs/archive/CODEX-TASK-2-PERF-REPORT.md).
|
||||
|
||||
The owner's **0.4–1.4 s** collision figure is submit-to-ready in the current observation window. The archived 12 September headless run reported a **0.052945 s p95 cook stage** in its different workload and instrumentation window. Neither number is a per-triangle CPU cost, and they should not be pooled or multiplied into a 512-tile total. The planned Unreal spike must capture the new ring's cold maximum, not reuse either statistic. [Archived local timing](Docs/archive/PERF-ATTRIBUTION.md).
|
||||
|
||||
The runtime already prioritizes edited and collision-critical work, but it applies at most four visible meshes per frame by default (`VoxelSettings.h:65`) and `ApplyModification` synchronously remeshes only the center tile while neighbors are asynchronous (`VoxelWorld.cpp:10369`). Underfoot carving can disable the old support component until recook (`VoxelWorld.cpp:10290`), directly explaining a transient disappearance. RMC can preserve old cooked collision while a replacement cooks (`VoxelWorld.cpp:9858`), which explains ghost support if the removed surface is still in that body. The default pawn collision box is radius one tile, **3³ = 27 tiles**; the requested 512-tile collidable ring is a much stronger target than this implementation currently attempts (`VoxelSettings.h:88`, `VoxelWorld.cpp:8168`). These are code observations, not new timing measurements.
|
||||
|
||||
### Standalone measurement, reproducible
|
||||
|
||||
Code: [`Saved/Research/brick_kernel.cpp`](Saved/Research/brick_kernel.cpp). Built only this standalone program with installed MSVC; Unreal was not built or launched. From `Saved\Research` in PowerShell:
|
||||
|
||||
```powershell
|
||||
cmd /c '"E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul && cl /nologo /O2 /std:c++20 /EHsc /arch:AVX2 brick_kernel.cpp /Fe:brick_kernel.exe'
|
||||
./brick_kernel.exe
|
||||
cmd /c '"E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul && cl /nologo /O2 /std:c++20 /EHsc /arch:AVX2 /DVF_WORKERS=6 brick_kernel.cpp /Fe:brick_kernel_6.exe'
|
||||
./brick_kernel_6.exe
|
||||
```
|
||||
|
||||
The `vcvars64.bat` initialization printed a harmless missing-`vswhere.exe` notice; `cl` compiled and both programs exited successfully. Values are wall milliseconds from `std::chrono::steady_clock`, three repeats, Windows on the stated machine:
|
||||
|
||||
| Stage | Eight workers (ms) | Six workers (ms) | Work |
|
||||
|---|---|---|---|
|
||||
| Full evaluation | 362.538, 367.161, 349.001 | 479.017, 470.581, 481.011 | 16,777,216 grid points, each checks 48 spheres and computes 3 octave integer value noise. |
|
||||
| Brick classification | 15.590 | 15.464 | 32,768 bricks: 24,778 solid, 427 air, 7,563 uncertain. Conservative sphere/AABB extrema plus a proven ±224 noise envelope. |
|
||||
| Classified evaluation | 109.641, 86.959, 86.439 | 106.184, 129.769, 108.978 | Full evaluation only for uncertain bricks; classified bricks retain their correct sign. |
|
||||
| Verification | 0 sign mismatches / 16,777,216 | 0 sign mismatches / 16,777,216 | The sign of every classified point was checked against the full run. Density *magnitudes* in certified uniform bricks were intentionally not preserved. |
|
||||
|
||||
The probe lacks passages, smooth Boolean operators, strata boundaries, edits, meshing, CPU cache competition with UE, upload and collision. Its 76.9% uniform-brick rate is **synthetic** and must never be substituted for the game's observed one-third uniform *tile* rate. The timing is evidence for the kernel's potential only. Full production sign/mesh/collision parity across seeds, strates, edits and CPU models is a release gate. The probe uses scalar integer code compiled with `/arch:AVX2`; that switch alone does not prove SIMD vectorization. AVX2 is a later optimization target, not a factor claimed in these numbers.
|
||||
|
||||
## Three architectures considered
|
||||
|
||||
| Architecture | Representation and pipeline | Main advantage | Main cost/risk | Verdict |
|
||||
|---|---|---|---|---|
|
||||
| **A. Sparse compiled float bricks + CPU contouring + field-aware player edits** | Seed/layout compile to a stable feature plan. Bin primitives to touched bricks. Each 8³ brick stores a final-sign certificate, quantized density cache and edit version. Mesh uncertain bricks with marching cubes; measure per-tile RMC upload before grouping. Use field queries for immediate player edits; test prioritized cook for steady collision. GPU edit transactions cover crater-scale changes. | Eliminates repeated feature traversal, supports edits and 3D topology, and avoids waiting on all ring cooks for player readiness. | Conservative certificates, movement integration and same-frame crater rendering still need UE/VR proof. | **Recommended staged experiment.** |
|
||||
| **B. Analytic CSG/BVH + raymarch render and collision** | Keep a spatially indexed graph of implicit primitives and bounded noise, no resident sampled terrain except caches. GPU raymarches image-space rays through an acceleration structure; CPU queries analytic field for physics. Edits add CSG nodes. | Very long view and same-frame edits can avoid remeshing entirely. | Full-screen/stereo ray cost, non-SDF operators break safe sphere tracing, shadows/materials/UE integration; many edits enlarge BVH and query cost. It changes visual appearance. | Radical alternative if meshing/upload still dominate after A. |
|
||||
| **C. Integer GPU brick generation + compute meshing** | CPU generates canonical integer plan/brick list; compute shader evaluates fixed-point density and emits deterministic mesh indices by prefix-sum, while CPU uses the same integer evaluator for authoritative player collision. | Could offload 17–22 M sample and contour work without collision readback; GPU Gems 3 demonstrates fully 3D GPU generation rather than a heightfield. | Cross-vendor integer ABI, duplicate CPU/GPU evaluators, VR frame contention, runtime mesh integration. | Research branch only until exact AMD/NVIDIA and CPU/GPU hash parity is demonstrated. |
|
||||
|
||||
Architecture A should treat the **plan as the source of truth** and bricks as disposable caches. Compile op nodes once per region into a fixed order and collect local feature references by feature bounding box, with an explicit bound for noise, warps and edits. A brick certificate must enclose the **final** composed field: an operator that can add solid after a carve invalidates a carve-only proof. If any op cannot supply a safe bound, mark the brick uncertain. The current interval route's measured slowdown is a warning that a proof must cost less than the points it avoids. A local feature raster pass reverses the present “each point searches nearby features” loop and keeps shape data hot in cache. The sparse hierarchy stores occupied, empty and uncertain bricks; a compact, flat Morton or Z-major page table is likely better for the active 64 m cube than a pointer-heavy tree. OpenVDB provides a precedent for sparse, editable narrow-band fields, while sparse voxel octrees/DAGs are more compelling for static repeated geometry than for heavily edited procedural bricks. [OpenVDB](https://museth.org/Ken/Publications_files/Museth_TOG13.pdf), [Laine and Karras SVO report](https://research.nvidia.com/sites/default/files/pubs/2010-02_Efficient-Sparse-Voxel/laine2010tr1_paper.pdf), [Kämpe et al. sparse voxel DAG](https://www.cse.chalmers.se/~uffe/HighResolutionSparseVoxelDAGs.pdf).
|
||||
|
||||
Retain marching cubes initially: in the local measured export, evaluation dominates, so changing the mesher first has little expected payoff. A later surface-nets or dual-contouring trial may reduce vertex/triangle output or preserve sharp authored features, but QEF placement, topology and transitions add complexity; benchmark the *whole upload path* before changing. Dual contouring's paper addresses Hermite samples and adaptive octrees; Transvoxel has explicit transition cells for adjacent marching-cubes LODs. The current skirts can visually hide cracks but are not a geometric continuity proof. [Dual contouring paper](https://people.eecs.berkeley.edu/~jrs/meshpapers/JuLosassoSchaeferWarren.pdf), [Transvoxel author](https://transvoxel.org/), [surface nets paper](https://www.sciweavers.org/publications/constrained-elastic-surface-nets-generating-smooth-surfaces-binary-segmented-data).
|
||||
|
||||
For A's collision, store an immutable field snapshot and expose a broad-phase AABB per resident spatial page. Its exact local ray/shape queries must use the canonical quantized lattice or a triangle patch made from it. The provisional player route is a purpose-built field-aware capsule sweep whose support state belongs to the current edit epoch; the staged cook and CMC override experiment in the consolidated plan may justify retaining prioritized cooked collision for steady terrain. For arbitrary Chaos rigid bodies, test a custom `FImplicitObject`/geometry-query adapter only if field-query fallback is insufficient; UE 5.7's installed `Chaos/ImplicitObject.h` exposes virtual `PhiWithNormal` and `Raycast`, but that does **not** prove the complete broad-phase, GJK/contact, CharacterMovement, navmesh or replication integration. A conservative collision box cannot stand in for a cave: it would block playable air. A signed density is not necessarily a distance bound, so ordinary sphere tracing may step through a thin wall; use interval subdivision/voxel traversal or a certified Lipschitz bound. [Chaos API](https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Chaos), [Hart sphere tracing](https://experts.illinois.edu/en/publications/sphere-tracing-a-geometric-method-for-the-antialiased-ray-tracing/).
|
||||
|
||||
## Determinism contract
|
||||
|
||||
**A:** Preserve the current precise-float CPU operator math and deterministic feature order as the first implementation. Pin build flags and CPU feature path, retain the scalar/SSE parity checks, and run full ring/edited-world hashes on every supported CPU/compiler pair. Define a versioned *lattice* ABI: integer world coordinates, float-to-int16 rounding/clamping, sign at zero, fixed interpolation/ties, ordered integer edit composition and material choice. Brick jobs may finish in any order because each writes a disjoint key and shared corners have one owner. The GPU consumes the quantized cache; it need not reproduce float Perlin for edit visuals. A full fixed-point procedural generator and AVX2 path remain conditional experiments. FastNoise2's own FAQ warns that automatically selected floating SIMD levels can differ across machines, so do not import that mode as world authority without locked semantics and hash tests. [FastNoise2 FAQ](https://github.com/Auburn/FastNoise2/wiki/FAQ), [MSVC floating-point rules](https://learn.microsoft.com/en-us/cpp/build/reference/fp-specify-floating-point-behavior?view=msvc-170).
|
||||
|
||||
**B:** Analytic shape transforms and BVH construction need a stable sort and the same versioned CPU float/lattice contract. A GPU raymarch can remain a visual cache if it consumes canonical quantized samples or if its independently evaluated field passes CPU/GPU parity; arbitrary GPU float `sin`, `sqrt` and FMA cannot be assumed to match. `/fp:precise` helps source ordering but is not a cross-build magic guarantee, and CRT transcendental implementations can vary. The existing `DetSinCos` avoids one such dependency. [MSVC `/fp`](https://learn.microsoft.com/en-us/cpp/build/reference/fp-specify-floating-point-behavior?view=msvc-170), [CRT FMA3 selection](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-fma3-enable-set-fma3-enable?view=msvc-170).
|
||||
|
||||
**C:** Floating-point GPU density is ruled out for world decisions: Vulkan itself is not pixel exact, and available shader float controls have capability/operation limits. An integer GPU path is *potentially* deterministic only after restricting operations to defined 32-bit integer add/XOR/shift/multiply, explicitly emulating wider arithmetic if needed, forbidding unordered atomics for output order, using deterministic prefix sums, and hashing complete density/sign/mesh buffers on AMD, NVIDIA and CPU after every shader/compiler/driver class change. Until those parity tests pass, never make the GPU result authoritative for collision or replicated world state. [Vulkan invariance](https://docs.vulkan.org/spec/latest/appendices/invariance.html), [float controls proposal](https://github.com/KhronosGroup/Vulkan-Docs/blob/main/proposals/VK_KHR_shader_float_controls2.adoc). GPU Gems 3 demonstrates speed opportunities for 3D terrain, but its floating-point GPU pipeline is no determinism proof. [GPU Gems 3](https://developer.nvidia.com/gpugems/gpugems3/part-i-geometry/chapter-1-generating-complex-procedural-terrains-using-gpu).
|
||||
|
||||
## Preserving the game
|
||||
|
||||
**3D and composition.** The plan retains volumetric SDF/CSG regions, mountains, caves, overhangs and islands. A surface heightfield clipmap alone is disallowed: the original geometry-clipmap paper explicitly targets heightfields. Each current archetype, composer tag, structural post, passage and diff becomes a compiled op with a finite or explicitly unbounded influence declaration. [Geometry clipmaps paper](https://hhoppe.com/geomclipmap.pdf). The shipped examples support broad direction, not a claimed matching performance number: No Man's Sky describes its voxel-to-polygon pipeline and reports a later dual-marching-cubes rewrite improved loading; Astroneer describes re-polygonizing modified chunks; neither publishes a directly comparable one-second/64 m benchmark. Teardown's movable voxel volumes and packed occupancy hierarchy solve a different destructible-object problem, and Enshrouded's developer account confirms editable voxel terrain without exposing a transferable throughput figure. Dual Universe demonstrates a multiplayer editable voxel world, but its published material likewise gives no comparable generation latency. [NMS GDC talk](https://www.gdcvault.com/play/1024265/), [NMS update](https://www.nomanssky.com/worlds-part-I-update/), [Astroneer developer account](https://www.gamedeveloper.com/design/what-i-astroneer-i-s-devs-learned-while-leaving-early-access), [Teardown developer's voxel rendering account](https://blog.voxagon.se/2018/10/17/from-screen-space-to-voxel-space.html), [Enshrouded developer AMA](https://enshrouded.com/pl-PL/news/enshrouded-world-design-team-reddit-ama-recap), [Dual Universe developer diary](https://www.youtube.com/watch?v=MWcuwt9bjHo).
|
||||
|
||||
**Walk guarantee.** Keep the composer-generated entrance/exit route as an explicit graph of swept, capsule-clear walking corridors and landing floors. Apply that route as a late, protected operator whose minimum clearance and maximum slope have margins below the player capsule's mechanical limit; validate the final sampled collision representation with capsule sweeps. Decorative ops cannot narrow the protected clearance. For edits, protect a minimal support/clearance tube along one certified route; clip a crater's destructive part where it would remove the last walkable support or add an automatically generated bypass as part of the same transaction. A crater may therefore leave a thin protected bridge. This is a concrete rule that preserves the guarantee; the design of its visible cue is a gameplay/content task. The current route tests do not establish post-edit traversal, so verify it after each affected edit graph change.
|
||||
|
||||
**Clean digging.** Treat one edit as a transaction: allocate a monotonically numbered edit ID, compute every affected brick and one-cell halo from a single immutable pre-edit snapshot, and publish one canonical field/edit epoch before player movement in that frame. Border vertices use global lattice coordinates and a single owner, with identical edit IDs on both sides. The player queries the new quantized field immediately; a cooked body may serve steady terrain only while its edit epoch matches, and an obsolete cook cannot restore stale support. Character velocity is carried across an edit from actual displacement/input only; recompute grounded state without an invented downward impulse. A 1 m-radius interactive brush touches at most eight 2 m bricks at a grid corner before halo expansion. The render path switches the complete affected region in one frame using the GPU transaction described below, never a sequence of independent RMC tile updates. **This does not yet prove same-frame edits**: the UE/VR test must measure the whole transaction including GPU completion, stencil/depth, shadowing and stereo. Network edits carry server order and ID; prediction displays the same transaction locally and reconciles on authoritative order.
|
||||
|
||||
**Streaming and long view.** Keep a 3D clipmap but make LOD1 available first at 16 cells per tile: 19³/35³ = **0.160** of today's per-tile sample requests, an arithmetic count, not a measured 6.25× wall speedup. Applying that ratio to the supplied 16.5 M LOD1 requests yields **2.64 M requests**; at the old ~2 µs/request this is **5.28 CPU-seconds**, or **0.88 s on six ideally scaled cores**, before upload. This arithmetic makes “a few seconds” credible for the preview even without the new brick speed, but it is no end-to-end guarantee. Refine to 32 cells later. Reuse lattice coordinates when coarse samples coincide with resident fine samples; outside the fine cube, evaluate coarse bricks independently. A far shell should be surface-aware and perhaps use a much lower-resolution 3D contour plus impostor for sealed distant strata; preserve visible caves/islands rather than flattening to terrain height. Use Transvoxel or a tested equivalent when meshes at unequal LOD meet. Prioritize spawn/teleport center, movement direction, falling support and edits; apply back-pressure to far work. UE Tasks priorities affect scheduling but do not preempt a running expensive tile; divide work into cancellable bricks. [UE Tasks documentation](https://dev.epicgames.com/documentation/unreal-engine/tasks-systems-in-unreal-engine), [Transvoxel](https://transvoxel.org/).
|
||||
|
||||
**Multiplayer.** Replicate seed, numeric ABI/content hash, compact plan/layout ID and ordered edit log. Do not replicate bricks, meshes or cooked physics. Hash canonical brick signs, surface vertices and collision answers on clients; reject incompatible builds rather than silently diverging. An edit arrives as one transaction with all affected brick IDs, never independent tile messages. For late joins, bound and checkpoint the edit log: Teardown's developer describes why replaying a deterministic command stream saves bandwidth but can become too slow after many edits. This is a qualitative design warning, not a VoxelForge timing estimate. [Teardown multiplayer technical account](https://blog.voxagon.se/2026/03/13/teardown-multiplayer.html).
|
||||
|
||||
## Unreal rendering and Nanite
|
||||
|
||||
Keep RMC for the first experiment and measure its per-tile sustained apply rate with a raised frame cap. Group spatial pages and separate render pages from collision only if the measured game/render/RHI path misses the consolidated throughput gate. Replacing RMC with ProceduralMeshComponent only changes the wrapper around the same asynchronous cook; Epic documents `bUseAsyncCooking`, not a latency guarantee. `UDynamicMeshComponent` has useful edit representation but Epic lists no Nanite/LOD support and warns about render/memory efficiency. RMC's current upstream documentation claims runtime Nanite, and the locally installed RMC 5.4 source contains a runtime Nanite module and provider checks. **Revisit Nanite as an isolated *far/mostly static render* benchmark**, including PC VR stereo, page-build time, edit rebuild time and fallback mesh cost. It does not solve density evaluation or Chaos collision, and the first one-second ring should not wait for Nanite conversion. Nanite collision uses a fallback mesh, not Nanite triangles. [PMC API](https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/ProceduralMeshComponent/UProceduralMeshComponent), [Epic dynamic-mesh guide](https://dev.epicgames.com/documentation/unreal-engine/geometry-scripting-users-guide-in-unreal-engine), [RMC docs](https://github.com/TriAxis-Games/RealtimeMeshComponent/blob/master/Docs/index.md), [Epic Nanite fallback details](https://dev.epicgames.com/documentation/unreal-engine/nanite-technical-details).
|
||||
|
||||
## Round two: wider search and revised engineering test
|
||||
|
||||
### What the new local probe measured
|
||||
|
||||
[`Saved/Research/round2_probe.cpp`](Saved/Research/round2_probe.cpp) is an independent C++20 program, not copied production code. It models the *kinds* found in `VoxelNoise.h`, `VoxelDensityOpStack.cpp`, and `VoxelGenerator.cpp`: hashed 3D gradient Perlin with a fixed-point quintic fade, three-octave fBm, ridged noise, one domain warp, upper/lower stratum bounds, a passage-like corridor, a room, and two smooth maxima. The gradient choice and fade shape follow the plugin, but the hash, coordinate scale, parameters, and composed graph differ. The code has a scalar reference and explicit eight-lane AVX2 integer evaluator; each lane writes exactly the same 16-bit value in the measured domain. The present plugin already has **SSE4.1 x4 Perlin batching across octaves**; the new speedup is therefore **against this program's scalar reference**, not against the production plugin's SSE path.
|
||||
|
||||
Build/run from `Saved\Research` on the owner's Ryzen/Windows machine:
|
||||
|
||||
```powershell
|
||||
cmd /c '"E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul && cl /nologo /O2 /std:c++20 /EHsc /arch:AVX2 round2_probe.cpp /Fe:round2_probe.exe'
|
||||
.\round2_probe.exe
|
||||
```
|
||||
|
||||
MSVC compiled successfully; the installed setup script emitted a missing-`vswhere.exe` notice. Wall times below include six worker threads and output writes, exclude process startup and verification. Three three-trial runs with the same evaluation code (the second after dead-code cleanup, the third after adding the direct-query measurement) gave:
|
||||
|
||||
| Work | Measured wall time | Meaning |
|
||||
|---|---:|---|
|
||||
| 256³ complete scalar field | 492.0, 496.2, 469.6; 430.3, 456.2, 447.1; 478.6, 471.0, 444.3 ms | 16,777,216 output points. |
|
||||
| Same field, explicit AVX2 | 101.3, 106.0, 92.5; 89.2, 107.5, 94.6; 85.8, 80.3, 82.7 ms | Paired speedups **4.25–5.86×**, zero value mismatches. |
|
||||
| Full value comparison | 0 mismatches in all 16,777,216 points on each trial | One machine and coordinate domain, not cross-vendor proof. |
|
||||
| 200,000 spatially scattered direct field queries | 63.030, 62.785, 62.799 ms | **313.9–315.1 ns/query**, one scalar thread; all checksums equal. A synthetic player-query latency proxy, not a capsule sweep. |
|
||||
| Radius-10 m spherical edit against cached field | 0.865; 0.914; 0.861 ms | Six-worker max-composition over the 81³ bounding cube; excludes rendering and copy. |
|
||||
| Copy entire 256³ 16-bit field | 1.623; 1.422; 1.394 ms | An intentionally wasteful 32 MiB copy; local brick copy-on-write would copy much less. |
|
||||
| Touched 8³ bricks / changed lattice sites | 714 / 94,843 | Exact for the synthetic sphere at a brick/tile boundary; geometry and edit content affect changed count. |
|
||||
|
||||
**Inference, not measured production latency:** 17,373,979 ring points divided by the observed AVX2 throughput give **83.2–111.3 ms** for this operator mix. Even the slowest scalar trial extrapolates to ~514 ms; a 0.5 s worker budget is close without AVX2 and leaves no room for a more expensive real composer. The scattered-query result implies ~**635 queries in 0.2 ms** at this synthetic rate, versus **100** at the supplied current ~2 µs/sample. A real player capsule sweep must be bounded and separately timed; this arithmetic does not establish a 0.2 ms sweep. SIMD is one possible factor in the required ~12× reduction, not the entire answer. Probe limitations: the plugin uses float Perlin today, its ridged noise has feedback/squaring absent from this simplified ridge, several operator classes and dynamic world-composer decisions are absent, the program computes all points without brick classification, the six worker threads contend with no UE tasks, and no mesher/upload/collision/VR frame is timed. A full fixed-point replacement would visibly alter worlds, which the owner now permits through a new world version, but its production speed benefit has not been established. Avoid claiming the synthetic 5× is a drop-in speedup. [FastNoise2's fused SIMD graph and published 261.1 M 3D Perlin points/s on a different Intel CPU](https://github.com/Auburn/FastNoise2), [FastNoise2 exact-reproducibility FAQ](https://github.com/Auburn/FastNoise2/wiki/FAQ).
|
||||
|
||||
### New evidence that changes the architecture
|
||||
|
||||
| Source-measured or documented result | Transfer to VoxelForge |
|
||||
|---|---|
|
||||
| Eurographics 2025 **Lipschitz Pruning** reports 0.91–1.61 ms for 256³ discretization on a laptop RTX 4060 for scenes with 53–6,023 nodes, plus 0.72–14.29 ms pruning; its most complex model gained 629× in sphere-tracing against naïve evaluation. | Strong evidence for **region-specific compiled operator programs**, including smooth CSG, not a VoxelForge GPU speed prediction. The method assumes primitives/operators with computable 1-Lipschitz bounds; arbitrary warped scalar density needs derived bounds or a fallback. Its fluid scene with 27,977 close particles needs 125.5 ms pruning and 6.99 ms discretization, demonstrating layout sensitivity. [Paper and Table 1](https://wbrbr.org/publications/LipschitzPruning/documents/LipschitzPruning_submitted_to_EG25.pdf). |
|
||||
| **Accelerating Signed Distance Functions** reports up to three orders of magnitude on its construction-tree scenes, using proxy/LOD nodes. It expressly warns that a discontinuous proxy below a smooth union causes artifacts, and that manual LOD authoring and extra nodes cost memory. | Compile local plans and conservative proxies **with operator-aware legality**; no generic “replace any far branch with a constant.” Preserving the exact zero set near transitions matters for walking and digging. [Paper](https://perso.liris.cnrs.fr/eric.galin/Articles/2025-lod.pdf). |
|
||||
| Keeter's SIGGRAPH 2020 interval method simplifies complex closed-form expressions by region; Knoll et al. compared interval and affine arithmetic for implicit ray tracing. | Both are real alternatives for exotic operators, but the **local** blanket interval attempt worsened 0.588 s to 2.057 s. Use inexpensive analytic primitive influence and final-sign certificates first; profile any tighter affine bound by avoided evaluations per microsecond. [Keeter](https://www.mattkeeter.com/research/mpr/), [Knoll et al.](https://www.vacet.org/publications/Knoll_CGF-implicits-2009.pdf), [local control](Docs/archive/PERF-ATTRIBUTION.md). |
|
||||
| Eurographics 2025 **Fast Sphere Tracing of Procedural Volumetric Noise** reports a detailed 3D procedural-noise scene at **16 ms for 1024² pixels** on an Nvidia 4080 Ti, versus 110 ms naïve; the authors report up to 12× on opaque/constant-transparency scenes using derivative-aware stepping and nested fBm bounds. | Strongest recent case for the analytic-render alternative. Linear scaling from 1.05 M to 4.15 M stereo pixels would be ~**63 ms** on that *different GPU/scene*, before VR lighting; this is a cautionary inference, not a measured Quest 3 figure. Reuse its nested fBm bound idea for local-region culling even if raster meshes remain primary. [Paper](https://diglib.eg.org/bitstream/handle/10.1111/cgf70072/cgf70072.pdf). |
|
||||
| JCGT 2019 reports **15–20%** faster 3D cellular noise traversal than standard Worley on its benchmark (for 16 M queries, one table row falls from 13.69 s to 11.15 s). | Relevant only to the plugin's cellular roughness branch, not its dominant gradient-Perlin calls; do not multiply this into a ring-wide speedup. [JCGT paper](https://jcgt.org/published/0008/01/02/). |
|
||||
| Voxel Plugin 2's own change log claims up to **2×** world generation from skipping cells far from surface, plus up to **40%** VRAM reduction in an experimental Nanite path. Its docs separate collision and render invokers. | Confirms that surface sparsity and collision/render separation are practical in UE, but vendor numbers are not a transferable end-to-end 12×. [Voxel Plugin 2 change log](https://docs.voxelplugin.com/resources/change-log/2.0p8-changelog), [collision design](https://docs.voxelplugin.com/2.0p8%20-%20v2/design-docs/collision-mesh-navmesh). |
|
||||
| FastNoise2 reports **261.1 M** AVX2 3D Perlin points/s on an Intel 7820X; its FAQ says floating SIMD levels can differ and advises strict FP plus a locked SIMD level for exact output. | Fuse whole op graph in registers; do not import float FastNoise2 with automatic SIMD dispatch as the canonical multiplayer field. Our fixed integer benchmark shows a path to parity on one CPU; cross-machine parity still needs testing. [Library and benchmark](https://github.com/Auburn/FastNoise2), [FAQ](https://github.com/Auburn/FastNoise2/wiki/FAQ). |
|
||||
| Epic's UE 5.7 Mover comparison says movement can use a root scene component without a collision primitive and custom movement modes; local UE 5.7 `CharacterMovementComponent.cpp` still makes floor and move sweeps through the engine collision scene. | Mover is the provisional field-aware movement route, preserving sweeps against nonterrain actors. A prioritized cook plus an edit-time CMC field override may be less invasive if the staged-cook and underfoot edit gates both pass. Simply leaving the old CMC capsule on an uncooked mesh does not fulfill “collidable.” Mover remains a migration risk under network prediction. [Epic Mover comparison](https://dev.epicgames.com/documentation/unreal-engine/comparing-mover-and-character-movement-component-in-unreal-engine). |
|
||||
| Epic's RDG documentation supports compute passes, dependency barriers and async compute but notes platform fallback to the graphics pipe. | A GPU edit mesher can be integrated with a render-graph transaction, but “async” does **not** establish same-frame latency or free VR time. [Epic RDG guide](https://dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine). |
|
||||
|
||||
### Crater spell: one transaction, up to 64 tiles
|
||||
|
||||
A radius-10 m sphere is **40 lattice intervals** at 25 cm. Its 20 m AABB reaches up to **4 tiles per axis** when centered on a tile boundary, hence **64 full-detail 8 m tiles**; at a tile center it reaches at most 27. The sphere's continuous volume is `4π40³/3 = 268,083` voxel cubes versus `81³ = 531,441` candidate lattice sites. The local exact sphere/AABB enumeration found **714 2 m bricks** touching the ball. Independently contouring each 8-cell brick needs `714×9³ = 520,506` corner-sample reads before sharing; the real distinct set is smaller, and normal or LOD halos can add reads. A naïve re-evaluation at today's 2 µs/sample would cost about **1.04 CPU-seconds** for those corners, so **cache the base field and compose the edit over it**. The measured synthetic cached-field edit took **0.865–0.914 ms**, but no extraction or upload was measured. CPU copy-on-write at touched bricks has a raw 16-bit interior payload of `714×8³×2 = 731,136 bytes`; a full near-field 32 MiB copy was measured at 1.422–1.623 ms. The crater itself is ~one command (center/radius/shape/seed/version), not 95,000 replicated voxels. A pathological marching-cubes upper bound is `714×512×5 = 1.83 million` triangles; at three unshared 16-byte vertices each that is **87.7 MB** of transient output. Realistic counts must be measured, and GPU buffer overflow must fall back to a complete-region visual path, never publish half a crater.
|
||||
|
||||
**Proposed fast path.** Commit an edit in a deterministic order with a monotonically increasing edit sequence. On the CPU, publish a new immutable edit-plan pointer for *all* 714 bricks before movement and render gather for that frame. A canonical signed integer field query composes the crater immediately, so player floor/capsule sweeps cannot see stale support; nonplayer traces use the same fallback until their optional Chaos mesh is ready. Never transfer a removed collision body's velocity or use old cooked floor data after the edit sequence changes. If the dig removes the floor, real gravity begins from the player's pre-edit velocity exactly once; if it adds terrain around the capsule, resolve penetration with a bounded positional correction that does not enter velocity. The entrance-to-exit route is a **protected graph corridor**: preflight the crater against a protected walkable tube and either clip its solid-adding part, carve a replacement bypass, or reject the spell by explicit gameplay rule. For pure excavation, connectivity cannot be reduced, though walking surfaces can be removed, so the route proof must include footholds/clearance, not only air connectivity.
|
||||
|
||||
For visuals, use one GPU edit transaction over the whole affected region. Keep the near lattice field resident on the GPU; upload the compact command, compute the changed bricks and their transition halo in a **single render-graph dependency**, generate canonical marching-cubes triangles with a stable per-cell prefix sum, and swap the entire region's draw descriptors together at a frame boundary. Suppress the old region only when its replacement is drawable; no per-tile apply loop. All visible coarser LOD footprints touched by the spell take the same edit sequence, using the corresponding coarse lattice and transition mesh; an unloaded distant region records the command for generation when it enters view. A local volume-raymarch overlay can cover the transaction while mesh extraction is late, using the same canonical cached field and edit command, with a one-cell overlap and exact depth writes. The overlay must be checked against the meshed boundary and stereo views to rule out cracks, double surfaces, and ghost shadows. **This is an engineering design, not a proved same-frame guarantee:** no UE/GPU measurement was authorized in this research pass. The decisive GPU/VR test must finish edit compute + descriptors + draw in the **first submitted render frame whose simulation includes the command**; an input received after that frame's render submission belongs to the next frame. No affected tile may show a different edit sequence in that frame. [Epic RDG synchronization](https://dev.epicgames.com/documentation/unreal-engine/render-dependency-graph-in-unreal-engine), [GPU Gems 3's 3D GPU marching cubes](https://developer.nvidia.com/gpugems/gpugems3/part-i-geometry/chapter-1-generating-complex-procedural-terrains-using-gpu).
|
||||
|
||||
The current crash mode is especially clear for crater scale: synchronous center-tile remesh followed by up to 63 independently queued neighbors produces a visible wave; disabling old collision and waiting 0.4–1.4 s for cooks creates a falling gap; retaining old BodySetup creates ghost support. The transaction above removes those three states. It does **not** assume that optional Chaos rigid bodies can all get exact crater collision in the same frame; the owner expressly accepts field-query fallback for NPCs, arrows and other physics until their mesh is ready.
|
||||
|
||||
### Serious alternatives, with quantified wins and losses
|
||||
|
||||
| Alternative | Where it could win | Why it is not the primary route now |
|
||||
|---|---|---|
|
||||
| **Whole-world analytic raymarch (radical B)** | No persistent triangle mesh; edit command changes the next render. Keeter and 2025 pruning show that complex trees can be raytraced after regional simplification. | At 1920×1080, **2.07 M rays per eye**, or **4.15 M stereo rays** before shadows/VR supersampling; at 32 field steps this is **133 M queries/frame**. At the measured synthetic AVX2 throughput of ~156–209 M *grid* points/s across six cores, that many CPU queries would take ~0.64–0.85 s; GPU may be far faster. The 2025 procedural-noise paper measures **16 ms at 1024²** on a 4080 Ti; simple stereo-pixel scaling to 4.15 M gives ~63 ms before VR lighting, a scene/hardware inference. Density is not necessarily a distance bound for safe sphere tracing, and a full GPU renderer is a larger rewrite. [Keeter](https://www.mattkeeter.com/research/mpr/), [EG 2025 noise tracing](https://diglib.eg.org/bitstream/handle/10.1111/cgf70072/cgf70072.pdf), [SDF-grid ray tracing, JCGT](https://jcgt.org/published/0011/03/06/). |
|
||||
| **All-GPU integer generation and meshing (C)** | The Eurographics RTX 4060 paper's 256³ discretization at 0.91–1.61 ms shows enormous headroom in a suitable GPU expression, though its data/model differ. Could supply same-frame crater triangles. | Requires explicit 32/64-bit integer arithmetic rules, fixed division/rounding, no unordered atomics for vertex order, deterministic prefix sums, CPU/GPU/AMD/NVIDIA parity hashes, and an UE RHI path. Any float-transcendental or vendor-specific FMA noise breaks the canonical requirement. The paper's pruning and rendering are GPU-only and do not prove the CPU player query. [Lipschitz Pruning](https://wbrbr.org/publications/LipschitzPruning/documents/LipschitzPruning_submitted_to_EG25.pdf), [Khronos float controls](https://github.com/KhronosGroup/Vulkan-Docs/blob/main/proposals/VK_KHR_shader_float_controls2.adoc). |
|
||||
| **Dual contouring / surface nets / adaptive power-diagram contouring** | Potentially fewer vertices and sharper features; JCGT's SurfaceNets work reports better triangle quality than classic MC for segmentation models. A 2025 power-diagram method reconstructs complex SDF geometry from fewer samples. | Generation is ~97% density time in the local attribution, so changing contouring *alone* can save at most the remaining ~3% of that worker window. Adaptive sampling could save more but needs a certified error/feature rule for narrow passages. SIGGRAPH Asia 2024's GPU occupancy dual contouring still describes runtimes of **a few seconds** for its distinct reconstruction problem, not subsecond terrain streaming. Seam dependencies and LOD transition rules become more complex; neither solves cook or atomic edit transactions. Benchmark later if upload dominates after density rewrite. [Dual contouring](https://people.eecs.berkeley.edu/~jrs/meshpapers/JuLosassoSchaeferWarren.pdf), [JCGT SurfaceNets](https://jcgt.org/published/0011/01/03/), [Eurographics 2025 power-diagram contouring](https://onlinelibrary.wiley.com/doi/10.1111/cgf.70037), [SIGGRAPH Asia 2024 occupancy dual contouring](https://occupancy-based-dual-contouring.github.io/), [chunk seam example](https://ngildea.blogspot.com/2014/09/dual-contouring-chunked-terrain.html). |
|
||||
| **SVO/DAG or OpenVDB as source of truth** | Empty-space skipping and sparse memory; GPU SVDAG editing shows large 3D painting can be interactive in a different system. | The world's seed/composer already *is* a compact source. At full 25 cm resolution, the 64 m cube alone has 17.37 M unique samples or **34.7 MB at 16 bits** if fully resident; a global 6 km view volume is vastly larger. DAG sharing degrades with unique noise and edits; keep a sparse cache/hierarchy, not a required full-world DAG. The cited GPU editor's code uses CUDA, so it is no cross-vendor solution. [OpenVDB](https://museth.org/Ken/Publications_files/Museth_TOG13.pdf), [SVDAG editing paper](https://diglib.eg.org/items/ffd7acf6-01f7-4e53-87f8-4cded382c944), [author code](https://github.com/mathijs727/GPU-SVDAG-Editing). |
|
||||
| **Prebake each seasonal world in the shipped game** | If seasons have a finite, known seed/layout set, local NVMe transfer could remove cold density evaluation and deterministic divergence. | For this 64 m cube, even raw 16-bit density is **34.7 MB** before meshes; ~6 km horizontal radius with only an **8 m thick** full-detail slab is `π×6000²×8/(0.25³) ≈ 57.9 billion` voxels, **116 GB raw** per season. That slab is an intentionally generous lower bound for a mountainous/cave world. Sparse compressed meshes could be far smaller, but their disk, decompression, upload, and arbitrary composer/layout variants need an actual content budget. Prebake a few spawn hubs or far-field impostors, not every possible volume. |
|
||||
| **Server-streamed chunk data** | Server could send already generated chunks, avoiding client generation. | A raw 34.7 MB 64 m cube in 1 s requires **278 Mb/s payload** before meshes/protocol, and this contradicts the explicit requirement that only seed, layout and edits replicate. Compression helps bandwidth, but latency/server scaling and the hard protocol rule remain. |
|
||||
| **Far-field impostors, 2.5D clipmaps, Nanite** | Far view can be far cheaper if represented by coarse meshes/impostors; RMC locally contains a runtime Nanite proxy path, and Voxel Plugin 2 reports experimental Nanite memory savings. | An exterior heightfield loses caves/overhangs/islands when used as the whole world. Impostors are acceptable **beyond** interaction range if the world returns to 3D on approach, with a continuous crossfade and occlusion policy. Nanite can help mostly static far render, but Epic's dynamic mesh guidance says `UDynamicMeshComponent` is not Nanite-compatible, and Nanite fallback geometry still serves collision; runtime build/edit/VR costs here are unmeasured. [Geometry clipmaps](https://hhoppe.com/geomclipmap.pdf), [Epic dynamic mesh guidance](https://dev.epicgames.com/documentation/unreal-engine/working-with-meshes-in-unreal-engine), [Nanite fallback](https://dev.epicgames.com/documentation/unreal-engine/nanite-technical-details), [RMC code](https://github.com/TriAxis-Games/RealtimeMeshComponent). |
|
||||
|
||||
### What was searched, and what was ruled out
|
||||
|
||||
The source sweep covered SIGGRAPH closed-form interval rendering and GPU Gems 3 volumetric marching cubes; Eurographics 2025 Lipschitz pruning, derivative-aware procedural-noise tracing, proxy/LOD SDFs and adaptive contouring; older affine-interval ray tracing; JCGT SDF-grid ray tracing, cellular noise and SurfaceNets; HPG/i3D-style GPU mesh shader and SDF work; OpenVDB, SVO, DAG and GPU SVDAG editing; FastNoise2, ISPC, Inigo Quilez's analytic noise-derivative slides and table-free hashed noise; UE 5.7 Mover/CMC source, RMC local source, Epic RDG/Nanite/dynamic mesh documentation; Voxel Plugin 2; and developer material for No Man's Sky, Astroneer, Teardown, Enshrouded, Space Engineers, Vintage Story, Dual Universe, Sebastian Lague, Nick Gildea and 0fps. Quilez's derivative formula is useful for normal or local bound construction, but a sampled derivative is **not** a certified maximum over a brick. The strongest transferable ideas were **region-pruned expression trees, fused SIMD evaluation, sparse near-surface work, direct player field collision, and whole-region render transactions**. No retrieved shipped-game account publishes a directly comparable **cold 64 m, 25 cm, 3D editable, UE/VR, all-visible-and-player-collidable <1 s** measurement; their performance figures must not be presented as proof of this target. [Quilez noise-derivative slides](https://iquilezles.org/articles/function2009/function2009.pdf), [NMS GDC](https://www.gdcvault.com/play/1024265/), [Astroneer developer](https://www.gamedeveloper.com/design/what-i-astroneer-i-s-devs-learned-while-leaving-early-access), [Teardown developer](https://blog.voxagon.se/2018/10/17/from-screen-space-to-voxel-space.html), [Space Engineers source](https://github.com/KeenSoftwareHouse/SpaceEngineers), [Vintage Story developer](https://www.vintagestory.at/blog.html/news/de-jank-and-game-juice-update-1170-pre1-r322/), [0fps smooth voxel terrain](https://0fps.net/2012/07/12/smooth-voxel-terrain-part-2/).
|
||||
|
||||
Specifically ruled out as a **complete** answer: greedy block meshing (different surface model), heightfield-only terrain, unbounded generic interval recursion (failed local control), auto-selected float SIMD noise (vendor/CPU parity risk), CUDA-only edit structures (AMD target), Nanite or another UE mesh component alone (density/cook bottlenecks), and one tile at a time crater updates. Isosurface mesher alternatives, affine arithmetic, full GPU generation, and far impostors remain research branches with explicit benchmark gates. [0fps block meshing](https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/), [FastNoise2 determinism FAQ](https://github.com/Auburn/FastNoise2/wiki/FAQ), [SVDAG editor code](https://github.com/mathijs727/GPU-SVDAG-Editing), [Epic PMC async cooking](https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/ProceduralMeshComponent/UProceduralMeshComponent).
|
||||
|
||||
### Revised gates and migration sequence
|
||||
|
||||
The consolidated seven-step plan above supersedes this round-two sequence. Its decisive change is to **measure the existing collision and RMC apply stages first**, then benchmark a prepared *float* region evaluator against the actual composer before choosing a full integer procedural ABI, Mover or render grouping. Keep the composer/content schema, route graph, multiplayer edit log and old tile evaluator as an oracle while migrating. The final numeric ABI still specifies quantization scale, rounding/saturation, zero tie, interpolation, material ID and stable op ordering; scalar/SSE parity and two-CPU hashes are release gates. A cross-vendor GPU hash is required for integer edit and mesh decisions, but the GPU does not need to reproduce float procedural noise. The round-two standalone probe remains a lower-complexity feasibility bound, not the acceptance test for production latency.
|
||||
|
||||
## What not to do
|
||||
|
||||
- Do not infer a 512-tile deadline from one tile's p95, or from the synthetic 0.1 s classified benchmark. Queueing, bandwidth, upload and physics are separate clocks.
|
||||
- Do not put all 512 Chaos triangle cooks on the **player** readiness path. The observed 0.4–1.4 s interval is submit-to-ready and may be mostly queue delay; prioritize and time the small player-support neighborhood before ruling out cooked steady-terrain collision.
|
||||
- Do not assume a sparse octree/DAG or OpenVDB automatically speeds a procedural query. Their compression and hierarchy help only if the final-field signs/bounds can be computed cheaply; edits and arbitrary smooth operators can make most nodes uncertain.
|
||||
- Do not rely on heightfield clipmaps, GPU float world decisions, unordered GPU atomics, or `sin`/CRT results as a network world ABI.
|
||||
- Do not claim AVX2, dual contouring, Nanite or a different mesh component will erase the observed 97% density cost. Benchmark each in its proper stage.
|
||||
- Do not publish one touched tile while its neighbors still show the old edit; do not suppress collision under a pawn before a consistent replacement exists. Those are the visible ripple, vanish and false-support failure modes in the present implementation.
|
||||
|
||||
The owner has clarified both contracts: collision inside one second applies to the player, while bounded digging and radius-scale spells both require atomic same-frame visuals. Immediate **field-aware edit support** and the transaction-wide visual path are required; Mover versus a prioritized-cook/CMC field-overlay hybrid is decided by the staged tests above. Their deadlines remain unproved until an authorized UE/VR integration gate can be run.
|
||||
@@ -49,19 +49,8 @@ Legend: ✅ verified against code · ◻️ checklist box.
|
||||
> ("nothing is set in stone"). ✅ BUILT & WORKING together with pass 2 (ticked 2026-07-27):
|
||||
> • **Terracing gradient Z-only** (6→2 SDF samples — see the ticked item above).
|
||||
> • **Lerp X-macro** + **BakeRoomFeature dedupe** (both ticked above, bit-identical).
|
||||
> • **T2.b LOD octave drop** — opt-in `UVoxelSettings::LODOctaveDrop` (default 0 = byte-identical);
|
||||
> `VoxelGenLOD::OctaveBias` thread_local set per tile in `GenerateMesh`, per-voxel fractal sites
|
||||
> wrapped in `VoxelGenLOD::Eff(N)`, XY-field noise deliberately excluded. ARCHITECTURE §8.10.
|
||||
> • **T2.c tile component pool** — `TileComponentPool` + `Acquire/ReleaseTileComponent`; unload
|
||||
> parks (RemoveSectionGroup strips geometry+collision, hidden, stays registered), apply pops.
|
||||
> Bonus: `ApplyMeshToTile` reuses the existing `URealtimeMesh` (`GetRealtimeMeshAs`) — the old
|
||||
> unconditional `InitializeRealtimeMesh` allocated + orphaned one mesh UObject PER APPLY.
|
||||
> • **T2.d worker clamp** — `GetMaxConcurrentTasks()` caps the asset budget to logical cores − 2
|
||||
> (the BackgroundNormal priority half had already shipped). **Tier 2 is now COMPLETE** —
|
||||
> T2.a was already done (float SSE `VoxelNoise` core, §8.10).
|
||||
> • **F2 determinism validator** — `AVoxelWorld::ValidateDeterminism` CallInEditor button:
|
||||
> boundary points sampled under two cache-window alignments + a repeat pass; any non-zero
|
||||
> delta = §8.4 regression. The tool that VERIFIES all the "bit-identical" claims above.
|
||||
> • **T2.b / T2.c / T2.d and F2** shipped in the same batch — tracked in `fable-idea.md` (Tier 2
|
||||
> status, F2) and ARCHITECTURE §8.10, not repeated here.
|
||||
|
||||
## Performance
|
||||
- [x] ~~**`VoxelGenerator.cpp` (terrain-op gradient)** — compute once per voxel and reuse.~~
|
||||
@@ -90,7 +79,10 @@ Legend: ✅ verified against code · ◻️ checklist box.
|
||||
like Box/Capsule already did (one diff-layer + remesh dispatch). *(2026-07-04)*
|
||||
- [x] **`VoxelContentManager.cpp`** — duplicated drain/reset loops → `DrainDecoResults()` +
|
||||
`ResetGridBuildState(FDecoGrid&)`. *(2026-07-04)*
|
||||
- [ ] **`EVoxelPassageType` vs `EVoxelPassageStyle`** — two overlapping passage-shape enums, both in
|
||||
- [x] **`EVoxelPassageType` vs `EVoxelPassageStyle`** — ✅ **CLOSED 2026-09-04 as WONTFIX**, enacting
|
||||
the 2026-08-16 judgement below, which recommended closure and was then left open by oversight. No
|
||||
code change; the two enums stay.
|
||||
Two overlapping passage-shape enums, both in
|
||||
active use (11 refs). Consider consolidating to one. *(judgment call, not dead)*
|
||||
**JUDGED 2026-08-16 — LEAVE THEM. Recommendation: close this item rather than act on it.** They
|
||||
read as duplicates from the index and are not: `EVoxelPassageType` (`VoxelStrateTypes.h` ~42) is
|
||||
@@ -113,6 +105,43 @@ Legend: ✅ verified against code · ◻️ checklist box.
|
||||
two-grid deco redesign (`StreamTier` / `DecorationNearRadiusChunks`).
|
||||
**CORRECTION: `GetStrateChunkZBounds` is NOT dead** — `BuildDesiredTiles` uses it for the
|
||||
strate-aware vertical clamp; struck from the dead list.
|
||||
- [x] **2026-09 cleanup (`cleanup/2026-09`)** — the far sheet ring runtime (`GenerateSheetMesh` + its
|
||||
streaming path) and ~40 unreferenced functions/members/types removed (each proven by a search of
|
||||
`Source/`, `Tools/`, `Config/` and build files; see that branch's `refactor(dead-code)` commit).
|
||||
Reflected (`UPROPERTY`/`UFUNCTION`/`UENUM`…) and enum candidates were NOT removed — see
|
||||
*Owner decisions* below.
|
||||
|
||||
## Owner decisions (2026-09 cleanup)
|
||||
Items left for the owner: unused data that assets, Blueprints, tools or tests may reference by name,
|
||||
and changes that would alter generated terrain.
|
||||
- [x] `UVoxelSettings::bFarSheetRing` / `FarSheetSpanLevels`, `voxel.TestFarSheetRing` and the
|
||||
selector's sheet branch — removed (owner go-ahead); the harness no longer passes the switch and the
|
||||
`sheet_tile` dump field stays for the parity compare.
|
||||
- [x] `UVoxelSettings::CeilingViewMultiplier` / `CeilingBandChunks` / `DensityVolumeMaxTasks` — removed
|
||||
(never read).
|
||||
- [x] **Operator stack as the only density path** (cleanup/2026-09, owner-approved). Every strate —
|
||||
authored, cooked-season (recipe or native) and editor composer candidate — now generates through its
|
||||
operator stack; a degenerate strate is air. Removed: `UVoxelStrateDefinition::bUseOperatorStack`, the
|
||||
archetype `switch` in `GetDensityAt`, `GetSlabDensity` / `GetMazeDensity` / `GetSurfaceDensity` /
|
||||
`SurfaceDensityFromColumn` / `GetVerticalShaftDensity` / `GetFloatingIslandDensity`, the four
|
||||
`Cave Bail Not Op Stack *` stats, the startup opt-in warning, commandlet `-opstack`, and the
|
||||
legacy-vs-stack comparisons in the tests. **Terrain changes for any strate asset that did not
|
||||
already have the opt-in ticked.**
|
||||
- [ ] **No-strate-manager fallback world.** A world whose settings have no season, pool or fixed
|
||||
strates runs `GetDensityAt`'s fallback: default `FStrateGenerationParams` through
|
||||
`GetDensityWithParams` with its direct-caller ("legacy") structural posts. It is the only caller of
|
||||
that post path outside tests/commandlet diagnostics. Decide whether an empty configuration should
|
||||
keep generating generic caves or refuse to generate.
|
||||
- [ ] `UVoxelStrateDefinition::WaterColor`, `Creatures`, `AmbientSound`, `Music`, `MusicVolume` and the
|
||||
`FStrateCreature` / `FStrateAmbientActor` spawn fields (`SpawnChancePerChunk`, `MaxPerStrate`,
|
||||
`SpawnWeight`) — authored data with no consuming system yet (F9 audio, creatures).
|
||||
- [ ] `UVoxelBiomeDefinition::WaterMaterial` — never read; the water plane uses the strate's
|
||||
`WaterMaterial` only.
|
||||
- [ ] Plain C++ enums with no user: `EVoxelFace` (`VoxelTypes.h`), `EVoxelSeasonSelectionReason`
|
||||
(`VoxelSeasonManifest.h`), `EVoxelOpCombine` (`VoxelDensityOp.h` — its comment carries the
|
||||
two-channel sign convention; keep that text if the enum goes).
|
||||
- [ ] `IVoxelHeightOp::MaxDisplacement` / `FVoxelHeightStack::MaxTotalDisplacement` — no caller; kept as
|
||||
the documented hook for a future heightfield `ClassifyBox`.
|
||||
|
||||
## Over-complexity (behavior-preserving splits, optional)
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Standalone throughput probe, not VoxelForge's generator or an Unreal integration.
|
||||
// Build from a VS 2022 x64 Native Tools prompt:
|
||||
// cl /nologo /O2 /std:c++20 /EHsc /arch:AVX2 brick_kernel.cpp /Fe:brick_kernel.exe
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
constexpr int N = 256, B = 8, NB = N / B;
|
||||
#ifndef VF_WORKERS
|
||||
#define VF_WORKERS 8
|
||||
#endif
|
||||
struct Sphere { int x, y, z, r; };
|
||||
static std::array<Sphere, 48> spheres;
|
||||
static std::vector<int16_t> field(size_t(N) * N * N);
|
||||
static std::vector<uint8_t> reference_sign(size_t(N) * N * N);
|
||||
static volatile uint64_t sink = 0;
|
||||
|
||||
static uint32_t hash3(int x, int y, int z, uint32_t seed) {
|
||||
uint32_t h = seed ^ (uint32_t(x) * 0x9e3779b1u) ^
|
||||
(uint32_t(y) * 0x85ebca77u) ^ (uint32_t(z) * 0xc2b2ae3du);
|
||||
h ^= h >> 16; h *= 0x7feb352du; h ^= h >> 15;
|
||||
h *= 0x846ca68bu; return h ^ (h >> 16);
|
||||
}
|
||||
// Integer lattice noise: fixed shifts and products; no FP/CRT/FMA.
|
||||
static int noise(int x, int y, int z) {
|
||||
int sum = 0;
|
||||
for (int octave = 0; octave != 3; ++octave) {
|
||||
int s = 16 >> octave, ix = x / s, iy = y / s, iz = z / s;
|
||||
int fx = (x % s) * 256 / s, fy = (y % s) * 256 / s, fz = (z % s) * 256 / s;
|
||||
uint32_t seed = 1337u + uint32_t(octave) * 17u;
|
||||
int yz[2];
|
||||
for (int dz = 0; dz != 2; ++dz) {
|
||||
int yy[2];
|
||||
for (int dy = 0; dy != 2; ++dy) {
|
||||
int a = int(hash3(ix, iy + dy, iz + dz, seed) & 255u);
|
||||
int b = int(hash3(ix + 1, iy + dy, iz + dz, seed) & 255u);
|
||||
yy[dy] = a + ((b - a) * fx >> 8);
|
||||
}
|
||||
yz[dz] = yy[0] + ((yy[1] - yy[0]) * fy >> 8);
|
||||
}
|
||||
int v = yz[0] + ((yz[1] - yz[0]) * fz >> 8);
|
||||
sum += (v - 128) >> octave;
|
||||
}
|
||||
return sum; // in [-224, 222], conservatively use +/-224.
|
||||
}
|
||||
static int density(int x, int y, int z) {
|
||||
int d = -10000;
|
||||
for (const Sphere& s : spheres) {
|
||||
int dx=x-s.x, dy=y-s.y, dz=z-s.z;
|
||||
int q=s.r*s.r-dx*dx-dy*dy-dz*dz;
|
||||
d=std::max(d,q);
|
||||
}
|
||||
return d + noise(x,y,z);
|
||||
}
|
||||
static size_t at(int x,int y,int z) { return (size_t(z)*N+y)*N+x; }
|
||||
template<class F> static double run(F&& f) {
|
||||
auto a=std::chrono::steady_clock::now(); f();
|
||||
return std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-a).count();
|
||||
}
|
||||
static void parallel_z(auto fn) {
|
||||
std::array<std::thread,VF_WORKERS> t;
|
||||
for (int i=0;i<VF_WORKERS;++i) t[i]=std::thread([=]{
|
||||
fn((i*NB/VF_WORKERS)*B, ((i+1)*NB/VF_WORKERS)*B);
|
||||
});
|
||||
for (auto& x:t) x.join();
|
||||
}
|
||||
int main() {
|
||||
std::printf("workers=%d\n", VF_WORKERS);
|
||||
for(size_t i=0;i<spheres.size();++i) {
|
||||
uint32_t h=hash3(int(i),17,71,12345);
|
||||
spheres[i]={int((h&255u)),int((h>>8)&255u),int((h>>16)&255u), 12+int((h>>24)%16u)};
|
||||
}
|
||||
for(int trial=0;trial<3;++trial) {
|
||||
double full=run([]{parallel_z([](int lo,int hi){
|
||||
for(int z=lo;z<hi;++z) for(int y=0;y<N;++y) for(int x=0;x<N;++x)
|
||||
field[at(x,y,z)]=int16_t(density(x,y,z));
|
||||
});});
|
||||
uint64_t checksum=0; for(size_t i=0;i<field.size();i+=4093) checksum+=uint16_t(field[i]); sink=checksum;
|
||||
std::printf("full trial=%d ms=%.3f checksum=%llu\n",trial,full,(unsigned long long)checksum);
|
||||
}
|
||||
for(size_t i=0;i<field.size();++i) reference_sign[i]=field[i]>0;
|
||||
// Conservative sphere/brick bounds with a +/-224 noise interval.
|
||||
std::array<uint8_t, size_t(NB)*NB*NB> kind{};
|
||||
size_t solid=0,air=0,mixed=0;
|
||||
double classify=run([&]{
|
||||
for(int bz=0;bz<NB;++bz) for(int by=0;by<NB;++by) for(int bx=0;bx<NB;++bx) {
|
||||
int x0=bx*B,y0=by*B,z0=bz*B,x1=x0+B-1,y1=y0+B-1,z1=z0+B-1;
|
||||
bool possiblyAir=false, definitelyAir=false;
|
||||
for(const Sphere& s:spheres) {
|
||||
int dx=std::max({x0-s.x,0,s.x-x1});
|
||||
int dy=std::max({y0-s.y,0,s.y-y1});
|
||||
int dz=std::max({z0-s.z,0,s.z-z1});
|
||||
int near2=dx*dx+dy*dy+dz*dz;
|
||||
int fx=std::max(std::abs(x0-s.x),std::abs(x1-s.x));
|
||||
int fy=std::max(std::abs(y0-s.y),std::abs(y1-s.y));
|
||||
int fz=std::max(std::abs(z0-s.z),std::abs(z1-s.z));
|
||||
int far2=fx*fx+fy*fy+fz*fz;
|
||||
possiblyAir |= (s.r*s.r-near2+224>=0);
|
||||
definitelyAir |= (s.r*s.r-far2-224>0);
|
||||
}
|
||||
uint8_t k=definitelyAir?2:(possiblyAir?1:0);
|
||||
kind[(size_t(bz)*NB+by)*NB+bx]=k;
|
||||
if(k==0)++solid; else if(k==2)++air; else ++mixed;
|
||||
}
|
||||
});
|
||||
std::printf("classify ms=%.3f solid=%zu air=%zu mixed=%zu total=%zu\n",classify,solid,air,mixed,kind.size());
|
||||
for(int trial=0;trial<3;++trial) {
|
||||
double sparse=run([&]{parallel_z([&](int lo,int hi){
|
||||
for(int bz=lo/B;bz<hi/B;++bz) for(int by=0;by<NB;++by) for(int bx=0;bx<NB;++bx) {
|
||||
uint8_t k=kind[(size_t(bz)*NB+by)*NB+bx];
|
||||
for(int z=bz*B;z<(bz+1)*B;++z) for(int y=by*B;y<(by+1)*B;++y)
|
||||
for(int x=bx*B;x<(bx+1)*B;++x)
|
||||
field[at(x,y,z)]= k==1?int16_t(density(x,y,z)):int16_t(k==2?1:-1);
|
||||
}
|
||||
});});
|
||||
uint64_t checksum=0; for(size_t i=0;i<field.size();i+=4093) checksum+=uint16_t(field[i]); sink=checksum;
|
||||
std::printf("sparse trial=%d ms=%.3f checksum=%llu\n",trial,sparse,(unsigned long long)checksum);
|
||||
}
|
||||
size_t mismatches=0;
|
||||
for(size_t i=0;i<field.size();++i) mismatches+=(reference_sign[i]!=(field[i]>0));
|
||||
std::printf("sign mismatches=%zu of %zu\n",mismatches,field.size());
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
# Analytic intervals and packed signs: one-day spike
|
||||
|
||||
Date: 2026-10-02
|
||||
Result: the near-region interval/bit compiler is promising at this corpus size. The experiment does **not** prove the complete realistic visible world can be built in under one second: the near output is smaller than the proposed 60 MB workload, and the far-field generator is a coarse sheet proxy rather than the production cave hierarchy.
|
||||
|
||||
## What was built
|
||||
|
||||
`interval_spike.cpp` is a standalone C++20 program. It generates the same ordered quadric CSG world for both implementations:
|
||||
|
||||
- A point reference evaluates every one of the 17,373,979 near-region lattice nodes, using a 3D AABB index to select possible features.
|
||||
- The interval path intersects the same fixed-point quadrics along all three 67,081-line axis families, sweeps sorted entry/exit events with a 64-bit membership word, composes the ordered edits, and fills packed sign words.
|
||||
- `OR(8 corners) & ~AND(8 corners)` finds active cells 64 at a time. The mesher joins edge crossings around cell faces, uses an exact center sign for ambiguous faces, and emits indexed position/normal buffers. Near-region vertices come from quantized analytic quadric crossings, not edge midpoints.
|
||||
- Sub-cell strata that have no lattice point inside them receive an explicit analytic surface patch. The patches are filtered against the final composed occupancy.
|
||||
- The near cube has 256³ core cells at 25 cm and one node of halo on each side. Its cell region is split into 64 pages. Edge vertices are shared in one keyed buffer and assigned to a deterministic owner page.
|
||||
- A separate far-field stage builds five stacked parent ellipsoid sheets as indexed patches, without generating children.
|
||||
|
||||
The corpus has **372 features** per seed: 18 rooms, 147 overlapping ellipsoid tunnel segments on winding and vertical connections, 40 pillars, 36 stalactites/stalagmites, 42 shelves, 24 floating masses, 12 craters, 40 crowded-junction edits, and 13 thin strata. It uses the current HyperComplex scale as a guide: 26 m room pitch and roughly 0.28–0.37 pitch radius fractions. Five deterministic seeds were measured.
|
||||
|
||||
The crowded cluster has 40 overlapping operations. Across the whole corpus the largest candidate list on a line was 58 shapes, below the 64-membership limit. The largest primitive-interval list was 52. No tested line overflowed the membership word.
|
||||
|
||||
## Toolchain and numeric contract
|
||||
|
||||
The host reports an AMD Ryzen 7 5800X3D with 16 logical processors. The build used the requested VS 2022 `vcvars64.bat` and `cl /std:c++20 /O2 /arch:AVX2 /EHsc`.
|
||||
|
||||
From the plugin root, the tested build command was:
|
||||
|
||||
```bat
|
||||
call "E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
|
||||
cl /nologo /std:c++20 /O2 /arch:AVX2 /EHsc /Fe:Saved\Research\intervals\interval_spike.exe /Fo:Saved\Research\intervals\interval_spike.obj Saved\Research\intervals\interval_spike.cpp
|
||||
```
|
||||
|
||||
Each benchmark process takes `<seed> <workers>` arguments, for example `interval_spike.exe 1 6`, and writes one JSON record to stdout. The final per-process records are `final_seed1_w1.json` through `final_seed5_w8.json` in this directory.
|
||||
|
||||
Shape transforms are Q15 integer matrices; quadric coefficients are rounded once into Q28 per cm². Occupancy points and interval endpoints use signed 1/16 cm integer coordinates. A shape contains a point when its integer quadratic predicate is `<= 0`; the endpoint inclusion and ordered CSG operation rules are fixed. Line endpoints are found by integer bisection over the canonical coordinate grid, and mesh crossings are the midpoints between the last outside and first inside coordinates.
|
||||
|
||||
The sign paths use integer arithmetic only. With this corpus, local coordinates stay within each feature’s conservative AABB; the largest Q28 coefficient is bounded by the 4 cm minimum radius, and the largest possible quadratic terms stay below 10¹⁸, within signed 64-bit range. That makes the sign decision identical on x64 CPUs implementing the same C++ integer rules. The far parent surface root and explicit thin-patch parameterization use floating point for vertex placement, after the sign/classification decision; cross-vendor bitwise mesh identity was not tested. On this 5800X3D, final geometry hashes matched across worker counts and repeated fresh processes.
|
||||
|
||||
## Timings
|
||||
|
||||
Each row summarizes five fresh processes, one per seed. The p95 uses linear interpolation between ordered samples. The interval stage includes solving/composing all three axes **and writing packed sign words**; active-cell masks are timed separately. Meshing is single-threaded in this spike, so worker counts primarily affect interval generation.
|
||||
|
||||
Times are milliseconds, shown as median / p95 / worst.
|
||||
|
||||
| Stage | 1 worker | 6 workers | 8 workers |
|
||||
|---|---:|---:|---:|
|
||||
| Corpus and spatial hierarchy | 0.5 / 0.6 / 0.6 | 0.5 / 0.7 / 0.7 | 0.5 / 0.7 / 0.8 |
|
||||
| Interval solve, compose, and sign words (3 axes) | 154.2 / 167.8 / 170.8 | 48.2 / 52.6 / 52.9 | 43.2 / 49.0 / 50.1 |
|
||||
| Active-cell masks | 2.1 / 3.4 / 3.7 | 2.1 / 2.3 / 2.3 | 2.1 / 2.2 / 2.2 |
|
||||
| Near mesh emission | 188.4 / 197.4 / 198.7 | 191.2 / 229.3 / 238.4 | 192.8 / 219.8 / 223.9 |
|
||||
| Far-field proxy | 8.6 / 8.7 / 8.8 | 8.6 / 8.6 / 8.6 | 8.6 / 8.9 / 9.0 |
|
||||
| **Near CPU total** | **349.3 / 357.9 / 358.1** | **241.1 / 280.3 / 288.3** | **240.2 / 270.9 / 276.7** |
|
||||
| Near plus far proxy | 357.9 / 366.5 / 366.7 | 249.7 / 288.9 / 296.9 | 248.8 / 279.5 / 285.3 |
|
||||
|
||||
The interval stage fits its 150 ms allocation at 6 and 8 workers, but not at one worker. The near CPU total stays below 430 ms for every measured run. The 6-worker mesh p95 exceeds the proposed 220 ms mesh allocation; the 8-worker p95 is just below it, while its worst run is slightly over.
|
||||
|
||||
## Output and memory
|
||||
|
||||
| Result | Median across five seeds | Range |
|
||||
|---|---:|---:|
|
||||
| Near vertices | 282,404 | 272,704–296,660 |
|
||||
| Near triangles | 563,052 | 543,828–591,512 |
|
||||
| Near mesh buffers | 13,535,700 bytes (13.54 MB) | 13,072,232–14,219,284 bytes |
|
||||
| Near plus far buffers | 19,848,460 bytes (19.85 MB) | 19,384,992–20,532,044 bytes |
|
||||
| Process working set after validation | 45,191,168 bytes (43.1 MiB) | 44,199,936–46,628,864 bytes |
|
||||
|
||||
The proposed 60 MB output workload was not reached. This corpus emitted about 23% of the round-3 estimate of 2.4 million triangles, so the measured mesh time does not establish how a 60 MB result scales.
|
||||
|
||||
## Interval and crossing statistics
|
||||
|
||||
Each axis has 67,081 lines. The table gives the median line p50, the median of the five per-seed line p95 values, and the worst single-line value over all five seeds. The per-seed line p95 uses the sorted index `floor(0.95 * (N - 1))`.
|
||||
|
||||
| Axis | Candidate shapes per line: p50 / p95 / worst | Primitive intervals per line: p50 / p95 / worst | Composed crossings per line: p50 / p95 / worst | Occupancy runs per line: p50 / p95 / worst |
|
||||
|---|---:|---:|---:|---:|
|
||||
| X | 0 / 16 / 44 | 0 / 11 / 40 | 0 / 8 / 20 | 1 / 9 / 21 |
|
||||
| Y | 0 / 17 / 46 | 0 / 12 / 40 | 0 / 10 / 24 | 1 / 9 / 23 |
|
||||
| Z | 1 / 13 / 58 | 0 / 9 / 52 | 0 / 8 / 22 | 1 / 9 / 19 |
|
||||
|
||||
The busiest observed line used 58 candidate slots, leaving six membership bits unused. The long lists are localized; the median line has no candidate shape in X and Y and one in Z. The distributions and maxima were stable across worker counts.
|
||||
|
||||
## Correctness and determinism
|
||||
|
||||
- **Full sign agreement:** all 15 final benchmark processes compared the pointwise reference with all three interval-axis sign paths at every lattice node: zero mismatches.
|
||||
- **Mesh crossings:** zero sign-changing lattice edges lacked an analytic crossing; zero contour-degree faults and zero open cell contours.
|
||||
- **Page ownership:** all 64 near pages passed unique edge-key and owner-page checks. Shared border vertices use one canonical crossing record and quantized position.
|
||||
- **Tangency:** an exact tangent line retained its single-point interval.
|
||||
- **Coincident edits:** coincident carve/add intervals had identical roots, and both operation orders produced their defined last-operation result.
|
||||
- **Negative coordinates:** the full lattice includes negative world X and Z coordinates and passed the point-reference comparison.
|
||||
- **Thin strata:** 13 sub-cell strata were in each corpus. Across the five seeds, 1–10 had no structural lattice point inside; every such feature received explicit surface triangles (288–1,993 triangles per seed). The deliberately isolated 12 cm-thick air seam had no lattice sample and two crossing tags on its central line. The validation gate was also given a deliberately omitted thin surface and rejected it.
|
||||
- **Determinism:** for each of five seeds, the combined geometry hash was identical at 1, 6, and 8 workers. A repeated fresh seed-1/one-worker process also matched. Per-seed hashes: `e5ad62303de5f641`, `17a0d35b9c500445`, `c74544dc1b78ce7b`, `69cef91397829b57`, `afb5e5e8fa54b8a9`.
|
||||
|
||||
## Far-field result and limit
|
||||
|
||||
The proxy selected **47,040 nodes**, **92,150 patches**, **94,090 vertices**, and **184,300 triangles** in **6,312,760 bytes**, with zero descendants. It takes about 8.6 ms. The grid spans ±6 km on each horizontal axis and has five stacked parent layers.
|
||||
|
||||
This is not a proof of the far cave hierarchy. The patches are all on the same 125 m grid; the reported LOD bands are metadata rather than different tessellation levels. The parent shapes are five broad ellipsoid sheets, not the room/tunnel grammar used in the near corpus. The square reaches about 8.5 km at its corners, and no far/near transition surface was built or validated. The production far field is therefore **unproven**.
|
||||
|
||||
## Gate decision
|
||||
|
||||
The near interval/bit core passes the **430 ms timing** portion of the first gate on this 372-feature corpus, and crowded junctions stay below the 64-shape line limit. The output is only 13.1–14.2 MB nearby, however, so the proposed ~60 MB mesh budget was not exercised. The 6-worker mesh p95 also exceeds its 220 ms sub-budget. Thin-feature and shared-boundary checks pass for the tested feature family.
|
||||
|
||||
This is a positive near-core result, not a whole-world pass. Do not treat the under-367 ms near-plus-proxy figure as evidence that the realistic six-kilometre cave world is ready for the Unreal publication/collision test; the far hierarchy and larger output workload remain unproven.
|
||||
|
||||
## Failed and retried builds/runs
|
||||
|
||||
- Build attempt 1 failed because Win32 `min`/`max` macros expanded `std::min`/`std::max` calls. `NOMINMAX` fixed that class of error.
|
||||
- Build attempt 2 then failed on the room-center array dimensions and the Windows `far` macro colliding with a local variable name. Both were corrected; subsequent MSVC builds succeeded.
|
||||
- `vcvars64.bat` printed that `vswhere.exe` was not recognized on each build, but it continued and launched `cl.exe`. The final build succeeded with only unused-parameter and bool-comparison warnings.
|
||||
- The first benchmark launch attempt used PowerShell’s unsupported `Start-Process -PriorityClass` parameter and did not start the executable. The retry launched the process and immediately set its priority to `BelowNormal`.
|
||||
- The first executable run completed but failed validation with 878,033 sign mismatches and no thin crossing tags. The line solver had omitted the shape-center offset when converting the quadratic vertex to an absolute coordinate. After correction, the full sign comparison passed; thin surfaces and explicit contour-integrity assertions were then added.
|
||||
- A final-run preflight initially aborted at a self-imposed 6 GB free-memory threshold; no process started. The machine then had 4.5 GB free while another foreground process used about 12.3 GB. The benchmark working set was about 45 MB, so the final checks were retried below-normal with a 1 GB floor. All three checks passed, followed by all 15 final fresh-process runs.
|
||||
|
||||
All final runs exited 0. No Unreal process was launched, no `Source/` file was modified, and no commit, push, or stash was made.
|
||||
|
||||
## Next step
|
||||
|
||||
Extend the CPU corpus until it emits the intended ~60 MB / 2.4 million triangle workload, with page meshing parallelized deterministically. In parallel, replace the far sheet proxy with actual selected parent room/tunnel/strata shapes, radial horizon clipping, and a shared near/far transition. Re-run the same validation and gates; then decide whether the Unreal publication/collision test is justified.
|
||||
|
||||
## Round 2: bounded blends, QEF mesh, and surface relief
|
||||
|
||||
Date: 2026-10-02. The existing section above records the first-round measurements; this section records the changed field and mesh.
|
||||
|
||||
### What changed
|
||||
|
||||
- The corpus now has **363 shapes**: 18 rooms, 147 anisotropic tunnel segments with changing cross-sections, 40 pillars, 36 stalactite/stalagmite forms, 42 shelves, 24 floating masses, 12 craters, 40 crowded-junction edits, three sparse rock bands, and the isolated audit seam. The 12 repeated thin strata were removed. The three remaining bands have different orientations and are placed at a nearby air lattice node so the interval mesh sees their thin crossings.
|
||||
- Smooth min/max is applied only in bounded overlap regions. At 70 cm blend width, the mask covers a median **3.08% of cell centers** (3.07–3.19% across five seeds) and **1.94% of lattice nodes** (1.94–1.98%). Everywhere else the original exact ordered interval field is used. The banded features keep their hard field so their crossings remain exact.
|
||||
- The near mesh now uses per-cell QEF dual contouring with cached analytic Hermite crossings/normals, face-connectivity component splits, a deterministic ambiguous-face sign, and deterministic page ownership. Page-edge ownership is checked across all 64 pages. The 12 cm audit seam, which no lattice point can see, gets a closed 336-triangle analytic patch.
|
||||
- A deterministic four-band value-noise displacement runs after meshing, with wavelengths **6.4, 3.2, 1.6, and 1.2 m**. The shortest band spans 4.8 voxel cells. The hard displacement bound is **0.60 m**; the five measured seeds reached 0.404–0.439 m. Vertex normals are rebuilt after displacement. No extra volume samples are taken for relief.
|
||||
- `interval_viewer2.exe` renders the blended dual-contour mesh and keeps a separate displaced vertex buffer. Press **R** to switch displacement off/on. The world-space rock color mottling and D3D11 back-buffer PNG capture are part of this standalone viewer. Capture mode closes its own window after writing the screenshots.
|
||||
|
||||
### Timing comparison
|
||||
|
||||
The first-round values below are the medians from the original five-seed report. Round 2 reports the median/p95/worst of five fresh sequential processes per worker count; p95 is linearly interpolated at the 95th percentile. The blend-index row is the AABB overlap raster plus surface-pair filtering. Near total includes the listed stages; far-field time is separate.
|
||||
|
||||
| Stage (ms) | Before medians, 1 / 6 / 8 workers | After, 1 worker med / p95 / worst | After, 6 workers med / p95 / worst | After, 8 workers med / p95 / worst |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Corpus/hierarchy, excluding blend-zone index | 0.5 / 0.5 / 0.5 | 0.96 / 1.03 / 1.04 | 0.96 / 0.97 / 0.97 | 0.96 / 1.08 / 1.10 |
|
||||
| Blend-zone index and mask | — / — / — | 186.68 / 203.10 / 205.61 | 186.07 / 203.27 / 205.51 | 186.97 / 202.43 / 204.94 |
|
||||
| Interval solve, compose, and sign words | 154.2 / 48.2 / 43.2 | 154.88 / 156.72 / 157.12 | 48.89 / 50.47 / 50.65 | 46.10 / 46.97 / 47.16 |
|
||||
| Blend-zone pointwise evaluation and crossings | — / — / — | 203.85 / 225.44 / 229.87 | 200.79 / 205.81 / 205.98 | 201.49 / 208.96 / 209.65 |
|
||||
| Active-cell masks | 2.1 / 2.1 / 2.1 | 2.62 / 3.82 / 3.86 | 2.85 / 4.05 / 4.28 | 3.58 / 4.29 / 4.29 |
|
||||
| Near meshing: old contour emitter / QEF dual contour | 188.4 / 191.2 / 192.8 | 315.62 / 324.11 / 325.76 | 322.14 / 344.69 / 349.24 | 330.38 / 331.55 / 331.57 |
|
||||
| Surface displacement and normal rebuild | — / — / — | 61.43 / 62.29 / 62.45 | 61.04 / 61.80 / 61.91 | 61.26 / 63.41 / 63.69 |
|
||||
| **Near CPU total** | **349.3 / 241.1 / 240.2** | **931.93 / 957.68 / 959.08** | **836.06 / 848.28 / 849.57** | **824.27 / 854.37 / 861.33** |
|
||||
| Far-field proxy | 8.6 / 8.6 / 8.6 | 8.59 / 8.61 / 8.62 | 8.57 / 9.51 / 9.58 | 8.58 / 8.60 / 8.61 |
|
||||
| **Near plus far** | **357.9 / 249.7 / 248.8** | **940.54 / 966.28 / 967.68** | **844.62 / 857.37 / 858.80** | **832.85 / 862.93 / 869.89** |
|
||||
|
||||
That is a **2.67× / 3.47× / 3.43×** near-total cost against round 1 at 1/6/8 workers. Most of the new cost is visible in the table: roughly 187–190 ms to build the overlap mask, 201–204 ms to evaluate blended nodes/rebuild crossings, 316–330 ms for dual contouring, and about 61 ms for relief plus normal rebuild. The 6/8-worker interval speedup remains, but the zone work and mesher are single-threaded here.
|
||||
|
||||
The median output across the five seeds is **265,508 near vertices**, **531,164 triangles**, and **12,747,440 near-buffer bytes**. Median working sets were 69.6 / 70.0 / 70.4 MB at 1 / 6 / 8 workers. The proposed 60 MB mesh workload is still not exercised.
|
||||
|
||||
### Correctness, topology, and repeatability
|
||||
|
||||
- All 15 final processes (seeds 1–5 at 1, 6, and 8 workers) exited 0. Each compared the interval/blend signs against the new pointwise blended-field reference at every lattice node: **zero mismatches** and zero blend-boundary sign disagreements. All page ownership checks passed, with zero missing analytic crossings, zero cell-contour faults, and zero open cell contours.
|
||||
- The three lattice-snapped bands are present in the structural mesh. The 12 cm audit seam has no lattice sample and is retained by its closed 336-triangle patch. The thin-surface omission check still rejects a deliberately omitted patch.
|
||||
- The edge-manifold diagnostic reports **zero boundary edges** on all five seeds, but **3–7 overused edge groups** remain. The JSON `non_manifold_edges_zero` field is therefore false. These are localized dual-contour singularities around ambiguous/multiple-crossing cells; they are visible in the diagnostics and were not waived as a zero count. The page-ownership/sign/thin-feature validation result is reported separately from this remaining topology count.
|
||||
- The QEF clamp counter is 141,729–155,046 across seeds. It increments per clamped coordinate axis, not per unique vertex. There are also 1,654–2,035 retained near-zero-area triangles; keeping them preserves edge connectivity after QEF clamping. These facts, plus the remaining overused edges, mean the mesher still needs topology work before treating the output as production collision geometry.
|
||||
- For each seed, raw mesh, displaced mesh, and combined hashes match at all three worker counts. A fresh repeated seed-1/one-worker process also matched: mesh `20d4d80f439c17f6`, displaced mesh `0592d64cb4f269ca`, combined `fe846e12af9fef67`. Per-seed combined hashes: `fe846e12af9fef67`, `8f0994fe2295a34e`, `ee13abc933f14a3f`, `129dcc0c9bac3b3e`, `66361d9daff2b1a6`.
|
||||
|
||||
### Screenshots and visual judgement
|
||||
|
||||
These PNGs were captured from the viewer’s own D3D11 swap-chain back buffer, at 1280×800:
|
||||
|
||||
| View | Capture |
|
||||
|---|---|
|
||||
| Inside the first chamber, relief on | [`chamber.png`](views/chamber.png) |
|
||||
| Same camera, relief off | [`chamber_relief_off.png`](views/chamber_relief_off.png) |
|
||||
| Open point in the crowded junction | [`junction.png`](views/junction.png) |
|
||||
| Close to the chamber wall | [`wall.png`](views/wall.png) |
|
||||
|
||||
Compared with the reject image, the repeated thin slab stack is gone, the color has rock-like variation, and many primitive junctions read as rounded transitions. The relief-on chamber is visibly less smooth than the relief-off view. The result is still well short of the reference cave: the close-wall view shows an obvious rounded ellipsoid, the chamber has broad flat-looking facets, and the material lacks the target’s rich rock texture, crystals, and water. I would describe this as a measurable improvement over the rejection, not a finished natural-rock look.
|
||||
|
||||
### Failed and retried Round 2 attempts
|
||||
|
||||
- Two early compile invocations passed the `vcvars64.bat` path with broken shell quoting, so `cl.exe` never started. An intermediate C++ build also failed because `near` was used as an identifier; it was renamed. The first D3D11 viewer link failed with unresolved `CoInitializeEx`/`CoUninitialize`/`CoCreateInstance`; linking `ole32.lib` fixed it. A later corpus-snap build failed once because `eval_quad` was used before declaration; a forward declaration fixed it. The corrected builds succeeded. MSVC continued to print that `vswhere.exe` was unavailable from `vcvars64.bat`, but it still launched `cl.exe`.
|
||||
- `round2_probe3` through `round2_probe12` were intermediate dual-contour runs that exited on validation failures while the Hermite cache, component topology, degenerate triangles, and thin patches were being corrected. At probe 12, missing crossings had reached zero, but the mesh still reported about 3.7k non-manifold edges. Skipping the zero-area triangles produced thousands of boundary edges; a one-QEF-vertex-per-cell experiment made the overused-edge count worse (798), so both approaches were reverted. The final five-seed check has zero boundary edges and 3–7 overused groups.
|
||||
- The first two D3D capture runs failed at WIC pixel-format selection (`0x88982f80`, unsupported RGBA); they left zero-byte PNGs. The capture path now swaps the D3D11 RGBA readback into WIC BGRA and writes all four non-empty images above. The capture-mode viewer closed itself; no previously open viewer was touched.
|
||||
- MSVC continued to print that `vswhere.exe` was unavailable from `vcvars64.bat`, but all final builds completed. All 15 final benchmark runs and the repeated determinism run exited 0. No Unreal build or launch was done, no plugin `Source/` file changed, and nothing was committed, pushed, or stashed.
|
||||
|
||||
### Round 2 conclusion
|
||||
|
||||
Smooth joins and broad relief improve the rejected world, with only about 3.1% of cell centers in blend zones. The price is substantial: the near build rises from 241 ms to 836 ms at six workers and from 240 ms to 824 ms at eight workers. QEF clamping and the remaining 3–7 overused edges also keep this from being production-ready geometry. The viewer and measurements are ready for owner review; the screenshots show honestly that more corpus shaping/material work is needed to approach the reference.
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":1,"workers":1,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.497,"interval_solve_compose_and_sign_words_3_axes":153.430,"active_cell_masks":1.978,"near_mesh_emission":172.709,"far_field":8.577,"validation_outside_timing":242.817,"near_cpu_total":328.614,"near_plus_far_total":337.191},"near":{"active_cells":272986,"vertices":275646,"triangles":549585,"bytes":13211864,"hash":"ff39dc05fa0b59e0","pages":64,"index_ranges":67,"page_border_vertices":14777,"ambiguous_faces":3870,"multi_cross_edges":416,"thin_detected":13,"thin_hidden_lattice":3,"thin_meshed":3,"explicit_thin_triangles":639},"interval_stats":{"x":{"lines":67081,"candidate_total":181692,"candidate_p50":0,"candidate_p95":16,"candidate_max":44,"interval_total":110023,"interval_p50":0,"interval_p95":11,"interval_max":40,"crossing_total":88770,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":21},"y":{"lines":67081,"candidate_total":193718,"candidate_p50":0,"candidate_p95":18,"candidate_max":45,"interval_total":111420,"interval_p50":0,"interval_p95":12,"interval_max":40,"crossing_total":91616,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19},"z":{"lines":67081,"candidate_total":206488,"candidate_p50":0,"candidate_p95":13,"candidate_max":58,"interval_total":120012,"interval_p50":0,"interval_p95":9,"interval_max":52,"crossing_total":109308,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"ab4a669002ce586d"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":3,"audit_thin_crossing_events":2},"output_bytes_total":19524624,"combined_hash":"e5ad62303de5f641","working_set_bytes":44474368,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":1,"workers":1,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.493,"interval_solve_compose_and_sign_words_3_axes":151.928,"active_cell_masks":2.082,"near_mesh_emission":178.676,"far_field":8.558,"validation_outside_timing":241.893,"near_cpu_total":333.179,"near_plus_far_total":341.737},"near":{"active_cells":272986,"vertices":275646,"triangles":549585,"bytes":13211864,"hash":"ff39dc05fa0b59e0","pages":64,"index_ranges":67,"page_border_vertices":14777,"ambiguous_faces":3870,"multi_cross_edges":416,"thin_detected":13,"thin_hidden_lattice":3,"thin_meshed":3,"explicit_thin_triangles":639},"interval_stats":{"x":{"lines":67081,"candidate_total":181692,"candidate_p50":0,"candidate_p95":16,"candidate_max":44,"interval_total":110023,"interval_p50":0,"interval_p95":11,"interval_max":40,"crossing_total":88770,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":21},"y":{"lines":67081,"candidate_total":193718,"candidate_p50":0,"candidate_p95":18,"candidate_max":45,"interval_total":111420,"interval_p50":0,"interval_p95":12,"interval_max":40,"crossing_total":91616,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19},"z":{"lines":67081,"candidate_total":206488,"candidate_p50":0,"candidate_p95":13,"candidate_max":58,"interval_total":120012,"interval_p50":0,"interval_p95":9,"interval_max":52,"crossing_total":109308,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"ab4a669002ce586d"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":3,"audit_thin_crossing_events":2},"output_bytes_total":19524624,"combined_hash":"e5ad62303de5f641","working_set_bytes":44490752,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":1,"workers":6,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.725,"interval_solve_compose_and_sign_words_3_axes":72.450,"active_cell_masks":3.939,"near_mesh_emission":254.653,"far_field":8.601,"validation_outside_timing":320.588,"near_cpu_total":331.767,"near_plus_far_total":340.367},"near":{"active_cells":272986,"vertices":275646,"triangles":549585,"bytes":13211864,"hash":"ff39dc05fa0b59e0","pages":64,"index_ranges":67,"page_border_vertices":14777,"ambiguous_faces":3870,"multi_cross_edges":416,"thin_detected":13,"thin_hidden_lattice":3,"thin_meshed":3,"explicit_thin_triangles":639},"interval_stats":{"x":{"lines":67081,"candidate_total":181692,"candidate_p50":0,"candidate_p95":16,"candidate_max":44,"interval_total":110023,"interval_p50":0,"interval_p95":11,"interval_max":40,"crossing_total":88770,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":21},"y":{"lines":67081,"candidate_total":193718,"candidate_p50":0,"candidate_p95":18,"candidate_max":45,"interval_total":111420,"interval_p50":0,"interval_p95":12,"interval_max":40,"crossing_total":91616,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19},"z":{"lines":67081,"candidate_total":206488,"candidate_p50":0,"candidate_p95":13,"candidate_max":58,"interval_total":120012,"interval_p50":0,"interval_p95":9,"interval_max":52,"crossing_total":109308,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"ab4a669002ce586d"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":3,"audit_thin_crossing_events":2},"output_bytes_total":19524624,"combined_hash":"e5ad62303de5f641","working_set_bytes":44904448,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":1,"workers":8,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.501,"interval_solve_compose_and_sign_words_3_axes":49.895,"active_cell_masks":2.151,"near_mesh_emission":183.250,"far_field":9.053,"validation_outside_timing":243.429,"near_cpu_total":235.797,"near_plus_far_total":244.850},"near":{"active_cells":272986,"vertices":275646,"triangles":549585,"bytes":13211864,"hash":"ff39dc05fa0b59e0","pages":64,"index_ranges":67,"page_border_vertices":14777,"ambiguous_faces":3870,"multi_cross_edges":416,"thin_detected":13,"thin_hidden_lattice":3,"thin_meshed":3,"explicit_thin_triangles":639},"interval_stats":{"x":{"lines":67081,"candidate_total":181692,"candidate_p50":0,"candidate_p95":16,"candidate_max":44,"interval_total":110023,"interval_p50":0,"interval_p95":11,"interval_max":40,"crossing_total":88770,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":21},"y":{"lines":67081,"candidate_total":193718,"candidate_p50":0,"candidate_p95":18,"candidate_max":45,"interval_total":111420,"interval_p50":0,"interval_p95":12,"interval_max":40,"crossing_total":91616,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19},"z":{"lines":67081,"candidate_total":206488,"candidate_p50":0,"candidate_p95":13,"candidate_max":58,"interval_total":120012,"interval_p50":0,"interval_p95":9,"interval_max":52,"crossing_total":109308,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"ab4a669002ce586d"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":3,"audit_thin_crossing_events":2},"output_bytes_total":19524624,"combined_hash":"e5ad62303de5f641","working_set_bytes":45068288,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":2,"workers":1,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.505,"interval_solve_compose_and_sign_words_3_axes":165.156,"active_cell_masks":2.084,"near_mesh_emission":184.966,"far_field":8.577,"validation_outside_timing":246.244,"near_cpu_total":352.711,"near_plus_far_total":361.288},"near":{"active_cells":283161,"vertices":286734,"triangles":569817,"bytes":13720900,"hash":"dab38d009700f1f0","pages":64,"index_ranges":74,"page_border_vertices":15582,"ambiguous_faces":2854,"multi_cross_edges":536,"thin_detected":13,"thin_hidden_lattice":10,"thin_meshed":10,"explicit_thin_triangles":1993},"interval_stats":{"x":{"lines":67081,"candidate_total":187058,"candidate_p50":0,"candidate_p95":17,"candidate_max":43,"interval_total":111999,"interval_p50":0,"interval_p95":11,"interval_max":40,"crossing_total":89658,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":21},"y":{"lines":67081,"candidate_total":199365,"candidate_p50":0,"candidate_p95":18,"candidate_max":44,"interval_total":120586,"interval_p50":0,"interval_p95":12,"interval_max":39,"crossing_total":104152,"crossing_p50":0,"crossing_p95":10,"crossing_max":22,"runs_p50":1,"runs_p95":11,"runs_max":19},"z":{"lines":67081,"candidate_total":204987,"candidate_p50":1,"candidate_p95":13,"candidate_max":56,"interval_total":124121,"interval_p50":0,"interval_p95":10,"interval_max":48,"crossing_total":112170,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"0293ad0bb4aab1f8"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":10,"audit_thin_crossing_events":2},"output_bytes_total":20033660,"combined_hash":"17a0d35b9c500445","working_set_bytes":45158400,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":2,"workers":6,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.499,"interval_solve_compose_and_sign_words_3_axes":51.718,"active_cell_masks":2.512,"near_mesh_emission":191.036,"far_field":8.609,"validation_outside_timing":269.096,"near_cpu_total":245.766,"near_plus_far_total":254.375},"near":{"active_cells":283161,"vertices":286734,"triangles":569817,"bytes":13720900,"hash":"dab38d009700f1f0","pages":64,"index_ranges":74,"page_border_vertices":15582,"ambiguous_faces":2854,"multi_cross_edges":536,"thin_detected":13,"thin_hidden_lattice":10,"thin_meshed":10,"explicit_thin_triangles":1993},"interval_stats":{"x":{"lines":67081,"candidate_total":187058,"candidate_p50":0,"candidate_p95":17,"candidate_max":43,"interval_total":111999,"interval_p50":0,"interval_p95":11,"interval_max":40,"crossing_total":89658,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":21},"y":{"lines":67081,"candidate_total":199365,"candidate_p50":0,"candidate_p95":18,"candidate_max":44,"interval_total":120586,"interval_p50":0,"interval_p95":12,"interval_max":39,"crossing_total":104152,"crossing_p50":0,"crossing_p95":10,"crossing_max":22,"runs_p50":1,"runs_p95":11,"runs_max":19},"z":{"lines":67081,"candidate_total":204987,"candidate_p50":1,"candidate_p95":13,"candidate_max":56,"interval_total":124121,"interval_p50":0,"interval_p95":10,"interval_max":48,"crossing_total":112170,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"0293ad0bb4aab1f8"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":10,"audit_thin_crossing_events":2},"output_bytes_total":20033660,"combined_hash":"17a0d35b9c500445","working_set_bytes":45715456,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":2,"workers":8,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.496,"interval_solve_compose_and_sign_words_3_axes":52.960,"active_cell_masks":2.198,"near_mesh_emission":191.148,"far_field":8.569,"validation_outside_timing":246.765,"near_cpu_total":246.802,"near_plus_far_total":255.372},"near":{"active_cells":283161,"vertices":286734,"triangles":569817,"bytes":13720900,"hash":"dab38d009700f1f0","pages":64,"index_ranges":74,"page_border_vertices":15582,"ambiguous_faces":2854,"multi_cross_edges":536,"thin_detected":13,"thin_hidden_lattice":10,"thin_meshed":10,"explicit_thin_triangles":1993},"interval_stats":{"x":{"lines":67081,"candidate_total":187058,"candidate_p50":0,"candidate_p95":17,"candidate_max":43,"interval_total":111999,"interval_p50":0,"interval_p95":11,"interval_max":40,"crossing_total":89658,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":21},"y":{"lines":67081,"candidate_total":199365,"candidate_p50":0,"candidate_p95":18,"candidate_max":44,"interval_total":120586,"interval_p50":0,"interval_p95":12,"interval_max":39,"crossing_total":104152,"crossing_p50":0,"crossing_p95":10,"crossing_max":22,"runs_p50":1,"runs_p95":11,"runs_max":19},"z":{"lines":67081,"candidate_total":204987,"candidate_p50":1,"candidate_p95":13,"candidate_max":56,"interval_total":124121,"interval_p50":0,"interval_p95":10,"interval_max":48,"crossing_total":112170,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"0293ad0bb4aab1f8"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":10,"audit_thin_crossing_events":2},"output_bytes_total":20033660,"combined_hash":"17a0d35b9c500445","working_set_bytes":45731840,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":3,"workers":1,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.508,"interval_solve_compose_and_sign_words_3_axes":155.158,"active_cell_masks":3.366,"near_mesh_emission":198.934,"far_field":8.589,"validation_outside_timing":264.253,"near_cpu_total":357.966,"near_plus_far_total":366.555},"near":{"active_cells":292580,"vertices":296660,"triangles":591512,"bytes":14219284,"hash":"600f405efd49ce29","pages":64,"index_ranges":65,"page_border_vertices":15757,"ambiguous_faces":7772,"multi_cross_edges":264,"thin_detected":13,"thin_hidden_lattice":1,"thin_meshed":1,"explicit_thin_triangles":288},"interval_stats":{"x":{"lines":67081,"candidate_total":190357,"candidate_p50":0,"candidate_p95":16,"candidate_max":42,"interval_total":112011,"interval_p50":0,"interval_p95":11,"interval_max":37,"crossing_total":95212,"crossing_p50":0,"crossing_p95":10,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":21},"y":{"lines":67081,"candidate_total":194781,"candidate_p50":0,"candidate_p95":17,"candidate_max":46,"interval_total":112410,"interval_p50":0,"interval_p95":12,"interval_max":40,"crossing_total":93860,"crossing_p50":0,"crossing_p95":10,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19},"z":{"lines":67081,"candidate_total":215639,"candidate_p50":1,"candidate_p95":13,"candidate_max":57,"interval_total":124874,"interval_p50":0,"interval_p95":9,"interval_max":49,"crossing_total":118734,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"dbc331a49aa1719e"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":1,"audit_thin_crossing_events":2},"output_bytes_total":20532044,"combined_hash":"c74544dc1b78ce7b","working_set_bytes":45920256,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":3,"workers":6,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.518,"interval_solve_compose_and_sign_words_3_axes":51.474,"active_cell_masks":2.204,"near_mesh_emission":203.409,"far_field":8.579,"validation_outside_timing":247.255,"near_cpu_total":257.606,"near_plus_far_total":266.185},"near":{"active_cells":292580,"vertices":296660,"triangles":591512,"bytes":14219284,"hash":"600f405efd49ce29","pages":64,"index_ranges":65,"page_border_vertices":15757,"ambiguous_faces":7772,"multi_cross_edges":264,"thin_detected":13,"thin_hidden_lattice":1,"thin_meshed":1,"explicit_thin_triangles":288},"interval_stats":{"x":{"lines":67081,"candidate_total":190357,"candidate_p50":0,"candidate_p95":16,"candidate_max":42,"interval_total":112011,"interval_p50":0,"interval_p95":11,"interval_max":37,"crossing_total":95212,"crossing_p50":0,"crossing_p95":10,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":21},"y":{"lines":67081,"candidate_total":194781,"candidate_p50":0,"candidate_p95":17,"candidate_max":46,"interval_total":112410,"interval_p50":0,"interval_p95":12,"interval_max":40,"crossing_total":93860,"crossing_p50":0,"crossing_p95":10,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19},"z":{"lines":67081,"candidate_total":215639,"candidate_p50":1,"candidate_p95":13,"candidate_max":57,"interval_total":124874,"interval_p50":0,"interval_p95":9,"interval_max":49,"crossing_total":118734,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"dbc331a49aa1719e"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":1,"audit_thin_crossing_events":2},"output_bytes_total":20532044,"combined_hash":"c74544dc1b78ce7b","working_set_bytes":46493696,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":3,"workers":8,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.499,"interval_solve_compose_and_sign_words_3_axes":43.016,"active_cell_masks":2.446,"near_mesh_emission":240.567,"far_field":8.808,"validation_outside_timing":267.854,"near_cpu_total":286.528,"near_plus_far_total":295.336},"near":{"active_cells":292580,"vertices":296660,"triangles":591512,"bytes":14219284,"hash":"600f405efd49ce29","pages":64,"index_ranges":65,"page_border_vertices":15757,"ambiguous_faces":7772,"multi_cross_edges":264,"thin_detected":13,"thin_hidden_lattice":1,"thin_meshed":1,"explicit_thin_triangles":288},"interval_stats":{"x":{"lines":67081,"candidate_total":190357,"candidate_p50":0,"candidate_p95":16,"candidate_max":42,"interval_total":112011,"interval_p50":0,"interval_p95":11,"interval_max":37,"crossing_total":95212,"crossing_p50":0,"crossing_p95":10,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":21},"y":{"lines":67081,"candidate_total":194781,"candidate_p50":0,"candidate_p95":17,"candidate_max":46,"interval_total":112410,"interval_p50":0,"interval_p95":12,"interval_max":40,"crossing_total":93860,"crossing_p50":0,"crossing_p95":10,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19},"z":{"lines":67081,"candidate_total":215639,"candidate_p50":1,"candidate_p95":13,"candidate_max":57,"interval_total":124874,"interval_p50":0,"interval_p95":9,"interval_max":49,"crossing_total":118734,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"dbc331a49aa1719e"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":1,"audit_thin_crossing_events":2},"output_bytes_total":20532044,"combined_hash":"c74544dc1b78ce7b","working_set_bytes":46301184,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":4,"workers":1,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.485,"interval_solve_compose_and_sign_words_3_axes":153.462,"active_cell_masks":2.555,"near_mesh_emission":189.354,"far_field":8.573,"validation_outside_timing":242.167,"near_cpu_total":345.856,"near_plus_far_total":354.429},"near":{"active_cells":279309,"vertices":282404,"triangles":563052,"bytes":13535700,"hash":"4ed64f01c5df738b","pages":64,"index_ranges":69,"page_border_vertices":13975,"ambiguous_faces":3838,"multi_cross_edges":680,"thin_detected":13,"thin_hidden_lattice":5,"thin_meshed":5,"explicit_thin_triangles":1120},"interval_stats":{"x":{"lines":67081,"candidate_total":181609,"candidate_p50":0,"candidate_p95":16,"candidate_max":41,"interval_total":109240,"interval_p50":0,"interval_p95":11,"interval_max":37,"crossing_total":88640,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":17},"y":{"lines":67081,"candidate_total":191723,"candidate_p50":0,"candidate_p95":17,"candidate_max":42,"interval_total":114274,"interval_p50":0,"interval_p95":12,"interval_max":39,"crossing_total":98854,"crossing_p50":0,"crossing_p95":10,"crossing_max":24,"runs_p50":1,"runs_p95":9,"runs_max":23},"z":{"lines":67081,"candidate_total":201505,"candidate_p50":0,"candidate_p95":13,"candidate_max":56,"interval_total":124143,"interval_p50":0,"interval_p95":10,"interval_max":50,"crossing_total":113480,"crossing_p50":0,"crossing_p95":8,"crossing_max":22,"runs_p50":1,"runs_p95":9,"runs_max":17}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"75f729c8e51da37e"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":5,"audit_thin_crossing_events":2},"output_bytes_total":19848460,"combined_hash":"69cef91397829b57","working_set_bytes":45002752,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":4,"workers":6,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.478,"interval_solve_compose_and_sign_words_3_axes":51.348,"active_cell_masks":2.415,"near_mesh_emission":187.195,"far_field":8.576,"validation_outside_timing":243.163,"near_cpu_total":241.436,"near_plus_far_total":250.012},"near":{"active_cells":279309,"vertices":282404,"triangles":563052,"bytes":13535700,"hash":"4ed64f01c5df738b","pages":64,"index_ranges":69,"page_border_vertices":13975,"ambiguous_faces":3838,"multi_cross_edges":680,"thin_detected":13,"thin_hidden_lattice":5,"thin_meshed":5,"explicit_thin_triangles":1120},"interval_stats":{"x":{"lines":67081,"candidate_total":181609,"candidate_p50":0,"candidate_p95":16,"candidate_max":41,"interval_total":109240,"interval_p50":0,"interval_p95":11,"interval_max":37,"crossing_total":88640,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":17},"y":{"lines":67081,"candidate_total":191723,"candidate_p50":0,"candidate_p95":17,"candidate_max":42,"interval_total":114274,"interval_p50":0,"interval_p95":12,"interval_max":39,"crossing_total":98854,"crossing_p50":0,"crossing_p95":10,"crossing_max":24,"runs_p50":1,"runs_p95":9,"runs_max":23},"z":{"lines":67081,"candidate_total":201505,"candidate_p50":0,"candidate_p95":13,"candidate_max":56,"interval_total":124143,"interval_p50":0,"interval_p95":10,"interval_max":50,"crossing_total":113480,"crossing_p50":0,"crossing_p95":8,"crossing_max":22,"runs_p50":1,"runs_p95":9,"runs_max":17}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"75f729c8e51da37e"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":5,"audit_thin_crossing_events":2},"output_bytes_total":19848460,"combined_hash":"69cef91397829b57","working_set_bytes":45342720,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":4,"workers":8,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.485,"interval_solve_compose_and_sign_words_3_axes":40.148,"active_cell_masks":2.266,"near_mesh_emission":188.109,"far_field":8.564,"validation_outside_timing":242.086,"near_cpu_total":231.009,"near_plus_far_total":239.573},"near":{"active_cells":279309,"vertices":282404,"triangles":563052,"bytes":13535700,"hash":"4ed64f01c5df738b","pages":64,"index_ranges":69,"page_border_vertices":13975,"ambiguous_faces":3838,"multi_cross_edges":680,"thin_detected":13,"thin_hidden_lattice":5,"thin_meshed":5,"explicit_thin_triangles":1120},"interval_stats":{"x":{"lines":67081,"candidate_total":181609,"candidate_p50":0,"candidate_p95":16,"candidate_max":41,"interval_total":109240,"interval_p50":0,"interval_p95":11,"interval_max":37,"crossing_total":88640,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":17},"y":{"lines":67081,"candidate_total":191723,"candidate_p50":0,"candidate_p95":17,"candidate_max":42,"interval_total":114274,"interval_p50":0,"interval_p95":12,"interval_max":39,"crossing_total":98854,"crossing_p50":0,"crossing_p95":10,"crossing_max":24,"runs_p50":1,"runs_p95":9,"runs_max":23},"z":{"lines":67081,"candidate_total":201505,"candidate_p50":0,"candidate_p95":13,"candidate_max":56,"interval_total":124143,"interval_p50":0,"interval_p95":10,"interval_max":50,"crossing_total":113480,"crossing_p50":0,"crossing_p95":8,"crossing_max":22,"runs_p50":1,"runs_p95":9,"runs_max":17}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"75f729c8e51da37e"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":5,"audit_thin_crossing_events":2},"output_bytes_total":19848460,"combined_hash":"69cef91397829b57","working_set_bytes":45486080,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":5,"workers":1,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.470,"interval_solve_compose_and_sign_words_3_axes":172.633,"active_cell_masks":3.120,"near_mesh_emission":184.374,"far_field":8.605,"validation_outside_timing":244.536,"near_cpu_total":360.596,"near_plus_far_total":369.201},"near":{"active_cells":269847,"vertices":272704,"triangles":543828,"bytes":13072232,"hash":"885750d27c475ba9","pages":64,"index_ranges":70,"page_border_vertices":12496,"ambiguous_faces":2638,"multi_cross_edges":976,"thin_detected":13,"thin_hidden_lattice":6,"thin_meshed":6,"explicit_thin_triangles":1282},"interval_stats":{"x":{"lines":67081,"candidate_total":186280,"candidate_p50":0,"candidate_p95":17,"candidate_max":44,"interval_total":111929,"interval_p50":0,"interval_p95":11,"interval_max":38,"crossing_total":89242,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19},"y":{"lines":67081,"candidate_total":192484,"candidate_p50":0,"candidate_p95":17,"candidate_max":44,"interval_total":111223,"interval_p50":0,"interval_p95":12,"interval_max":36,"crossing_total":90196,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19},"z":{"lines":67081,"candidate_total":214972,"candidate_p50":1,"candidate_p95":13,"candidate_max":56,"interval_total":122709,"interval_p50":0,"interval_p95":9,"interval_max":49,"crossing_total":111168,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"c100350769615273"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":6,"audit_thin_crossing_events":2},"output_bytes_total":19384992,"combined_hash":"afb5e5e8fa54b8a9","working_set_bytes":44163072,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":5,"workers":6,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.468,"interval_solve_compose_and_sign_words_3_axes":52.449,"active_cell_masks":2.217,"near_mesh_emission":184.168,"far_field":8.708,"validation_outside_timing":248.390,"near_cpu_total":239.302,"near_plus_far_total":248.010},"near":{"active_cells":269847,"vertices":272704,"triangles":543828,"bytes":13072232,"hash":"885750d27c475ba9","pages":64,"index_ranges":70,"page_border_vertices":12496,"ambiguous_faces":2638,"multi_cross_edges":976,"thin_detected":13,"thin_hidden_lattice":6,"thin_meshed":6,"explicit_thin_triangles":1282},"interval_stats":{"x":{"lines":67081,"candidate_total":186280,"candidate_p50":0,"candidate_p95":17,"candidate_max":44,"interval_total":111929,"interval_p50":0,"interval_p95":11,"interval_max":38,"crossing_total":89242,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19},"y":{"lines":67081,"candidate_total":192484,"candidate_p50":0,"candidate_p95":17,"candidate_max":44,"interval_total":111223,"interval_p50":0,"interval_p95":12,"interval_max":36,"crossing_total":90196,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19},"z":{"lines":67081,"candidate_total":214972,"candidate_p50":1,"candidate_p95":13,"candidate_max":56,"interval_total":122709,"interval_p50":0,"interval_p95":9,"interval_max":49,"crossing_total":111168,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"c100350769615273"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":6,"audit_thin_crossing_events":2},"output_bytes_total":19384992,"combined_hash":"afb5e5e8fa54b8a9","working_set_bytes":44736512,"validation_pass":true}
|
||||
@@ -0,0 +1 @@
|
||||
{"seed":5,"workers":8,"shape_count":372,"near_cells":16777216,"near_nodes":17373979,"membership_limit":64,"overflow":false,"ms":{"corpus_hierarchy":0.471,"interval_solve_compose_and_sign_words_3_axes":43.064,"active_cell_masks":1.906,"near_mesh_emission":195.702,"far_field":8.607,"validation_outside_timing":264.699,"near_cpu_total":241.142,"near_plus_far_total":249.750},"near":{"active_cells":269847,"vertices":272704,"triangles":543828,"bytes":13072232,"hash":"885750d27c475ba9","pages":64,"index_ranges":70,"page_border_vertices":12496,"ambiguous_faces":2638,"multi_cross_edges":976,"thin_detected":13,"thin_hidden_lattice":6,"thin_meshed":6,"explicit_thin_triangles":1282},"interval_stats":{"x":{"lines":67081,"candidate_total":186280,"candidate_p50":0,"candidate_p95":17,"candidate_max":44,"interval_total":111929,"interval_p50":0,"interval_p95":11,"interval_max":38,"crossing_total":89242,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19},"y":{"lines":67081,"candidate_total":192484,"candidate_p50":0,"candidate_p95":17,"candidate_max":44,"interval_total":111223,"interval_p50":0,"interval_p95":12,"interval_max":36,"crossing_total":90196,"crossing_p50":0,"crossing_p95":8,"crossing_max":20,"runs_p50":1,"runs_p95":9,"runs_max":19},"z":{"lines":67081,"candidate_total":214972,"candidate_p50":1,"candidate_p95":13,"candidate_max":56,"interval_total":122709,"interval_p50":0,"interval_p95":9,"interval_max":49,"crossing_total":111168,"crossing_p50":0,"crossing_p95":8,"crossing_max":18,"runs_p50":1,"runs_p95":9,"runs_max":19}},"far":{"nodes":47040,"patches":92150,"vertices":94090,"triangles":184300,"bytes":6312760,"descendants":0,"lod_nodes":[240,880,3680,15040,11360,15840],"hash":"c100350769615273"},"validation":{"signs_ok":true,"mismatches":0,"negative_coordinates":true,"page_ownership_and_shared_edges":true,"tangency":true,"coincident_edits":true,"thin_surface_present":true,"omit_thin_surface_rejected":true,"thin_hidden_in_lattice":6,"audit_thin_crossing_events":2},"output_bytes_total":19384992,"combined_hash":"afb5e5e8fa54b8a9","working_set_bytes":44937216,"validation_pass":true}
|
||||
@@ -0,0 +1,138 @@
|
||||
#define main interval_spike_benchmark_main
|
||||
#include "../interval_spike.cpp"
|
||||
#undef main
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <d3dcompiler.h>
|
||||
#include <dxgi.h>
|
||||
#include <wincodec.h>
|
||||
#include <windowsx.h>
|
||||
#include <shellapi.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#pragma comment(lib, "d3d11.lib")
|
||||
#pragma comment(lib, "dxgi.lib")
|
||||
#pragma comment(lib, "d3dcompiler.lib")
|
||||
#pragma comment(lib, "shell32.lib")
|
||||
#pragma comment(lib, "user32.lib")
|
||||
#pragma comment(lib, "windowscodecs.lib")
|
||||
#pragma comment(lib, "ole32.lib")
|
||||
|
||||
namespace {
|
||||
struct V3 { float x,y,z; };
|
||||
V3 add(V3 a,V3 b){return {a.x+b.x,a.y+b.y,a.z+b.z};}
|
||||
V3 sub(V3 a,V3 b){return {a.x-b.x,a.y-b.y,a.z-b.z};}
|
||||
V3 mul(V3 a,float s){return {a.x*s,a.y*s,a.z*s};}
|
||||
float dot(V3 a,V3 b){return a.x*b.x+a.y*b.y+a.z*b.z;}
|
||||
V3 cross(V3 a,V3 b){return {a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x};}
|
||||
V3 norm(V3 a){float n=std::sqrt(std::max(1e-20f,dot(a,a)));return mul(a,1.0f/n);}
|
||||
struct M4 { float m[4][4]{}; };
|
||||
M4 identity(){M4 a{};for(int i=0;i<4;++i)a.m[i][i]=1;return a;}
|
||||
M4 perspective(float fov,float aspect,float zn,float zf){M4 a{};float y=1/std::tan(fov*.5f);a.m[0][0]=y/aspect;a.m[1][1]=y;a.m[2][2]=zf/(zf-zn);a.m[2][3]=1;a.m[3][2]=-zn*zf/(zf-zn);return a;}
|
||||
M4 view_matrix(V3 p,V3 f,V3 r,V3 u){M4 a=identity();a.m[0][0]=r.x;a.m[1][0]=r.y;a.m[2][0]=r.z;a.m[3][0]=-dot(r,p);a.m[0][1]=u.x;a.m[1][1]=u.y;a.m[2][1]=u.z;a.m[3][1]=-dot(u,p);a.m[0][2]=f.x;a.m[1][2]=f.y;a.m[2][2]=f.z;a.m[3][2]=-dot(f,p);return a;}
|
||||
M4 transpose(M4 a){M4 b{};for(int i=0;i<4;++i)for(int j=0;j<4;++j)b.m[i][j]=a.m[j][i];return b;}
|
||||
struct GPUVertex { float p[3],n[3]; };
|
||||
struct CB { M4 viewProj; float camera[3],pad; float light[3],fogStart; float fogEnd,ambient,pad2[2]; };
|
||||
struct App {
|
||||
HWND hwnd{}; ID3D11Device* dev{}; ID3D11DeviceContext* ctx{}; IDXGISwapChain* swap{}; ID3D11RenderTargetView* rtv{}; ID3D11DepthStencilView* dsv{};
|
||||
ID3D11VertexShader* vs{}; ID3D11PixelShader* ps{}; ID3D11InputLayout* layout{}; ID3D11Buffer* cb{}; ID3D11RasterizerState* solid{}; ID3D11RasterizerState* wire{};
|
||||
ID3D11Buffer* nearVB{}; ID3D11Buffer* nearReliefVB{}; ID3D11Buffer* nearIB{}; ID3D11Buffer* farVB{}; ID3D11Buffer* farIB{}; UINT nearIndices{},farIndices{};
|
||||
uint64_t seed=1;int workers=6;double generationMs{};bool showNear=true,showFar=true,wireframe=false,captured=false,showDisplacement=true,captureFailed=false;float yaw=0,pitch=0,speed=8;V3 pos{};POINT last{};int width=1280,height=800;Clock::time_point prev=Clock::now();double frameMs=0;bool keys[256]{};std::filesystem::path captureTarget;
|
||||
void release_mesh(){if(nearVB)nearVB->Release();if(nearReliefVB)nearReliefVB->Release();if(nearIB)nearIB->Release();if(farVB)farVB->Release();if(farIB)farIB->Release();nearVB=nearReliefVB=nearIB=farVB=farIB=nullptr;nearIndices=farIndices=0;}
|
||||
~App(){release_mesh();if(solid)solid->Release();if(wire)wire->Release();if(cb)cb->Release();if(layout)layout->Release();if(vs)vs->Release();if(ps)ps->Release();if(rtv)rtv->Release();if(dsv)dsv->Release();if(swap)swap->Release();if(ctx)ctx->Release();if(dev)dev->Release();}
|
||||
bool make_buffer(ID3D11Buffer** out,const void* data,size_t bytes,UINT bind){D3D11_BUFFER_DESC d{};d.ByteWidth=static_cast<UINT>(bytes);d.Usage=D3D11_USAGE_DEFAULT;d.BindFlags=bind;D3D11_SUBRESOURCE_DATA s{};s.pSysMem=data;return SUCCEEDED(dev->CreateBuffer(&d,&s,out));}
|
||||
bool generate(){
|
||||
auto begin=Clock::now(); Corpus corpus;build_corpus(corpus,seed);AxisData axes[3];AxisStats stats[3];bool overflow=false;
|
||||
for(int a=0;a<3;++a)axes[a]=solve_axis(corpus,a,workers,stats[a],overflow);
|
||||
BlendStats blendStats{};apply_blend_zones(corpus,axes,stats,blendStats);
|
||||
std::vector<uint64_t> active;active_cell_masks(axes[2],active);MeshOutput nearMesh=emit_dual_contour_mesh(corpus,axes,axes[2],active);FarOutput farMesh=build_far_world(seed);
|
||||
std::vector<Vertex> reliefVertices;double maxDisplacement=0;apply_surface_displacement(nearMesh,reliefVertices,maxDisplacement);
|
||||
generationMs=ms(begin,Clock::now());release_mesh();
|
||||
std::vector<GPUVertex> nv;nv.reserve(nearMesh.vertices.size());for(const auto& v:nearMesh.vertices)nv.push_back({{v.v.x,v.v.y,v.v.z},{v.v.nx,v.v.ny,v.v.nz}});
|
||||
std::vector<GPUVertex> rv;rv.reserve(reliefVertices.size());for(const auto& v:reliefVertices)rv.push_back({{v.x,v.y,v.z},{v.nx,v.ny,v.nz}});
|
||||
std::vector<GPUVertex> fv;fv.reserve(farMesh.vertices.size());for(const auto& v:farMesh.vertices)fv.push_back({{v.x,v.y,v.z},{v.nx,v.ny,v.nz}});
|
||||
if(!nv.empty()&&(!make_buffer(&nearVB,nv.data(),nv.size()*sizeof(GPUVertex),D3D11_BIND_VERTEX_BUFFER)||!make_buffer(&nearReliefVB,rv.data(),rv.size()*sizeof(GPUVertex),D3D11_BIND_VERTEX_BUFFER)||!make_buffer(&nearIB,nearMesh.indices.data(),nearMesh.indices.size()*sizeof(uint32_t),D3D11_BIND_INDEX_BUFFER)))return false;
|
||||
if(!fv.empty()&&(!make_buffer(&farVB,fv.data(),fv.size()*sizeof(GPUVertex),D3D11_BIND_VERTEX_BUFFER)||!make_buffer(&farIB,farMesh.indices.data(),farMesh.indices.size()*sizeof(uint32_t),D3D11_BIND_INDEX_BUFFER)))return false;
|
||||
nearIndices=static_cast<UINT>(nearMesh.indices.size());farIndices=static_cast<UINT>(farMesh.indices.size());
|
||||
// The first generated room is centred near local (-13, -13, -8.5) metres.
|
||||
// These offsets match its deterministic seed-1 center and keep the camera in its air volume.
|
||||
Rng rng(seed ^ 0x0A57A20261002ull);int lx=-1300+rng.range(-170,170),ly=-1300+rng.range(-170,170),lz=-850+rng.range(-120,120);
|
||||
pos={kWorldOriginCm[0]*.01f+lx*.01f,kWorldOriginCm[1]*.01f+ly*.01f,kWorldOriginCm[2]*.01f+lz*.01f};yaw=0;pitch=0;
|
||||
return true;
|
||||
}
|
||||
bool init_device(){
|
||||
DXGI_SWAP_CHAIN_DESC sd{};sd.BufferCount=1;sd.BufferDesc.Format=DXGI_FORMAT_R8G8B8A8_UNORM;sd.BufferUsage=DXGI_USAGE_RENDER_TARGET_OUTPUT;sd.OutputWindow=hwnd;sd.SampleDesc.Count=1;sd.Windowed=TRUE;sd.SwapEffect=DXGI_SWAP_EFFECT_DISCARD;
|
||||
UINT flags=0;
|
||||
#if defined(_DEBUG)
|
||||
flags|=D3D11_CREATE_DEVICE_DEBUG;
|
||||
#endif
|
||||
D3D_FEATURE_LEVEL fl{};if(FAILED(D3D11CreateDeviceAndSwapChain(nullptr,D3D_DRIVER_TYPE_HARDWARE,nullptr,flags,nullptr,0,D3D11_SDK_VERSION,&sd,&swap,&dev,&fl,&ctx)))return false;
|
||||
const char* shader="cbuffer C:register(b0){float4x4 VP;float3 Camera;float Pad;float3 LightDir;float FogStart;float FogEnd;float Ambient;float2 Pad2;} struct I{float3 p:POSITION;float3 n:NORMAL;};struct O{float4 p:SV_POSITION;float3 world:TEXCOORD0;float3 n:NORMAL;};O VS(I i){O o;o.p=mul(float4(i.p,1),VP);o.world=i.p;o.n=i.n;return o;}float hash3(float3 p){return frac(sin(dot(p,float3(127.1,311.7,74.7)))*43758.5453);}float noise3(float3 p){float3 i=floor(p),f=frac(p);f=f*f*(3-2*f);float a=lerp(hash3(i),hash3(i+float3(1,0,0)),f.x);float b=lerp(hash3(i+float3(0,1,0)),hash3(i+float3(1,1,0)),f.x);float c=lerp(hash3(i+float3(0,0,1)),hash3(i+float3(1,0,1)),f.x);float d=lerp(hash3(i+float3(0,1,1)),hash3(i+float3(1,1,1)),f.x);return lerp(lerp(a,b,f.y),lerp(c,d,f.y),f.z);}float4 PS(O i):SV_TARGET{float3 N=normalize(i.n);float3 L=normalize(-LightDir);float diffuse=saturate(dot(N,L))*0.72;float3 toCam=Camera-i.world;float dist=length(toCam);float head=saturate(dot(N,normalize(toCam)))*0.85/(1+dist*0.018);float light=Ambient+diffuse+head;float mottling=0.68*noise3(i.world*0.28)+0.32*noise3(i.world*1.0);float shade=0.45+mottling;float3 rock=float3(0.46,0.39,0.30)*shade*light;float fog=saturate((dist-FogStart)/(FogEnd-FogStart));return float4(lerp(rock,float3(0.075,0.095,0.12),fog),1);} ";
|
||||
ID3DBlob *vb=nullptr,*pb=nullptr,*err=nullptr;if(FAILED(D3DCompile(shader,strlen(shader),nullptr,nullptr,nullptr,"VS","vs_4_0",0,0,&vb,&err)))return false;if(FAILED(D3DCompile(shader,strlen(shader),nullptr,nullptr,nullptr,"PS","ps_4_0",0,0,&pb,&err))){if(vb)vb->Release();return false;}
|
||||
HRESULT hr=dev->CreateVertexShader(vb->GetBufferPointer(),vb->GetBufferSize(),nullptr,&vs);if(SUCCEEDED(hr))hr=dev->CreatePixelShader(pb->GetBufferPointer(),pb->GetBufferSize(),nullptr,&ps);
|
||||
D3D11_INPUT_ELEMENT_DESC il[]={{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},{"NORMAL",0,DXGI_FORMAT_R32G32B32_FLOAT,0,12,D3D11_INPUT_PER_VERTEX_DATA,0}};if(SUCCEEDED(hr))hr=dev->CreateInputLayout(il,2,vb->GetBufferPointer(),vb->GetBufferSize(),&layout);vb->Release();pb->Release();if(FAILED(hr))return false;
|
||||
D3D11_BUFFER_DESC cbd{};cbd.ByteWidth=sizeof(CB);cbd.Usage=D3D11_USAGE_DEFAULT;cbd.BindFlags=D3D11_BIND_CONSTANT_BUFFER;if(FAILED(dev->CreateBuffer(&cbd,nullptr,&cb)))return false;
|
||||
D3D11_RASTERIZER_DESC rd{};rd.FillMode=D3D11_FILL_SOLID;rd.CullMode=D3D11_CULL_NONE;rd.DepthClipEnable=TRUE;if(FAILED(dev->CreateRasterizerState(&rd,&solid)))return false;rd.FillMode=D3D11_FILL_WIREFRAME;if(FAILED(dev->CreateRasterizerState(&rd,&wire)))return false;
|
||||
return resize(width,height);
|
||||
}
|
||||
bool resize(int w,int h){if(!dev||w<1||h<1)return true;width=w;height=h;if(rtv){rtv->Release();rtv=nullptr;}if(dsv){dsv->Release();dsv=nullptr;}if(FAILED(swap->ResizeBuffers(0,w,h,DXGI_FORMAT_UNKNOWN,0)))return false;ID3D11Texture2D* back{};if(FAILED(swap->GetBuffer(0,__uuidof(ID3D11Texture2D),reinterpret_cast<void**>(&back))))return false;HRESULT hr=dev->CreateRenderTargetView(back,nullptr,&rtv);back->Release();if(FAILED(hr))return false;D3D11_TEXTURE2D_DESC dd{};dd.Width=w;dd.Height=h;dd.MipLevels=1;dd.ArraySize=1;dd.Format=DXGI_FORMAT_D24_UNORM_S8_UINT;dd.SampleDesc.Count=1;dd.BindFlags=D3D11_BIND_DEPTH_STENCIL;ID3D11Texture2D* depth{};if(FAILED(dev->CreateTexture2D(&dd,nullptr,&depth)))return false;hr=dev->CreateDepthStencilView(depth,nullptr,&dsv);depth->Release();return SUCCEEDED(hr);}
|
||||
bool save_backbuffer_png(const std::filesystem::path& path){
|
||||
ID3D11Texture2D* back{};ID3D11Texture2D* staging{};IWICImagingFactory* factory{};IWICStream* stream{};IWICBitmapEncoder* encoder{};IWICBitmapFrameEncode* frame{};IPropertyBag2* props{};D3D11_TEXTURE2D_DESC desc{};D3D11_MAPPED_SUBRESOURCE mapped{};bool ok=false;HRESULT hr=S_OK;const char* stage="get swapchain buffer";
|
||||
if(FAILED(hr=swap->GetBuffer(0,__uuidof(ID3D11Texture2D),reinterpret_cast<void**>(&back))))goto done;
|
||||
back->GetDesc(&desc);desc.Usage=D3D11_USAGE_STAGING;desc.BindFlags=0;desc.CPUAccessFlags=D3D11_CPU_ACCESS_READ;desc.MiscFlags=0;
|
||||
stage="create staging texture";if(FAILED(hr=dev->CreateTexture2D(&desc,nullptr,&staging)))goto done;
|
||||
ctx->CopyResource(staging,back);
|
||||
stage="map staging texture";if(FAILED(hr=ctx->Map(staging,0,D3D11_MAP_READ,0,&mapped)))goto done;
|
||||
{
|
||||
const UINT stride=desc.Width*4;std::vector<BYTE> pixels(static_cast<size_t>(stride)*desc.Height);
|
||||
for(UINT y=0;y<desc.Height;++y){const BYTE* src=static_cast<const BYTE*>(mapped.pData)+static_cast<size_t>(y)*mapped.RowPitch;BYTE* dst=pixels.data()+static_cast<size_t>(y)*stride;for(UINT x=0;x<desc.Width;++x){dst[x*4]=src[x*4+2];dst[x*4+1]=src[x*4+1];dst[x*4+2]=src[x*4];dst[x*4+3]=src[x*4+3];}}
|
||||
ctx->Unmap(staging,0);
|
||||
stage="create WIC factory";if(FAILED(hr=CoCreateInstance(CLSID_WICImagingFactory,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&factory))))goto done;
|
||||
stage="create WIC stream";if(FAILED(hr=factory->CreateStream(&stream)))goto done;
|
||||
stage="open PNG path";if(FAILED(hr=stream->InitializeFromFilename(path.c_str(),GENERIC_WRITE)))goto done;
|
||||
stage="create PNG encoder";if(FAILED(hr=factory->CreateEncoder(GUID_ContainerFormatPng,nullptr,&encoder)))goto done;
|
||||
stage="initialize PNG encoder";if(FAILED(hr=encoder->Initialize(stream,WICBitmapEncoderNoCache)))goto done;
|
||||
stage="create PNG frame";if(FAILED(hr=encoder->CreateNewFrame(&frame,&props)))goto done;
|
||||
stage="initialize PNG frame";if(FAILED(hr=frame->Initialize(props)))goto done;
|
||||
stage="set PNG size";if(FAILED(hr=frame->SetSize(desc.Width,desc.Height)))goto done;
|
||||
{WICPixelFormatGUID format=GUID_WICPixelFormat32bppBGRA;stage="set PNG pixel format";hr=frame->SetPixelFormat(&format);if(FAILED(hr)||format!=GUID_WICPixelFormat32bppBGRA){if(SUCCEEDED(hr))hr=WINCODEC_ERR_UNSUPPORTEDPIXELFORMAT;goto done;}}
|
||||
stage="write PNG pixels";if(FAILED(hr=frame->WritePixels(desc.Height,stride,static_cast<UINT>(pixels.size()),pixels.data())))goto done;
|
||||
stage="commit PNG";if(FAILED(hr=frame->Commit())||FAILED(hr=encoder->Commit()))goto done;ok=true;
|
||||
}
|
||||
done:
|
||||
if(!ok)std::fprintf(stderr,"Capture failed at %s (HRESULT 0x%08lx)\n",stage,static_cast<unsigned long>(hr));
|
||||
if(staging)staging->Release();if(back)back->Release();if(frame)frame->Release();if(props)props->Release();if(encoder)encoder->Release();if(stream)stream->Release();if(factory)factory->Release();return ok;
|
||||
}
|
||||
V3 forward(){return norm({std::sin(yaw)*std::cos(pitch),std::cos(yaw)*std::cos(pitch),std::sin(pitch)});}
|
||||
void title(){char b[256];snprintf(b,sizeof(b),"Interval spike | %.2f ms/frame | %u tris | relief %s | (%.1f, %.1f, %.1f) m | gen %.1f ms | seed %llu",frameMs,((showNear?nearIndices:0)+(showFar?farIndices:0))/3,showDisplacement?"ON":"OFF",pos.x,pos.y,pos.z,generationMs,(unsigned long long)seed);SetWindowTextA(hwnd,b);}
|
||||
void draw(){auto now=Clock::now();float dt=std::clamp(std::chrono::duration<float>(now-prev).count(),0.0f,.1f);prev=now;frameMs=dt*1000;
|
||||
V3 f=forward(),r=norm(cross(f,{0,0,1})),u=norm(cross(r,f));float step=speed*dt*(keys[VK_SHIFT]?4.0f:1.0f);if(keys['W'])pos=add(pos,mul(f,step));if(keys['S'])pos=sub(pos,mul(f,step));if(keys['A'])pos=sub(pos,mul(r,step));if(keys['D'])pos=add(pos,mul(r,step));if(keys[VK_SPACE]||keys['E'])pos.z+=step;if(keys[VK_CONTROL]||keys['Q'])pos.z-=step;
|
||||
float clear[]={.075f,.095f,.12f,1};ctx->OMSetRenderTargets(1,&rtv,dsv);ctx->ClearRenderTargetView(rtv,clear);ctx->ClearDepthStencilView(dsv,D3D11_CLEAR_DEPTH,1,0);D3D11_VIEWPORT vp{0,0,(float)width,(float)height,0,1};ctx->RSSetViewports(1,&vp);ctx->RSSetState(wireframe?wire:solid);ctx->IASetInputLayout(layout);ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||
CB c{};M4 v=view_matrix(pos,f,r,u);M4 p=perspective(1.0472f,(float)width/height,.05f,20000); // compose view and projection for row-vector shader
|
||||
M4 out{};for(int i=0;i<4;++i)for(int j=0;j<4;++j)for(int k=0;k<4;++k)out.m[i][j]+=v.m[i][k]*p.m[k][j];c.viewProj=transpose(out);c.camera[0]=pos.x;c.camera[1]=pos.y;c.camera[2]=pos.z;c.light[0]=-.45f;c.light[1]=-.8f;c.light[2]=-.35f;c.fogStart=80;c.fogEnd=1200;c.ambient=.28f;ctx->UpdateSubresource(cb,0,nullptr,&c,0,0);ctx->VSSetConstantBuffers(0,1,&cb);ctx->PSSetConstantBuffers(0,1,&cb);ctx->VSSetShader(vs,nullptr,0);ctx->PSSetShader(ps,nullptr,0);
|
||||
auto mesh=[&](ID3D11Buffer* vb,ID3D11Buffer* ib,UINT count){if(!vb||!ib||!count)return;UINT stride=sizeof(GPUVertex),offset=0;ctx->IASetVertexBuffers(0,1,&vb,&stride,&offset);ctx->IASetIndexBuffer(ib,DXGI_FORMAT_R32_UINT,0);ctx->DrawIndexed(count,0,0);};if(showNear)mesh(showDisplacement?nearReliefVB:nearVB,nearIB,nearIndices);if(showFar)mesh(farVB,farIB,farIndices);if(!captureTarget.empty()){if(save_backbuffer_png(captureTarget))std::printf("Captured %ls\n",captureTarget.c_str());else{std::fprintf(stderr,"Capture failed: %ls\n",captureTarget.c_str());captureFailed=true;}captureTarget.clear();}swap->Present(1,0);title();}
|
||||
void capture(bool on){captured=on;if(on){SetCapture(hwnd);ShowCursor(FALSE);GetCursorPos(&last);POINT c{width/2,height/2};ClientToScreen(hwnd,&c);SetCursorPos(c.x,c.y);}else{ReleaseCapture();ShowCursor(TRUE);}}
|
||||
};
|
||||
App* g{};
|
||||
LRESULT CALLBACK wndproc(HWND h,UINT m,WPARAM w,LPARAM l){if(!g)return DefWindowProc(h,m,w,l);switch(m){case WM_SIZE:if(w!=SIZE_MINIMIZED)g->resize(LOWORD(l),HIWORD(l));return 0;case WM_KEYDOWN:if(w<256)g->keys[w]=true;if(w==VK_ESCAPE){if(g->captured)g->capture(false);else DestroyWindow(h);}if(w=='F')g->wireframe=!g->wireframe;if(w=='R')g->showDisplacement=!g->showDisplacement;if(w=='N'||w=='P'){g->seed+=(w=='N'?1:static_cast<uint64_t>(-1));if(!g->generate())MessageBoxA(h,"Could not create mesh buffers.","Viewer error",MB_ICONERROR);}if(w=='M')g->showNear=!g->showNear;if(w=='B')g->showFar=!g->showFar;return 0;case WM_KEYUP:if(w<256)g->keys[w]=false;return 0;case WM_RBUTTONDOWN:g->capture(true);return 0;case WM_RBUTTONUP:g->capture(false);return 0;case WM_MOUSEMOVE:if(g->captured){POINT c{g->width/2,g->height/2};ClientToScreen(h,&c);POINT p{GET_X_LPARAM(l),GET_Y_LPARAM(l)};POINT sc=p;ClientToScreen(h,&sc);int dx=sc.x-c.x,dy=sc.y-c.y;if(dx||dy){g->yaw+=dx*.0035f;g->pitch=std::clamp(g->pitch-dy*.0035f,-1.55f,1.55f);SetCursorPos(c.x,c.y);}}return 0;case WM_MOUSEWHEEL:g->speed=std::clamp(g->speed*(GET_WHEEL_DELTA_WPARAM(w)>0?1.25f:.8f),.25f,1000.0f);return 0;case WM_DESTROY:PostQuitMessage(0);return 0;}return DefWindowProc(h,m,w,l);}
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE instance,HINSTANCE,LPSTR,int){
|
||||
int argc=0;LPWSTR* argv=CommandLineToArgvW(GetCommandLineW(),&argc);uint64_t seed=1;int workers=6,positional=0;std::filesystem::path captureDir;bool captureMode=false;
|
||||
if(argv)for(int i=1;i<argc;++i){if(std::wcscmp(argv[i],L"--capture-dir")==0&&i+1<argc){captureDir=argv[++i];captureMode=true;}else if(std::wcscmp(argv[i],L"--seed")==0&&i+1<argc)seed=_wcstoui64(argv[++i],nullptr,10);else if(std::wcscmp(argv[i],L"--workers")==0&&i+1<argc)workers=std::clamp(_wtoi(argv[++i]),1,8);else if(positional==0){seed=_wcstoui64(argv[i],nullptr,10);++positional;}else if(positional==1){workers=std::clamp(_wtoi(argv[i]),1,8);++positional;}}
|
||||
if(FAILED(CoInitializeEx(nullptr,COINIT_MULTITHREADED))){if(argv)LocalFree(argv);return 1;}
|
||||
AllocConsole();FILE* stream{};freopen_s(&stream,"CONOUT$","w",stdout);std::printf("Controls: hold right mouse button to look; WASD move; Space/Ctrl (E/Q) up/down; Shift faster; wheel speed; F wireframe; R relief toggle; M near; B far; N/P next/previous seed; Esc quit.\n");
|
||||
App app;g=&app;app.seed=seed;app.workers=workers;WNDCLASSA wc{};wc.lpfnWndProc=wndproc;wc.hInstance=instance;wc.lpszClassName="IntervalSpikeViewer2";wc.hCursor=LoadCursor(nullptr,IDC_ARROW);RegisterClassA(&wc);RECT rc{0,0,app.width,app.height};AdjustWindowRect(&rc,WS_OVERLAPPEDWINDOW,FALSE);app.hwnd=CreateWindowA(wc.lpszClassName,"Interval spike viewer 2",WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,rc.right-rc.left,rc.bottom-rc.top,nullptr,nullptr,instance,nullptr);if(!app.hwnd||!app.init_device()){MessageBoxA(nullptr,"Direct3D 11 initialization failed.","Viewer error",MB_ICONERROR);CoUninitialize();if(argv)LocalFree(argv);return 1;}ShowWindow(app.hwnd,SW_SHOW);UpdateWindow(app.hwnd);if(!app.generate()){MessageBoxA(app.hwnd,"World generation or GPU buffer creation failed.","Viewer error",MB_ICONERROR);CoUninitialize();if(argv)LocalFree(argv);return 2;}
|
||||
if(captureMode){
|
||||
std::error_code ec;std::filesystem::create_directories(captureDir,ec);if(ec){std::fprintf(stderr,"Could not create capture directory: %ls\n",captureDir.c_str());CoUninitialize();if(argv)LocalFree(argv);return 3;}
|
||||
auto capture=[&](const wchar_t* name){app.captureTarget=captureDir/name;app.draw();};
|
||||
capture(L"chamber.png");
|
||||
app.showDisplacement=false;capture(L"chamber_relief_off.png");app.showDisplacement=true;
|
||||
app.pos={kWorldOriginCm[0]*.01f-2.5f,kWorldOriginCm[1]*.01f-.75f,kWorldOriginCm[2]*.01f-3.5f};app.yaw=0;app.pitch=0;capture(L"junction.png");
|
||||
Rng rng(seed ^ 0x0A57A20261002ull);int lx=-1300+rng.range(-170,170),ly=-1300+rng.range(-170,170),lz=-850+rng.range(-120,120);
|
||||
app.pos={kWorldOriginCm[0]*.01f+(lx+500)*.01f,kWorldOriginCm[1]*.01f+ly*.01f,kWorldOriginCm[2]*.01f+lz*.01f};app.yaw=1.5707963f;app.pitch=0;capture(L"wall.png");
|
||||
const bool ok=!app.captureFailed;DestroyWindow(app.hwnd);g=nullptr;CoUninitialize();if(argv)LocalFree(argv);return ok?0:4;
|
||||
}
|
||||
if(argv)LocalFree(argv);MSG msg{};while(msg.message!=WM_QUIT){if(PeekMessage(&msg,nullptr,0,0,PM_REMOVE)){TranslateMessage(&msg);DispatchMessage(&msg);}else app.draw();}CoUninitialize();return 0;
|
||||
}
|
||||
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 406 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 498 KiB |
@@ -0,0 +1,31 @@
|
||||
Machine: Ryzen 7 5800X3D, Windows, MSVC 2022, 2026-09-29.
|
||||
Command from Saved/Research:
|
||||
cmd /c '"E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul && cl /nologo /O2 /std:c++20 /EHsc /arch:AVX2 round2_probe.cpp /Fe:round2_probe.exe'
|
||||
.\round2_probe.exe
|
||||
|
||||
First run:
|
||||
full trial=0 scalar_ms=492.007 avx2_ms=101.294 speedup=4.857 mismatch=0
|
||||
full trial=1 scalar_ms=496.238 avx2_ms=106.015 speedup=4.681 mismatch=0
|
||||
full trial=2 scalar_ms=469.590 avx2_ms=92.511 speedup=5.076 mismatch=0
|
||||
crater radius_vox=40 touched_bricks=714 tile_AABB_max=64 samples_AABB=531441
|
||||
crater copy_ms=1.623 edit_ms=0.865 changed_lattice=94843 copy_bytes=33554432
|
||||
|
||||
Second run after dead-code cleanup (same evaluation code):
|
||||
full trial=0 scalar_ms=430.286 avx2_ms=89.209 speedup=4.823 mismatch=0
|
||||
full trial=1 scalar_ms=456.202 avx2_ms=107.464 speedup=4.245 mismatch=0
|
||||
full trial=2 scalar_ms=447.117 avx2_ms=94.584 speedup=4.727 mismatch=0
|
||||
crater radius_vox=40 touched_bricks=714 tile_AABB_max=64 samples_AABB=531441
|
||||
crater copy_ms=1.422 edit_ms=0.914 changed_lattice=94843 copy_bytes=33554432
|
||||
|
||||
Third run after adding a scattered direct-query measurement (same field evaluation code):
|
||||
full trial=0 scalar_ms=478.627 avx2_ms=85.763 speedup=5.581 mismatch=0
|
||||
full trial=1 scalar_ms=471.015 avx2_ms=80.318 speedup=5.864 mismatch=0
|
||||
full trial=2 scalar_ms=444.320 avx2_ms=82.728 speedup=5.371 mismatch=0
|
||||
random_query trial=0 count=200000 ms=63.030 ns_per_query=315.1 checksum=-1501231
|
||||
random_query trial=1 count=200000 ms=62.785 ns_per_query=313.9 checksum=-1501231
|
||||
random_query trial=2 count=200000 ms=62.799 ns_per_query=314.0 checksum=-1501231
|
||||
crater radius_vox=40 touched_bricks=714 tile_AABB_max=64 samples_AABB=531441
|
||||
crater copy_ms=1.394 edit_ms=0.861 changed_lattice=94843 copy_bytes=33554432
|
||||
|
||||
Both MSVC builds succeeded. The vcvars script emitted a missing-vswhere.exe notice.
|
||||
Synthetic workload; no Unreal build/launch or GPU benchmark.
|
||||
@@ -0,0 +1,125 @@
|
||||
// Standalone research probe. Synthetic operator composition inspired by VoxelForge;
|
||||
// NOT extracted production code. Compile with MSVC /O2 /arch:AVX2 /std:c++20.
|
||||
#include <immintrin.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
constexpr int N=256, B=8, W=6;
|
||||
using V=__m256i;
|
||||
static std::vector<int16_t> scalarField(size_t(N)*N*N), vectorField(size_t(N)*N*N);
|
||||
alignas(32) static int32_t fadeTable[256];
|
||||
static inline uint32_t hashS(uint32_t x,uint32_t y,uint32_t z,uint32_t seed){
|
||||
uint32_t h=seed^(x*0x9e3779b1u)^(y*0x85ebca77u)^(z*0xc2b2ae3du);
|
||||
h^=h>>16; h*=0x7feb352du; h^=h>>15; h*=0x846ca68bu; return h^(h>>16);
|
||||
}
|
||||
static inline V hashV(V x,V y,V z,V seed){
|
||||
V h=_mm256_xor_si256(seed,_mm256_xor_si256(_mm256_mullo_epi32(x,_mm256_set1_epi32(0x9e3779b1u)),
|
||||
_mm256_xor_si256(_mm256_mullo_epi32(y,_mm256_set1_epi32(0x85ebca77u)),_mm256_mullo_epi32(z,_mm256_set1_epi32(0xc2b2ae3du)))));
|
||||
h=_mm256_xor_si256(h,_mm256_srli_epi32(h,16)); h=_mm256_mullo_epi32(h,_mm256_set1_epi32(0x7feb352du));
|
||||
h=_mm256_xor_si256(h,_mm256_srli_epi32(h,15)); h=_mm256_mullo_epi32(h,_mm256_set1_epi32(0x846ca68bu));
|
||||
return _mm256_xor_si256(h,_mm256_srli_epi32(h,16));
|
||||
}
|
||||
static inline int lerpS(int a,int b,int t){return a+((b-a)*t>>8);}
|
||||
static inline V lerpV(V a,V b,V t){return _mm256_add_epi32(a,_mm256_srai_epi32(_mm256_mullo_epi32(_mm256_sub_epi32(b,a),t),8));}
|
||||
static inline int gradS(uint32_t h,int x,int y,int z){
|
||||
h&=15;int u=(h&8)?y:x;int v=(h&12)==0?y:((h&13)==12?x:z);
|
||||
return ((h&1)?-u:u)+((h&2)?-v:v);
|
||||
}
|
||||
static inline V selectV(V mask,V a,V b){return _mm256_blendv_epi8(b,a,mask);}
|
||||
static inline V gradV(V h,V x,V y,V z){
|
||||
V zero=_mm256_setzero_si256(); h=_mm256_and_si256(h,_mm256_set1_epi32(15));
|
||||
V u=selectV(_mm256_cmpeq_epi32(_mm256_and_si256(h,_mm256_set1_epi32(8)),zero),x,y);
|
||||
V isY=_mm256_cmpeq_epi32(_mm256_and_si256(h,_mm256_set1_epi32(12)),zero);
|
||||
V isX=_mm256_cmpeq_epi32(_mm256_and_si256(h,_mm256_set1_epi32(13)),_mm256_set1_epi32(12));
|
||||
V v=selectV(isY,y,selectV(isX,x,z));
|
||||
V negU=_mm256_cmpeq_epi32(_mm256_and_si256(h,_mm256_set1_epi32(1)),_mm256_set1_epi32(1));
|
||||
V negV=_mm256_cmpeq_epi32(_mm256_and_si256(h,_mm256_set1_epi32(2)),_mm256_set1_epi32(2));
|
||||
return _mm256_add_epi32(_mm256_sub_epi32(_mm256_xor_si256(u,negU),negU),_mm256_sub_epi32(_mm256_xor_si256(v,negV),negV));
|
||||
}
|
||||
static inline int noiseS(int x,int y,int z,int shift,int seed){
|
||||
int ix=x>>shift,iy=y>>shift,iz=z>>shift,fx=(x&((1<<shift)-1))<<(8-shift),fy=(y&((1<<shift)-1))<<(8-shift),fz=(z&((1<<shift)-1))<<(8-shift);
|
||||
int ux=fadeTable[fx],uy=fadeTable[fy],uz=fadeTable[fz];
|
||||
int yz[2]; for(int dz=0;dz<2;++dz){int yy[2];for(int dy=0;dy<2;++dy){
|
||||
int a=gradS(hashS(ix,iy+dy,iz+dz,seed),fx,fy-dy*256,fz-dz*256);
|
||||
int b=gradS(hashS(ix+1,iy+dy,iz+dz,seed),fx-256,fy-dy*256,fz-dz*256);
|
||||
yy[dy]=lerpS(a,b,ux);}
|
||||
yz[dz]=lerpS(yy[0],yy[1],uy);} return lerpS(yz[0],yz[1],uz)/4;
|
||||
}
|
||||
static inline V noiseV(V x,V y,V z,int shift,int seed){
|
||||
V mask=_mm256_set1_epi32((1<<shift)-1), one=_mm256_set1_epi32(1);
|
||||
V ix=_mm256_srai_epi32(x,shift),iy=_mm256_srai_epi32(y,shift),iz=_mm256_srai_epi32(z,shift);
|
||||
V fx=_mm256_slli_epi32(_mm256_and_si256(x,mask),8-shift),fy=_mm256_slli_epi32(_mm256_and_si256(y,mask),8-shift),fz=_mm256_slli_epi32(_mm256_and_si256(z,mask),8-shift);
|
||||
V ux=_mm256_i32gather_epi32(fadeTable,fx,4),uy=_mm256_i32gather_epi32(fadeTable,fy,4),uz=_mm256_i32gather_epi32(fadeTable,fz,4);
|
||||
V yz[2]; for(int dz=0;dz<2;++dz){V yy[2];for(int dy=0;dy<2;++dy){
|
||||
V y1=_mm256_add_epi32(iy,_mm256_set1_epi32(dy)),z1=_mm256_add_epi32(iz,_mm256_set1_epi32(dz));
|
||||
V yyoff=_mm256_sub_epi32(fy,_mm256_set1_epi32(dy*256)),zzoff=_mm256_sub_epi32(fz,_mm256_set1_epi32(dz*256));
|
||||
V a=gradV(hashV(ix,y1,z1,_mm256_set1_epi32(seed)),fx,yyoff,zzoff);
|
||||
V b=gradV(hashV(_mm256_add_epi32(ix,one),y1,z1,_mm256_set1_epi32(seed)),_mm256_sub_epi32(fx,_mm256_set1_epi32(256)),yyoff,zzoff);
|
||||
yy[dy]=lerpV(a,b,ux);}yz[dz]=lerpV(yy[0],yy[1],uy);}
|
||||
V raw=lerpV(yz[0],yz[1],uz);V bias=_mm256_and_si256(_mm256_srai_epi32(raw,31),_mm256_set1_epi32(3));
|
||||
return _mm256_srai_epi32(_mm256_add_epi32(raw,bias),2);
|
||||
}
|
||||
static inline int smoothMaxS(int a,int b){int h=std::max(0,64-std::abs(a-b));return std::max(a,b)+(h*h>>8);}
|
||||
static inline V smoothMaxV(V a,V b){V d=_mm256_sub_epi32(a,b);V h=_mm256_max_epi32(_mm256_setzero_si256(),_mm256_sub_epi32(_mm256_set1_epi32(64),_mm256_abs_epi32(d)));
|
||||
return _mm256_add_epi32(_mm256_max_epi32(a,b),_mm256_srai_epi32(_mm256_mullo_epi32(h,h),8));}
|
||||
static inline V evalV(V x,V y,V z){
|
||||
V c=_mm256_set1_epi32(1024), warp=_mm256_srai_epi32(noiseV(_mm256_add_epi32(x,c),_mm256_add_epi32(y,c),_mm256_add_epi32(z,c),4,17),4);
|
||||
V nx=_mm256_add_epi32(_mm256_add_epi32(x,c),warp),ny=_mm256_sub_epi32(_mm256_add_epi32(y,c),warp),nz=_mm256_add_epi32(z,c);
|
||||
V fb=_mm256_add_epi32(noiseV(nx,ny,nz,4,100),_mm256_add_epi32(_mm256_srai_epi32(noiseV(nx,ny,nz,3,101),1),_mm256_srai_epi32(noiseV(nx,ny,nz,2,102),2)));
|
||||
V ridge=_mm256_sub_epi32(_mm256_set1_epi32(128),_mm256_abs_epi32(noiseV(nx,ny,nz,3,202)));
|
||||
// Scalar signed division truncates toward zero; AVX2 shift rounds negative down.
|
||||
// Correct sign before shift to make the two exact.
|
||||
auto divPow2=[](V a,int shift){V bias=_mm256_and_si256(_mm256_srai_epi32(a,31),_mm256_set1_epi32((1<<shift)-1));return _mm256_srai_epi32(_mm256_add_epi32(a,bias),shift);};
|
||||
// Exact truncation toward zero matches scalar division by powers of two.
|
||||
V floor=_mm256_add_epi32(_mm256_sub_epi32(z,_mm256_set1_epi32(84)),divPow2(fb,2));
|
||||
V ceil=_mm256_add_epi32(_mm256_sub_epi32(_mm256_set1_epi32(170),z),divPow2(ridge,2));
|
||||
V stratum=_mm256_min_epi32(floor,ceil),dy=_mm256_sub_epi32(y,_mm256_set1_epi32(128)),dz=_mm256_sub_epi32(z,_mm256_set1_epi32(128));
|
||||
V dy2=_mm256_mullo_epi32(dy,dy),dz2=_mm256_mullo_epi32(dz,dz),dx=_mm256_sub_epi32(x,_mm256_set1_epi32(128));
|
||||
V passage=divPow2(_mm256_sub_epi32(_mm256_set1_epi32(18*18),_mm256_add_epi32(dy2,dz2)),3);
|
||||
V room=divPow2(_mm256_sub_epi32(_mm256_set1_epi32(35*35),_mm256_add_epi32(_mm256_mullo_epi32(dx,dx),_mm256_add_epi32(dy2,dz2))),4);
|
||||
return smoothMaxV(stratum,smoothMaxV(passage,room));
|
||||
}
|
||||
static inline int evalS(int x,int y,int z){
|
||||
int warp=noiseS(x+1024,y+1024,z+1024,4,17)>>4; int nx=x+1024+warp,ny=y+1024-warp,nz=z+1024;
|
||||
int fb=noiseS(nx,ny,nz,4,100)+(noiseS(nx,ny,nz,3,101)>>1)+(noiseS(nx,ny,nz,2,102)>>2);
|
||||
int ridge=128-std::abs(noiseS(nx,ny,nz,3,202));
|
||||
int stratum=std::min(z-84+fb/4,170-z+ridge/4);
|
||||
int dy=y-128,dz=z-128,dx=x-128;
|
||||
int passage=(18*18-dy*dy-dz*dz)/8,room=(35*35-dx*dx-dy*dy-dz*dz)/16;
|
||||
return smoothMaxS(stratum,smoothMaxS(passage,room));
|
||||
}
|
||||
static inline size_t idx(int x,int y,int z){return(size_t(z)*N+y)*N+x;}
|
||||
template<class F> static double ms(F&& f){auto t=std::chrono::steady_clock::now();f();return std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-t).count();}
|
||||
template<class F> static void parallel(F fn){std::array<std::thread,W> threads;for(int k=0;k<W;++k)threads[k]=std::thread([=]{fn(k*N/W,(k+1)*N/W);});for(auto& t:threads)t.join();}
|
||||
static void fillScalar(){parallel([](int a,int b){for(int z=a;z<b;++z)for(int y=0;y<N;++y)for(int x=0;x<N;++x)scalarField[idx(x,y,z)]=(int16_t)evalS(x,y,z);});}
|
||||
static void fillVector(){parallel([](int a,int b){for(int z=a;z<b;++z)for(int y=0;y<N;++y)for(int x=0;x<N;x+=8){
|
||||
V xx=_mm256_add_epi32(_mm256_set1_epi32(x),_mm256_setr_epi32(0,1,2,3,4,5,6,7)); V d=evalV(xx,_mm256_set1_epi32(y),_mm256_set1_epi32(z));
|
||||
alignas(32) int32_t values[8];_mm256_store_si256((V*)values,d);for(int j=0;j<8;++j)vectorField[idx(x+j,y,z)]=(int16_t)values[j];}});}
|
||||
static inline int dist2box(int a,int lo,int hi){int d=std::max({lo-a,0,a-hi});return d*d;}
|
||||
int main(){
|
||||
for(int t=0;t<256;++t){int64_t q=t;int64_t q2=q*q,q3=q2*q,q4=q3*q,q5=q4*q;
|
||||
fadeTable[t]=int32_t((6*q5-15*q4*256+10*q3*256*256)/(256LL*256*256*256));}
|
||||
for(int t=0;t<3;++t){double s=ms(fillScalar),v=ms(fillVector);size_t mismatch=0;for(size_t i=0;i<scalarField.size();++i)mismatch+=scalarField[i]!=vectorField[i];
|
||||
std::printf("full trial=%d scalar_ms=%.3f avx2_ms=%.3f speedup=%.3f mismatch=%zu\n",t,s,v,s/v,mismatch);}
|
||||
struct Point{int x,y,z;};std::vector<Point> points(200000);uint32_t rng=0x12345678;
|
||||
for(auto& p:points){rng^=rng<<13;rng^=rng>>17;rng^=rng<<5;p.x=int(rng&255);
|
||||
rng^=rng<<13;rng^=rng>>17;rng^=rng<<5;p.y=int(rng&255);
|
||||
rng^=rng<<13;rng^=rng>>17;rng^=rng<<5;p.z=int(rng&255);}
|
||||
for(int t=0;t<3;++t){int64_t checksum=0;double q=ms([&]{for(const auto& p:points)checksum+=evalS(p.x,p.y,p.z);});
|
||||
std::printf("random_query trial=%d count=%zu ms=%.3f ns_per_query=%.1f checksum=%lld\n",t,points.size(),q,q*1e6/points.size(),(long long)checksum);}
|
||||
// Radius 10m at 25cm grid, center on tile boundary. Count exact AABB touched bricks.
|
||||
const int cx=128,cy=128,cz=128,r=40;int touched=0;for(int bz=0;bz<N/B;++bz)for(int by=0;by<N/B;++by)for(int bx=0;bx<N/B;++bx){
|
||||
int x0=bx*B,x1=x0+B-1,y0=by*B,y1=y0+B-1,z0=bz*B,z1=z0+B-1;
|
||||
if(dist2box(cx,x0,x1)+dist2box(cy,y0,y1)+dist2box(cz,z0,z1)<=r*r)++touched;}
|
||||
std::printf("crater radius_vox=%d touched_bricks=%d tile_AABB_max=%d samples_AABB=%d\n",r,touched,64,81*81*81);
|
||||
// One logical transaction: versioned edit, canonical max composition, copy-on-write buffer.
|
||||
std::vector<int16_t> next(vectorField.size());double copy=ms([&]{next=vectorField;});size_t changed=0;double edit=ms([&]{parallel([&](int za,int zb){for(int z=std::max(za,cz-r);z<std::min(zb,cz+r+1);++z)for(int y=cy-r;y<=cy+r;++y)for(int x=cx-r;x<=cx+r;++x){
|
||||
int dx=x-cx,dy=y-cy,dz=z-cz,q=r*r-dx*dx-dy*dy-dz*dz;
|
||||
if(q>=0){int d=std::max<int>(vectorField[idx(x,y,z)],q/16);next[idx(x,y,z)]=(int16_t)d;}}});});
|
||||
for(int z=cz-r;z<=cz+r;++z)for(int y=cy-r;y<=cy+r;++y)for(int x=cx-r;x<=cx+r;++x)changed+=next[idx(x,y,z)]!=vectorField[idx(x,y,z)];
|
||||
std::printf("crater copy_ms=%.3f edit_ms=%.3f changed_lattice=%zu copy_bytes=%zu\n",copy,edit,changed,vectorField.size()*sizeof(int16_t));
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# Step 2d launch ledger
|
||||
|
||||
All Unreal processes were launched through `Tools/VoxelForgeTest.ps1` and ran serially. The
|
||||
guarded launches used below-normal priority, the shared persistent DDC where recorded,
|
||||
`-MaxParallelActions=4`, a prelaunch 6 GiB memory check, an 8 GiB per-process limit, and a
|
||||
30-minute timeout. The retained ledgers show **no crash reporters, no process above 8 GiB, and
|
||||
no timeout**. Exit 255 below means automation assertions failed; it was not a native crash.
|
||||
|
||||
## Benchmark attempts
|
||||
|
||||
| Label | Build | Unreal process | Result |
|
||||
|---|---|---|---|
|
||||
| `step2d_rebuild_01` | UBT PID 25780, exit 6, peak 2.71 GiB | Not launched | Compile failed on the `ApplicationContextMask` automation flag; corrected in the next source pass. |
|
||||
| `step2d_rebuild_02` | UBT PID 19420, passed, peak 1.57 GiB | PID 13996, exit 2, peak 1.83 GiB | 3.834 s optimized ring; brick/sample skipping was too low for the speed gate. |
|
||||
| `step2d_rebuild_03` | Passed; PID not retained in the compact log | PID 15480, exit 2, peak 1.82 GiB | 3.918 s; detail spectrum failed. The smoothing negative control worked; the proof negative control did not. |
|
||||
| `step2d_rebuild_04` | Not launched | Not launched | Harness rejected a relative `-Out` path before Unreal startup. |
|
||||
| `step2d_rebuild_05` | UBT PID 23904, passed | PID 24708, exit 2, peak 1.82 GiB | Scratch reuse gave 233.27 density mismatches and hash/boundary failures; scratch ownership was corrected. |
|
||||
| `step2d_rebuild_06` | UBT PID 7540, passed | PID 25828, exit 2, peak 1.82 GiB | Detail, smoothing, boundary, and hash checks passed, but 3.70 s and 1.46% sample skipping missed the speed gate. |
|
||||
| `step2d_rebuild_07` | UBT PID 23800, passed | PID 5208, exit 2, peak 1.81 GiB | 3.66 s; the cached XY macro did not improve the timing. |
|
||||
| `step2d_rebuild_08` | UBT PID 25304, passed | PID 7108, exit 2, peak 1.81 GiB | 2.62 s; 22.20% of bricks and 21.18% of samples skipped, still over target. |
|
||||
| `step2d_rebuild_09` | UBT PID 25612, passed, peak 1.51 GiB | PID 24256, exit 0, peak 1.82 GiB | Superficially under 600 ms, but invalid as evidence: classifier gap/default/chunk semantics had not yet been fixed. Excluded. |
|
||||
| `step2d_harness_10` | UBT PID 2544, passed, peak 1.49 GiB | PID 23204, exit 0, peak 1.84 GiB | 503.026 ms and hash/detail/boundary checks passed. Superseded because its deliberately undersized-bound control did not catch a wrong sign. |
|
||||
| `step2d_harness_11` | UBT PID 2900, passed, peak 1.50 GiB | PID 10688, exit 0, peak 1.83 GiB | 497.518 ms; the scale-0 versus scale-4 undersized-bound control caught 35,847 wrong-sign samples. |
|
||||
| `step2d_harness_12` | UBT PID 13868, passed, peak 1.87 GiB | PID 16872, exit 0, peak 1.81 GiB | Final dynamically scheduled run: 308.177 ms median, all hashes and research gates passed. |
|
||||
|
||||
## Functional runs
|
||||
|
||||
| Label | Build | Unreal process | Result |
|
||||
|---|---|---|---|
|
||||
| `step2d_functional_01` | UBT PID 21184, passed | PID 3444, exit 255, peak 3.26 GiB | Base connectivity passed; refinement failed on an endpoint self-query. The launcher detected a write under `C:\Users\Jahni\AppData\Local\UnrealEngine`; later runs used isolated per-run local data. |
|
||||
| `step2d_functional_02` | UBT PID 11844, passed, peak 1.87 GiB | PID 5048, exit 255, peak 3.13 GiB | Same endpoint-snap failure; isolated LocalAppData was clean. |
|
||||
| `step2d_functional_03` | UBT PID 2060, passed, peak 1.90 GiB | PID 25648, exit 255, peak 3.19 GiB | Origin probe compared raw density 8 with a coarse center at -3.534534 near Z=-1012. |
|
||||
| `step2d_functional_04` | Failed launch; retained PID 23996, stage split not retained | Exit 255; peak not retained | Range-normalization fix did not resolve the origin probe. No crash was recorded. |
|
||||
| `step2d_redefined_seeds_01` | Build passed; PID not retained | PID 21888, exit 255 | Initial multi-seed endpoint-snap expectation was stricter than the route's full-density segment check; expectation was revised. |
|
||||
| `step2d_functional_05` | UBT PID 17000, passed | PID 7540, exit 255 | 12/16 fitted VerticalShafts seeds passed. |
|
||||
| `step2d_functional_06` | Build passed; PID not retained | PID 21948, exit 255, peak 3.43 GiB | 12/16; seed 1337's physical arrival/departure audit found 13 bad samples of 15. |
|
||||
| `step2d_redefined_seeds_02` | UBT PID 14672, exit 0, peak 1.63 GiB | PID 2220, exit 0, peak 2.61 GiB | Passed an earlier three-seed traversal version. |
|
||||
| `step2d_functional_07` | Build/Unreal stage PID 15712 in retained summary | Exit 255, peak 3.42 GiB | 12/16; no native crash. |
|
||||
| Relative-output-path prelaunch attempt | Not launched | Not launched | Harness stopped before Unreal startup; corrected to an absolute output directory. |
|
||||
| `step2d_functional_08` | UBT PID 12596, passed, peak 1.87 GiB | PID 20624, exit 255, peak 3.42 GiB | 13/16; origin path passed, three measurement limits remained. |
|
||||
| `step2d_functional_09` | UBT PID 10848, passed, peak 1.50 GiB | PID 16764, exit 255, peak 3.46 GiB | 13/16 after the cache-key fix. |
|
||||
| `step2d_functional_10` | UBT PID 24704, passed, peak 1.44 GiB | PID 7512, exit 255, peak 3.56 GiB | 13/16 after connector-radius changes. |
|
||||
| `step2d_functional_11` | UBT PID 17088, exit 6, peak 1.96 GiB | Not launched | Compile error C4456 from a duplicated `XYCount`; fixed before the next build. |
|
||||
| `step2d_functional_12` | UBT PID 4760, passed, peak 1.95 GiB | PID 26520, exit 255, peak 3.49 GiB | 13/16 after explicit bridges. |
|
||||
| `step2d_functional_13` | UBT PID 19416, passed, peak 1.50 GiB | PID 26444, exit 255, peak 3.50 GiB | 13/16; diagnostics showed 16 retries and both endpoints in the same large component. |
|
||||
| `step2d_functional_14` | UBT PID 3156, passed, peak 1.50 GiB | PID 17892, exit 255, peak 3.49 GiB | 13/16 after widening the landing bridge. |
|
||||
| `step2d_functional_15` | UBT PID 5648, passed, peak 1.49 GiB | PID 23788, exit 255, peak 3.53 GiB | 14/16 after adding the vertical cavern band. |
|
||||
| `step2d_functional_16` | UBT PID 20028, passed, peak 1.13 GiB | PID 4916, exit 255, peak 3.60 GiB | Controlled-wall assumptions and the 14/16 refinement audit both failed. |
|
||||
| `step2d_functional_17` | UBT PID 14952, passed, peak 1.50 GiB | PID 2604, exit 0, peak 2.69 GiB | Three-seed traversal test passed: 21 landing brackets and 18 routes. |
|
||||
| `step2d_functional_18` | UBT PID 10496, passed, peak 1.49 GiB | PID 25636, exit 0, peak 2.70 GiB | Passage-open-space test passed. |
|
||||
| `step2d_functional_19` | UBT PID 22588, passed, peak 1.48 GiB | PID 7316, exit 255, peak 3.71 GiB | Controlled wall returned conservative unknown after zero retries; refinement remained 14/16. |
|
||||
| `step2d_functional_20` | UBT PID 14076, passed, peak 1.88 GiB | PID 23120, exit 255, peak 3.60 GiB | Wall samples were -2/-102/-2, so the chosen location was not an air/solid/air control; refinement remained 14/16. |
|
||||
| `step2d_functional_21` | UBT PID 8224, passed, peak 1.58 GiB | PID 25652, exit 255, peak 3.67 GiB | `StrateConnectivity` passed; refinement remained 14/16 because the fallback had been placed in a skipped diagnostic branch. |
|
||||
| `step2d_functional_22` | UBT PID 26528, passed, peak 1.62 GiB | PID 24396, exit 0, peak 3.68 GiB | Both required connectivity tests passed; the fitted audit directly confirmed 16/16 routes at step 2. |
|
||||
| `step2d_functional_23` | Build not requested | PID 15176, exit 0, peak 2.44 GiB | `PassageLandsInOpenSpace` passed. |
|
||||
| `step2d_functional_24` | Build not requested | PID 26336, exit 0, peak 2.42 GiB | `RedefinedFieldTraversalSeeds` passed across three seeds. |
|
||||
|
||||
The manual `Win32_OperatingSystem` memory probe was denied once before a launch; no Unreal process
|
||||
was started by that probe. The harness's own native memory preflight succeeded on subsequent
|
||||
launches. No files outside the workspace were deleted.
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,131 @@
|
||||
# Rebuild step 2d research report
|
||||
|
||||
Date: 2026-09-30
|
||||
Branch: `rebuild/step2d-redefined`
|
||||
Scope: `Plugins/VoxelForge`
|
||||
|
||||
## Result
|
||||
|
||||
The editor-only research field passes the 259³, 25 cm ring target on six workers. Its final
|
||||
median is **308.177 ms** across three runs (295.325–317.807 ms), below both the 600 ms step-2
|
||||
bar and the 350 ms step-3 target. The old field measured 8,900.965 ms median on the same ring.
|
||||
Preparation, including the 135.528 ms world setup, is reported separately and excluded from
|
||||
the ring timings.
|
||||
|
||||
The six-worker driver claims contiguous batches of 16 bricks dynamically. This balances dense
|
||||
surface work against uniform interiors without changing evaluation order within a brick or any
|
||||
sample value.
|
||||
|
||||
The old and new density fields intentionally differ. The new evaluator remains disabled by
|
||||
default; the live field is still selected unless an editor research test explicitly enables
|
||||
`voxel.ResearchRedefinedDensityField`.
|
||||
|
||||
## Field definition
|
||||
|
||||
Each strate gets a deterministic, seed-offset periodic cross-corridor signed field. Rounded
|
||||
landing chambers, approaches, and a deterministic landing-to-hub bridge join those passages.
|
||||
Authored passage carving and floor geometry remain active. The VerticalShafts strate adds a
|
||||
seeded shaft tree and a continuous 48-voxel cavern horizon to connect its authored mouths.
|
||||
|
||||
Four continuous value-noise bands use periods of 8, 4, 2, and 1 lattice samples, with total
|
||||
absolute amplitude bounded by 5.25 density units. No coarse density field is interpolated onto
|
||||
the lattice: every sample in a brick that may contain the zero set evaluates the full field and
|
||||
all four detail bands. Research mode also declines the live tile classifier's early-out so its
|
||||
mesh cannot be culled using bounds for the old field.
|
||||
|
||||
## Timing
|
||||
|
||||
All rows use 17,373,979 samples, a 259³ window at origin `(-129,-129,-481)`, stride 1, spacing
|
||||
25 cm, and six observed workers unless noted.
|
||||
|
||||
| Field and path | Wall time | Worker CPU | CPU/sample | Bricks skipped | Samples skipped | Geometry evaluations/sample |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| Existing live field, six-worker median (range 8,890.120–8,923.020 ms) | 8,900.965 ms | 37.70 s | 2,170.09 ns | 0% | 0% | 6.734 |
|
||||
| Redefined field, all points evaluated, six workers | 467.212 ms | 2.313 s | 133.10 ns | 0% | 0% | 1.000 |
|
||||
| Redefined field, sound 8³ proofs, six-worker median (range 295.325–317.807 ms) | **308.177 ms** | 1.41 s | 80.94 ns | 60.54% | 63.06% | 0.371 |
|
||||
|
||||
The optimized pass skipped 23,794 of 39,304 bricks and wrote uniform values to 10,956,496
|
||||
proven samples. It evaluated full density at the remaining 6,417,483 samples. The 0.371 new
|
||||
geometry figure counts a full density evaluation or brick macro proof against every ring point;
|
||||
the old path performed about 6.734 room/tunnel/passage primitive evaluations per point. Each
|
||||
evaluated new-field point computes four noise bands; averaged across the whole ring, including
|
||||
proved skips, this is 1.477 noise-band evaluations per sample.
|
||||
|
||||
The final run's accumulated worker-phase timers were 1.79 s for full density evaluation, 0.04 s
|
||||
for uniform fills, and 0.01 s for brick proofs. Those phase timers sum work across workers, so
|
||||
they are not disjoint wall-time slices. The longest worker took 307.76 ms; measured parallel
|
||||
overhead after that was 0.42 ms. The remaining wall time is in full-detail evaluation of the
|
||||
6.42 million nonuniform samples.
|
||||
|
||||
## Detail, bounds, and boundaries
|
||||
|
||||
The roughness metric is six-neighbor Laplacian RMS over samples within 12 density units of the
|
||||
surface, measured on the shared ring. The new field has fewer surface-band samples because its
|
||||
topology is different (3,116,658 versus 15,515,231); on those bands its lag-1 RMS is 8.978 versus
|
||||
6.248 for the old field, and lag-2 RMS is 9.169 versus 8.646. A deliberately smoothed new-field
|
||||
control (detail scale 0) falls to 1.315 and 1.933. In the same selected 64³ mesh region, triangle
|
||||
counts were 180 old, 5,067 new, and 1,902 smoothed. The metric and mesh both detected the
|
||||
smoothing control. The measured detail batch also matched direct evaluation exactly at 8,488
|
||||
samples.
|
||||
|
||||
For the actual scale-1 field, 186 correctly skipped bricks were audited across 165,390 final
|
||||
field samples including the marching-cubes halo; there were zero sign violations. The bound
|
||||
includes the full 5.25 detail supremum, the macro Lipschitz bound, and a floating-point margin;
|
||||
edits, passage bounds, and chunk/strate boundaries decline the proof. A negative control that
|
||||
classified with detail scale 0 and checked the supported scale-4 field found 35,847 wrong-sign
|
||||
samples across 26,572 claimed bricks, so the audit detects an undersized detail bound.
|
||||
|
||||
Near-surface gradient-jump RMS was 3.420 at 8³ brick boundaries versus 8.694 in brick interiors,
|
||||
3.423 at chunk boundaries, and 3.225 at 128-sample macro-cell boundaries versus 10.036 in those
|
||||
cell interiors. These checks found no boundary spike consistent with stair-stepping. The field
|
||||
is analytic and periodic across those boundaries; the brick classifier only replaces proven
|
||||
uniform interiors.
|
||||
|
||||
## Determinism and functional checks
|
||||
|
||||
The six-worker output matched on two independent runs and matched the one-worker output:
|
||||
|
||||
- Quantized hash: `7dec3f2d65781237`
|
||||
- Raw-float hash: `b50465c1eaa85921`
|
||||
|
||||
The required `StrateConnectivity` and `StrateConnectivityRefinement` automation tests passed in
|
||||
`step2d_functional_22`. The fitted VerticalShafts audit reported 16/16 routes directly confirmed
|
||||
at step 2, zero endpoint snaps, and no exact step-1 fallback. Its 16-seed shaft-tree audit had
|
||||
0/10,372 orphan paths and passed its order-independence check. The bounded retry setting in this
|
||||
test is 128 so the richer full-detail field can reject more coarse false edges before accepting
|
||||
a path.
|
||||
|
||||
`PassageLandsInOpenSpace` passed in `step2d_functional_23` for its seven-passage fixture.
|
||||
`RedefinedFieldTraversalSeeds` passed in `step2d_functional_24`: over three seeds, 21 authored
|
||||
passage landings bracketed open air and 18 arrival-to-exit walking routes were checked; six
|
||||
routes used a resolved coarse endpoint only after a full-density segment check.
|
||||
|
||||
## Slices
|
||||
|
||||
Each image places the old field on the left and the redefined field on the right. Light is air,
|
||||
dark is solid, and cyan marks the near-zero surface.
|
||||
|
||||
- [Horizontal slice through the ring center](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Saved/Research/step2d/step2d_rebuild_12/slice_xy_center.png>)
|
||||
- [Vertical slice at Y=0](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Saved/Research/step2d/step2d_rebuild_12/slice_xz_y0.png>)
|
||||
- [Vertical slice at X=0](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Saved/Research/step2d/step2d_rebuild_12/slice_yz_x0.png>)
|
||||
- [Vertical slice through an authored passage](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Saved/Research/step2d/step2d_rebuild_12/slice_xz_passage.png>)
|
||||
|
||||
Machine-readable measurements are in [step2d_report.json](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Saved/Research/step2d/step2d_rebuild_12/step2d_report.json>).
|
||||
|
||||
## Scaling to 12.5 cm and compile risk
|
||||
|
||||
At 12.5 cm, double macro lengths and broad/medium noise periods in lattice units to preserve
|
||||
their world-space shape: the current noise periods 8/4/2/1 become 16/8/4/2 samples. Keep or add
|
||||
a 1-sample fine band so detail stays at least as rich as the 25 cm version. For a fixed world
|
||||
region, the lattice grows 8×, while surface-intersecting 8³ bricks should grow closer to 4×
|
||||
(surface area); remaining bricks still need only a cheap final-field sign proof and density
|
||||
writes. This scaling model is reasoned from the geometry and has not yet been timed at 12.5 cm.
|
||||
|
||||
The UE 5.7 Win64 Development build passed with `-ForceHeaderGeneration` and
|
||||
`-MaxParallelActions=4`. No UCLASS/UPROPERTY layout changed, so no generated-class layout risk
|
||||
was introduced. The sensitive code is the locally bounded landing-floor derivative and the
|
||||
supremum used by `TryClassifyBrick`; future field terms must update those bounds and keep the
|
||||
proof audit. The live path and ARCHITECTURE §8.10 were left unchanged when research mode is off.
|
||||
No `.uasset` file was edited. Nothing was committed, pushed, or stashed.
|
||||
|
||||
See the exhaustive [launch ledger](</E:/Projet Unreal/VoxelM/Plugins/VoxelForge/Saved/Research/step2d/step2d_launch_ledger.md>) for every benchmark, test run, failure, and prelaunch rejection.
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 7.8 KiB |