Cause found by profiling: room/SDF graph-cache churn (exact XY bounds + sample order rebuilt it constantly). Region grouping (one LOD3 tile: 1,084 -> 229 builds), taller-tile traversal for LOD6 (36,480 -> 12,160), extent-dependent cache slots, lambdas instead of recursive std::function. Serial ms/tile, 64 nearest tiles per level (before -> after): L0 50.4 -> 50.6, L1 112 -> 110, L2 24 -> 22, L3 223 -> 73, L4 269 -> 134, L5 393 -> 127, L6 283 -> 145. Hashes of all levels unchanged; parity 3,436/3,436 bit-identical. Full clipmap, 16 workers BelowNormal: full detail 66.6 s, all 12,054 tiles 175 s. In-stream L0 tail is high (median 83 ms, mean 198 ms, p95 669 ms) vs 51 ms serial. Streaming: recentre keeps the intersection, generates only missing tiles nearest/finest first; one crossing = exactly 441 new tiles in 6.1 s, no stale results. Lighting: normals already come from the density gradient; the striping was the squared headlamp; broad linear fill + warmer palette. Daytime CPU rule documented in Standalone/README.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
75 lines
5.1 KiB
Markdown
75 lines
5.1 KiB
Markdown
# Shared C++ library plan
|
|
|
|
This round keeps the standalone copy in place. The next extraction should make one implementation
|
|
serve both hosts; it should not turn the compatibility layer into a second public API.
|
|
|
|
## Library contents
|
|
|
|
Move the deterministic CPU pipeline into `VoxelForgeCore`:
|
|
|
|
- Plain voxel, tile, vector, bounds, and resolved-world input types.
|
|
- Deterministic math, noise, density operations, cave and passage fields, strate composition, and
|
|
the room/SDF caches they use.
|
|
- Marching-cubes tables and meshing, including the density-lattice gradient normals.
|
|
- Clipmap desired-set calculation and the shared density evaluation entry points.
|
|
|
|
Keep JSON and Unreal asset loading, material/actor creation, D3D11 and Unreal rendering, collision,
|
|
worker scheduling, cancellation policy, and per-frame mesh upload budgets in the host applications.
|
|
The cache-region traversal found in this round belongs with the density/meshing implementation.
|
|
The worker-local room graph cache is keyed by exact XY bounds and stratum. Grouping samples by the
|
|
legacy cache regions reduced a profiled LOD3 tile from 1,084 graph builds to 229. At coarser sizes,
|
|
plane-first traversal still thrashes the same XY windows across a tall Z run, so tiles wider than 16
|
|
chunks keep each XY column's Z samples contiguous. The profiled 64-tile LOD6 case fell from 36,480
|
|
graph builds (13.189 s in graph construction) to 12,160 (5.210 s), with the same density hash.
|
|
|
|
Cache capacity is also scale-dependent: a 32-chunk LOD5 tile benefits from two cached stratum
|
|
windows (26,926 to 9,700 builds across 64 tiles), while a 64-chunk LOD6 tile regresses with two
|
|
slots (12,160 to 23,104), so the current generator selects two only for 32-chunk tile contexts and
|
|
one otherwise. Replacing the room-graph `std::function` recursion then reduced profiled LOD5
|
|
RoomGraphBuild from 4.531 to 4.417 s and LOD6 from 5.210 to 5.038 s across the same 64 tiles,
|
|
without changing graph build counts or density hashes. These timings are profiling evidence, not a
|
|
cross-host benchmark. The shared API
|
|
should own the traversal and cache policy, with parity tests covering both traversal paths while
|
|
writing the same original density-grid indices.
|
|
|
|
The compatibility-map and allocator change did not move the original LOD0 run; those changes remove
|
|
known linear-lookup and heap-growth gaps, but stage profiling put the cost in density evaluation and
|
|
room-graph cache churn. The hot room-graph walks also used an owning `std::function` compatibility
|
|
alias for recursion; replacing them with generic self-recursive lambdas kept the same depth-first
|
|
order and removed repeated type-erasure/allocation overhead. Keep that code in the core rather than
|
|
carrying the UE-style callback alias into it. Viewer lighting and its palette remain host renderer
|
|
choices and stay outside the shared generator.
|
|
|
|
## Compatibility boundary
|
|
|
|
The core should expose standard C++ value types, explicit immutable resolved inputs, and owned mesh
|
|
results. No Unreal headers, reflection macros, `UObject`, Unreal containers, renderer types, or host
|
|
allocator should cross the boundary. The plugin adapter converts settings and loaded assets into
|
|
the resolved input snapshot, then converts the returned vertices, indices, and normals into Unreal
|
|
mesh types. The standalone JSON loader maps its exported snapshot into the same input snapshot.
|
|
|
|
Do not move scheduling into the library. Each host should pass an immutable generation context to a
|
|
worker and receive an owned result; this lets the plugin retain its task admission and game-thread
|
|
rules while the viewer retains its worker pool and bounded D3D11 upload pump.
|
|
|
|
## Plugin integration
|
|
|
|
Add a `VoxelForgeCore` C++20 target and make both builds compile or link that target from the same
|
|
source files. For Unreal, first prototype the Unreal Build Tool integration: either a small core
|
|
module that compiles the library sources or a platform-built static library with explicit build
|
|
rules. Prefer source integration if packaging a binary per Unreal platform would add maintenance.
|
|
The plugin then replaces its copied evaluator and mesher calls with the core API behind a thin
|
|
adapter. Preserve plugin-owned UObject resolution, task cancellation, collision, game-thread mesh
|
|
application, and upload budgets. The standalone keeps its JSON loader and D3D11 renderer.
|
|
|
|
## Preventing drift before extraction
|
|
|
|
Until the common target exists, keep a manifest of mirrored files and a sync/check script that names
|
|
the canonical source for each file, compares hashes, and refuses when both copies have changed.
|
|
Avoid hand-editing only one copy without recording that the edit is pending synchronization. After
|
|
each density or meshing change, run the 3,436-sample bitwise corpus and the selected-tile density
|
|
hash check. Keep `/fp:precise`; do not accept evaluator changes that alter density bits as a speed
|
|
shortcut. A standalone-only optimization must be marked as pending synchronization until it is
|
|
ported or explicitly rejected for the canonical core. Delete the duplicate standalone source only
|
|
after both host builds use the common target and the parity corpus passes through each adapter.
|