Owner-approved, on the A/B measured in 83a0e4d: the classifier plus exact validation cost more
than it saved on the game path. With it off, ready p95 is 182 vs 190-195 ms, generation p95 is
125-127 vs 139-141 ms, and density calls drop 18%.
- GVoxelForgeOuterClassifierMode defaults to 0. Mode 1 stays reachable via
voxel.OuterClassifierMode=1 as an A/B instrument; the clamp is now 0..1.
- Mode 2 (validation samples reused as the mesher core grid) is deleted: the reuse grid,
FVoxelMesherCoreDensityGrid, GenerateMesh's ReusableCoreGrid parameter, bValidationCoreReused,
TotalValidationCoreReuseTiles and its log field. It could not fire: it reused the grid only
when validation disproved a uniform verdict, which never happened, and Mixed tiles never
validate. The mode-1 validation loop exits early again on the first disagreeing sample.
- All three GenerateMesh call sites checked; the density-count pointer binds after BandVoxHi.
Verified by Codex, checked by me against the logs:
- build ok; the measured process loaded the staged DLL, SHA-256 1024D8CF..04D
- export OBJ SHA-256 unchanged: b3e5f4c3..9b377
- a run WITHOUT the override prints outer_classifier_mode=0 validation_density_calls=0,
proving the new default took effect
- 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 (applied by the
mesher, not by validation) was not hiding geometry.
Docs: ARCHITECTURE 8.10, CODEMAP (GenerateTileResult, ClassifyTile) and fable-idea T1.d record
that streaming no longer classifies by default. WORK-NEXT item 1 records the owner's playtest:
he can no longer fall through the world.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZS94UH8bSsBeg5sZaxe2Y
9.1 KiB
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_sends 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
LoadedTilesbefore 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 exactlyFVector::ZeroVector(VoxelWorld.cpp:1167,:1359) — a pawn at the world origin is indistinguishable from "no pawn", and the owner spawns at (0,0). ApplyMeshToTilehas 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.
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?