perf(gen): a thrashing one-slot cache cost 18.7x -- and the world is unchanged
TunnelNetwork density evaluation was 45x more expensive than Maze for identical work: 2,097,152 voxels, 64 tiles, same mesher, same machine. Profiling killed every hypothesis I brought to it -- box classification skipping 0/64 tiles was true but irrelevant (export bypasses classification), near-surface early-outs fired at 49.7%, the room cache built 340 entries for 2.74M samples, and the per-room overrides resolved once per sample rather than once per modifier. The cause was the wrapper, not the stack. A mesher tile samples an EXPANDED grid (-1..Cells+1), so one worker crosses several exact chunk coordinates while meshing a single tile. The generator's cache held only the last coordinate, so every boundary crossing rebuilt the resolved parameters, the whole 22-operator stack and the tunnel-core cache. On one representative tile: 3,675 refetches, 38.0s. The entire operator stack was 18.5s of a 4,272s total -- 0.43%. Replaced with a 128-slot direct-mapped thread_local cache keyed on owner, chunk, layout version, params fingerprint and generator type. Collisions force a safe rebuild, never a stale read. No shared mutable state, no order-dependent reduction. TunnelNetwork 344.5s / 164.267 us/vox -> 18.4s / 8.763 us/vox (18.7x) Maze 7.4s / 3.535 us/vox -> 1.08s / 0.513 us/vox ( 6.9x) same tile 38.0s -> 0.51s ; refetches 3675 -> 27 Output is BYTE-IDENTICAL, before vs after and across repeats, both archetypes: TunnelNetwork 50d6b4d0..f23126, Maze 364f7489..2fbe99. The permission to change the field was not needed. Also in this commit, from the two preceding tasks: - Explorer renders from the MESH instead of raymarching. It meshes the region once through the canonical UVoxelMarchingCubesMesher, then CPU-rasterises via a BVH. A viewpoint went from ~16 minutes to 0.155s; 8 views now cost 1.25s of drawing. No render pixel samples the density field. - One sampled grid and player-fit mask per run, shared by render/walk/export, instead of the same 368s walk computed twice. - A wall-clock budget (25 min default, hard 30 max) that stops cleanly and records truncated/truncated_during/completed_modes rather than overshooting. - ParallelFor across the measurement sampling; the suite was ~10% of a 16-thread machine. - -surfaceroughness= on the commandlet, and a surface-relief metric in export mode (edge lengths, dihedral distribution, face-vs-smooth normal divergence) so "does this look like rock" is a number, not a screenshot argument. - Smooth vertex normals in the rasteriser: FVoxelMeshData::Normals is the density-gradient normal the engine shades with, and the raster path was recomputing a face normal over it. - ScaleDiagnosis: baked candidate indices are now derived from the current corpus, so a legitimate field change stops breaking the test. NOT verified, and not claimed: - TunnelNetworkSpineEquivalence exceeded the 30-minute cap and was stopped. Inconclusive, not passing. The identical hashes are strong evidence the op-stack/legacy equivalence holds, but the test has not confirmed it. - CrossPlatformDigest passes but reports 2 samples within 1e-6 of the isosurface; FMath::Sin/Cos leaves Windows/Linux equivalence unproven. Pre-existing, not caused by this change. - The cache's memory footprint (128 entries x ~16 workers) is unmeasured. What the roughness sweep settled, for the record: SurfaceRoughness 0 -> 8 moves cost by 0.3% (163.2 / 163.2 / 163.7 us), so roughness was never the cost. It does reach the surface (dihedral p50 1.1 -> 2.2 -> 5.9), which kills my claim that the density-channel operator contributed nothing. TunnelNetwork's hard seams are present at roughness 0 (4.4% of edges past 40 deg vs Maze's 1.0%), so they are structural -- in how primitives are unioned -- not a roughness deficiency. More roughness makes them worse, not better. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
d97373c5a8
commit
e08b49a183
7 files changed
+2274
-450
No files matched your search
+28
-18
@@ -981,20 +981,23 @@ generic biome field), not just SurfaceWorld. Empty `Biomes[]` ⇒ all-zero colou
|
||||
- **Status:** C++ ✅ BUILT & WORKING (ticked 2026-07-27). The master material graph is still
|
||||
editor-side work and is deliberately NOT ticked — that half is Jahni's, not the code's.
|
||||
|
||||
### 8.16 Explorer commandlet — one authoritative field, three consumers
|
||||
### 8.16 Explorer commandlet — one authoritative field, one sampled grid, three consumers
|
||||
`-run=VoxelForgeExplore` lives in the separate `VoxelForgeEditor` module, so the commandlet and its
|
||||
PNG/JSON/OBJ writers are editor/commandlet-only and cannot enter a Game or Shipping link. It creates
|
||||
only transient settings, definitions, manager, generator, diff layer, and mesher objects; no authored
|
||||
asset, live manager, generator, or diff layer is mutated. The transient world keeps
|
||||
`WorldRadiusVoxels=0` and `InterStrateGapChunks=0`, preserving the lateral-region gate.
|
||||
|
||||
- **Render.** The renderer samples `UVoxelGenerator::GetDensityAt` at a fixed 0.25-voxel default
|
||||
step (overrideable down to 0.125), detects the MC density sign crossing, and refines it with ten
|
||||
bisection iterations. It never treats density as an SDF and never sphere-traces. Each of three
|
||||
deterministic views overlays a projected 1.76 m human-height marker; the JSON records the step,
|
||||
metre conversion, refinement count, and marker projection status.
|
||||
- **Render.** Render obtains the player-fit seed/mask from the shared measurement pass, then calls
|
||||
the canonical `UVoxelMarchingCubesMesher` once per 32³ tile for the bounded region. The aggregate
|
||||
mesh is merged in fixed Z/Y/X tile order, indexed into a deterministic CPU BVH, and rasterised
|
||||
from eight player-fit viewpoints with a depth buffer and fixed Lambert lights. The per-pixel path
|
||||
never calls `GetDensityAt`; `-renderstep` and the old bisection fields remain only as compatibility
|
||||
metadata. Each view overlays a projected 1.76 m human-height marker and the JSON records mesh,
|
||||
acceleration, per-view raster timings, and marker projection status.
|
||||
- **Walk.** `VF_MeasurePlayerFitWalkWithSampler` builds the existing step-1 capsule/floor-fit stencil
|
||||
and uses the same six-neighbour/full-resolution route gate as the existing diagnostics. Its
|
||||
and captures that exact sampled grid plus player-fit mask for the run. It uses the same
|
||||
six-neighbour/full-resolution route gate as the existing diagnostics. Its
|
||||
deterministic depth-first agent walk counts metres travelled (including branch backtracking),
|
||||
dead ends per 100 m, the arrival component's reachable fit volume, and traversed edges touching a
|
||||
narrow-gap proxy below 1.5× the 0.68 m capsule width. The proxy is explicitly axis-aligned air span;
|
||||
@@ -1002,19 +1005,26 @@ asset, live manager, generator, or diff layer is mutated. The transient world ke
|
||||
`MaxCells` refusal at a 32,000,000-cell default cap, with a conservative 20-byte/cell estimate
|
||||
against a documented 768 MiB working-memory budget. Reachable fit volume is explicitly the bounded
|
||||
fitted route window because `WorldRadiusVoxels=0` deliberately has no finite whole-world volume.
|
||||
- **Export.** `UVoxelMarchingCubesMesher::GenerateMesh` is the only mesher. Because that canonical
|
||||
entry point deliberately clamps one call to a 32³ tile, a 128³ request is four-by-four-by-four
|
||||
canonical calls whose returned vertices/triangles are streamed with an index offset; no second
|
||||
mesher or density evaluator exists. The bounded result is metre-space OBJ with source winding,
|
||||
voxel-space UVs, normals, and ground/ceiling material groups, accompanied by a manifest containing seed,
|
||||
archetype, slot, bounds, and player dimensions. Tile skirts are disabled at the export boundary
|
||||
because they are a render seam aid that would extend beyond the manifest box. A conservative
|
||||
preflight cap is checked before the canonical mesher is called.
|
||||
- **Export.** `UVoxelMarchingCubesMesher::GenerateMesh` is still the only mesher. Because that
|
||||
canonical entry point deliberately clamps one call to a 32³ tile, a 128³ request is four-by-four-
|
||||
by-four canonical calls. Their returned vertices/triangles are merged once and the same aggregate
|
||||
is consumed by both OBJ export and render; no second mesher or density evaluator exists. Export
|
||||
reuses the walk's grid hand-off when a walk/render mode is present. The bounded result is metre-space
|
||||
OBJ with source winding, voxel-space UVs, normals, and a manifest containing seed, archetype, slot,
|
||||
bounds, and player dimensions. Tile skirts are disabled at the export boundary because they are a
|
||||
render seam aid that would extend beyond the manifest box. A conservative preflight cap is checked
|
||||
before the canonical mesher is called.
|
||||
|
||||
- **Budget.** The default wall-clock budget is 25 minutes; `-budget=` may lower or raise it up to a
|
||||
hard 30-minute maximum. Setup, sampling, meshing, acceleration, rasterisation, and export check
|
||||
the budget and report `truncated`, `truncated_during`, and `completed_modes` while retaining any
|
||||
completed artifacts.
|
||||
|
||||
The report and export manifest use fixed-order writers and serialize twice in-process to assert the
|
||||
deterministic contract. Timings are console diagnostics only; putting wall-clock durations in the report would
|
||||
break byte identity. The browser viewer remains a separate display-only consumer: it must load this
|
||||
OBJ/manifest and never evaluate density or generate geometry.
|
||||
deterministic contract. The fixed tile merge, BVH ordering, and raster traversal are deterministic;
|
||||
timings are intentionally diagnostic metadata and do not affect world or image content. The browser
|
||||
viewer remains a separate display-only consumer: it must load this OBJ/manifest and never evaluate
|
||||
density or generate geometry.
|
||||
|
||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||
|
||||
|
||||
Reference in new issue
Block a user