Motif vocabulary v3: version-1 records keyed by (seed, strate, stable ID) with route keepouts, band clearance and pairwise spacing. A rounded capped-cylinder monolith (r 1.4 m, 24 m tall) and a rounded-box plinth, smooth-unioned into a flatField floor with a 0.55 m geometric rounding (measured fillets 0.73-1.17 m). Exact radial/box interval bounds, a BVH, isTileShapeFree aware (0 brute-force misses, false-proof controls rejected). Final-field route passes; whole-surface audit covers foot, cap and platform edges. Cold whole view 0.524 s. Owner world unchanged. fast/medium/full PASS (Clang and GCC). Research R25 (self-checked): FloatingIslands LOD0 column cache conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
12 KiB
VoxelForge R25 — revised self-check report
Self-check
The earlier “heavy thrashing” conclusion was too strong. The conflict counts alone do not prove that the column cache caused most of the grid time.
I reread the complete cache and FI density helper functions cited below, plus the mesher fill and X8 branches and the instrumentation blocks. I did not read every line of the large enclosing GenerateMesh and World::generateTile functions end to end; I rely only on the cited branches. This was a read-only review: no scripts, tests, builds, binaries, or edits.
Cache mechanics and the access sequence
There is no separate local index function: IslandColumnCache computes its index inline. The cache key is XY, not XYZ. The equality check includes recipe and plan identities and hashes, seed, stable island ID, sample X/Y, and island/recipe shape parameters. It contains no Z. The cached value holds per-island XY-derived metrics—delta X/Y, radial distance, angle, lobe phase, and top surface—not the 3D density value. See FloatingIslandsInternal.h.
The local cache has four ways and 2,048 sets, or 8,192 entries per worker. The key-match lines include:
const bool bKeyMatches=Entry.bValid&&Entry.RecipeIdentity==&recipe
&&Entry.PlanIdentity==&plan&&Entry.RecipeContentHash==recipe.contentHash
&&Entry.PlanContentHash==plan.contentHash&&Entry.Seed==recipe.seed
&&Entry.StableId==island.stableId
&&Entry.SampleX==SampleX&&Entry.SampleY==SampleY
The exact way selection is:
IndexKey^=IndexKey>>33;
IndexKey*=0xff51afd7ed558ccdull;
IndexKey^=IndexKey>>33;
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))];
Those lines are at 519, 520, 521, 522, and 523. If a way is invalid, the first invalid way is used; otherwise the preselected hash way is overwritten. This is four-way set associative, not direct mapped, and replacement is deterministic by hash bits, not LRU. Lines 545, 546, and 551 define the local “conflict”: a miss where all four ways in that set are already valid.
FLazy::Get lazily allocates the local cache once per worker. Its entries persist across that worker’s tile jobs; the per-tile instrumentation is reset, but the cache contents are not. That makes the near-equality unsurprising: on 27.683M local misses, only 86k were to sets with an invalid way; the other 27.597M landed in full sets. A full set can contain unrelated old keys, so this counter does not establish that a useful key was displaced or soon reused.
The shared cache is a world-owned second level with 16,384 sets × 4 ways. Its key also excludes Z and includes recipe/plan identity and hashes, XY, and the island stable ID as context. On insertion it uses the first empty way, then round-robins through NextWay; its conflicts likewise mean “set full when inserting.” A shared miss is counted before taking the lock; if the second lookup finds a value, that call increments both misses and hits. Thus shared counters are not mutually exclusive. See SharedColumnCache.h, especially lines 120, 128, 131, 133, and 143–152.
For a concrete fill sequence, the mesher’s ordinary path has Z outermost, then Y and contiguous X batches; see VoxelMarchingCubesMesher.cpp. An X8 batch is not SIMD evaluation of the FI field: DensityAtLatticeX8 loops through lanes and calls NativeFloatingIslandDensityAt lane by lane. A logged tile has 6,125 X8 calls and 42,875 active lanes—seven lanes per call on average.
For a full 35³ sample grid and four islands, each point makes four XY-column lookups: 171,500 lookups. With Z outermost, the next Z plane revisits the same 4 × 35 × 35 = 4,900 island/XY keys after a plane’s worth of accesses. That is below the cache’s 8,192-entry total capacity, but per-set collisions and keys left by earlier tiles can still evict entries. The 4-way hash can also alias keys within a set. The code does not provide the set-occupancy or reuse trace needed to separate those causes.
The access sequence does not reproduce the aggregate counters by itself. For example, one logged tile with 42,875 samples has 147,201 local hits, 24,299 misses, and 19,713 conflicts—171,500 lookups, exactly four per sample. Repeating those tile counts across 512 tiles would give about 75.4M hits and 12.4M misses, not the measured 34.6M hits and 27.7M misses. That row is not representative of all tiles, but it shows why a single-tile sequence cannot explain the aggregate. See fi.log.
The aggregate ratio has a simpler explanation: the conflict counter is a full-set condition applied to misses, and both caches persist long enough for many target sets to be full. The remaining miss pattern is consistent with plane-first traversal, set collisions, prior tile contents, and other callers; the counters cannot say which dominates.
There are also important scope effects:
- The FI cache instrumentation is enabled for the whole tile generation and copied after mesh assembly, not only around grid fill. The X8 tile above has 188,864 local lookups—17,364 more than its 42,875 × 4 grid baseline—and 5,935 skirt density probes. Skirt probes call back into shape density and can reach the FI field.
NativeFloatingIslandDensityAthas a passage-mouth filter. Near the mouth and within its density threshold, it samples a 3×3×3 neighborhood (26 additional field samples), and may make up to six more fragment probes. Those calls also reach the column cache.- The 892k proof skips are cells, not necessarily density samples avoided. There is a separate
proof_density_samplescounter. One logged tile skips 8,192 air cells and 9,800 density samples; those are different units. The aggregate cell count alone cannot quantify grid-fill savings. - The four-island L55 plan does not activate the tile feature-query route:
candidateEvaluationUsefulis set only when the plan has more than four islands, and the scoped query ignores it otherwise. The ordinaryFieldDensitypath therefore scans all four islands and their connector groups. Each full island evaluation runs five root-lobe distances; for four islands that is 20 root-lobe evaluations per field point, plus the plan’s connectors. The root parameter constants are cached separately, but the point-dependent distances are recomputed. The lobe contour uses three sine harmonics varying with Z; I found no separate helix evaluator.
Cost split
| Quantity | Status | Reading |
|---|---|---|
| FI LOD0 total 29.710 worker-s; grid 27.610; skirts 1.163 | Measured | The grid is 92.9% of LOD0 total. The remaining 0.937 worker-s is derived. |
| Local lookups 62.259M; hits 55.5%, misses 44.5%; conflicts are 99.69% of misses | Derived | From 34.576M hits, 27.683M misses, and 27.597M conflicts. |
| Shared computes 3.733M; shared hits 23.951M; shared wait 150.689 ms | Measured | Most local misses are served by the shared cache. Wait time is about 0.55% of grid time. |
| Field arithmetic versus all cache lookup overhead | Not measured | The 150.689 ms is lock wait only. It excludes local probes, shared atomic reads, hashing, and other cache work. No counter times FI field math separately. |
The 3.733M shared computes are cached XY-column builds, not full FI density evaluations. The measured counts and code show substantial field work, but they do not support a defensible percentage split. My estimate is that the repeated island/root evaluation is substantial and may dominate; the 23.951M shared hits still take atomic read work, so cache overhead could also be material. No numerical field-versus-cache fraction can be justified from L55.
Ranked fixes
The savings ratings are qualitative; L55 did not measure any of these changes.
| Rank by potential worker-second saving | Fix | Risk and exactness |
|---|---|---|
| 1 | Coarse per-island sampling with refinement near the surface. It has the largest potential because it could reduce the number of expensive field evaluations. | High implementation risk. Interpolating coarse values changes density, normals, and likely hashes; exactness is not preserved unless the refinement scheme proves and samples every required value. |
| 2 | Exact per-island culling with the existing tile feature bounds, or a tighter per-island BVH. The current four-island plan takes the full-scan path. Testing the exact candidate/lower-bound path could avoid whole island/root evaluations for distant positive-density points. | Medium risk and exact if conservative bounds are maintained. Actual savings are uncertain; the code currently marks this path uneconomic at four islands. |
| 3 | Test XY-contiguous (Z-inner) traversal at LOD0 before changing the hash or ways. It would revisit each island/XY key immediately across Z, reducing the current plane-sized reuse distance. The current vertical traversal threshold is only used for tiles larger than 512 voxels; LOD0 is 32. | Low correctness risk: samples still write to their lattice indices. Strided grid writes may cost more elsewhere. It removes cache misses, not root-lobe work, so it may not approach the roughly 11 worker-s LOD0 target by itself. |
| 4 | Tighten exact interval proofs / narrow-band sampling. The proof path currently skips 892k of 16.777M LOD0 candidate cells (5.3%) and certifies no solid cells. Tighter intervals or smaller blocks may help. | Exact with conservative bounds, but current cell skips suggest limited headroom; smaller blocks also add classification overhead. The cell count is not a sample-savings measure. |
| 5 | Precomputed island distance volumes. Could help if values are reused across tiles or LODs. | High memory and build cost. Exact stored lattice values preserve exactness at those points; interpolated values do not. Sparse hierarchical storage is an option only if the volume is sparse enough to benefit; OpenVDB’s overview describes that trade space. |
| 6 | Per-tile 3D brick cache of exact island-local values. | Exact, but likely low benefit for the one-pass grid, where each 3D lattice point is usually sampled once. It is more promising if measurement shows repeated mouth-filter, skirt, or overlapping-tile queries. |
The local index already mixes XY and island identity before selecting a set, so I would not label it a simple aliasing-stride bug without a set/reuse trace. Increasing associativity may help, but expands the per-worker cache and increases probe work. Keep the shared cache in the first A/B: it serves most local misses, and its measured lock wait is small; the atomic hit path remains unmeasured.
Confirming measurement
Add one instrumented LOD0 run that tags FI field calls by caller—grid fill, skirt, passage filter, and boundary work—and separately records sampled cycles for IslandColumnCache local probes, shared reads/lock/build, and the remaining FieldDensity work. Add a small per-worker reuse/eviction trace for local misses, including set index and whether the key was seen before.
That will show whether the 27.6M full-set misses are soon-reused keys evicted by plane traversal, mostly one-off XY keys, or extra non-grid callers—and whether cache work or island/root evaluation consumes the 27.610 grid worker-s. No such attribution exists in L55, so the present cause ranking remains a hypothesis.