481e7bbaeed515b3e11b9d2e27f2dbb8a740e0b6
169
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4c8d06fd8f |
fix(streaming): the player can walk from one strate into the next
Owner playtest: he could not walk from strate A into B. Forcing the pawn through in the editor showed the junction tunnel (and his edits to the lower strate) were there. Cause: UVoxelSettings::StrateContentCutMinLevel defaulted to 0, and DA_Settings does not override it, so the strate content band (the PAWN's strate) also cut LOD0, the collision tiles. While the player stood in A, the passage below A's seal, the gap and strate B had no LOD0 geometry, and the collision gate correctly refused to step onto missing collision. The band only moves once the pawn is inside B: a deadlock. bClampViewToStrate is a different setting, which is why toggling it did not help. Some historical fall-throughs were likely this, before the gate turned "fall" into "blocked". Fix (Codex; reviewed by me): - StrateContentCutMinLevel default 0 -> 1, ClampMin 1, and UVoxelSettings::GetEffectiveStrateContentCutMinLevel() clamps to [1,9] at every runtime consumer, so a legacy asset serialising 0 cannot bring the deadlock back. LOD1+ stay banded inside strates and go full in the gap. - A regression test, -voxel.TestStrateCrossing=1 (off unless passed): it walks the real manager passage control points with AddMovementInput and normal swept character movement, with limits of 10 s max gate, 120 s total and a 2-voxel floor tolerance. Old code: FAIL collision_gate_timeout at route 5/9 (10.0 s gated). Fixed: PASS reached_lower_strate, strate 0 -> 1, max gate 0.092 s. The trace shows band [-7..0] -> full in the gap -> [-17..-10]. Field unchanged: canonical export b3e5f4c3..9b377, capability 20,830 / 10,909. In game, the LOD0 change in the crossing scene is 11 transition-gap tiles going from 0 to 22,390 triangles; no LOD0 interior tile changed. COST, stated plainly (two interleaved runs, baseline vs fixed DLL): static generation CPU 34.1 -> 44.1 s (+29%), density calls +23%, LOD0 request p95 +8.9% moving generation CPU 78.8 -> 91.1 s (+16%), density calls +26%, LOD0 request p95 +3.1% applied triangles identical (669,834 / 1,331,482): the extra work is LOD0 tiles in sealed rock that the old cut skipped. The next round proves them solid instead of sampling them. Visual caveat: the old default of 0 was a deliberate July choice, because LOD0/1 tiles straddling a strate boundary mixed both strates' materials. With LOD0 never cut, that mixing may reappear near seals. The headless runs are -nullrhi, so this still needs checking in the editor. Launches: all serialized and waited by PID. Failures: one malformed first baseline attempt, one over-strict early harness assertion, and one run against stale staging (it logged CutMinLevel=0), all superseded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y |
||
|
|
7a972f513d |
fix(ablation): tunnel-core ablation no longer disables the fused evaluator
Salvaged from the parked exact-culling round (wip/exact-cull-20260914, 49369ac). This is the only
change ported. `if (!VoxelDensityAblation::IsTunnelCoreOff() && CP_UseOpStack)` becomes
`if (CP_UseOpStack)`: with the tunnel-core ablation on, the old condition also sent coarse tiles
to the scalar per-chunk cache path. That was the 2.9x "tunnel core off" anomaly in
|
||
|
|
6ccf3718ff |
measure(perf): per-component ablation of the TunnelNetwork path; exact worms restored
Round (Codex; reviewed by me against PERF-SAMPLED.md). Revert: voxel.WormNoiseMode / voxel.WormLatticeStep, their storage and their switches are REMOVED. VoxelWormField has one path again, exact Perlin with the N1 short circuit. Kept: bEnableWorms (asset), WormsForceOff (measurement only), the exact block skip (default 0). Ablation instrument: 14 development-only, world-changing force-off bits (voxel.TunnelAblate*), applied on the fused, op-stack/classifier, cached SDF/core and modifier/floor paths. They resolve once per process, are hashed into the generation fingerprint, and are compiled to zero in UE_BUILD_SHIPPING (VoxelDensityAblation.cpp). They are a measurement instrument, never a gameplay or multiplayer switch. Defaults exact: canonical export OBJ b3e5f4c3..9b377, worms-region 311CA42C / 75,482 triangles, capability 20,830 / 10,909, and 841/841 keyed tiles identical to NextHeadStaticTrace_20260913. Static worker seconds saved by removing ONE component (two off runs vs the interleaved all-on 33.06 s; baseline noise 0.27%): tunnel SDF 11.4%, cave warp 10.3%, passage carving 10.1%, landing posts 9.9%, passage structural posts 5.6%, worms 2.4%, native floor 2.2%, origin spine 1.6%, room SDF 1.6%, everything else <= 1.1% Moving (2+2 runs each) confirms the top five at 6.0-11.6%. Tunnel SDF off cuts LOD0 generation p95 104 -> 83 ms: it owns the slow tail. The positive savings sum to 58% of the static total; the other 42% is shared. The rows are not additive. Unexplained: tunnel-core OFF made the static total 2.9x SLOWER (95.5 s) while LOD0 generation got faster (p50 64.7 -> 60.6 ms) and triangles fell. The report blames the changed field, which does not fit; more likely a slow fallback path in coarse tiles. That row is not a cost measurement. Ledger: every Unreal process exited 0 and verified the loaded DLL; two PowerShell parser errors happened before any process started. No crashes or retries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y |
||
|
|
c132243b30 |
feat(worms): asset-owned worm off switch; offline stack symboliser -- cheap-worm default to be reverted
Worms round (Codex; reviewed by me against its report and PERF-SAMPLED.md).
Part 1, a real off switch: UVoxelStrateDefinition::bEnableWorms (default true, so existing assets
are unchanged). It is deliberately not in FStrateGenerationParams, so the 83-field memo-key guard
is untouched. It resolves after composer candidates, season vectors and boundary blending, is
included in the generation fingerprint, and clamps composer overrides, so a roll cannot re-enable
it. With strength 0 the fused path skips the worm block, the op-stack builder omits the worm
source, and the interval/classifier paths do no worm work. Proof: disabled == hand-zeroed control
byte for byte (70D310A4, 74,672 triangles), capability 20,856 / 10,917, connected, zero worm rows
in the symbolised sampler. voxel.WormsForceOff is a dev-only, world-changing measurement
override, included in the fingerprint.
Part 0: the sampler writes a module map (733 modules, identity checked), and a post-exit
offline symboliser (VoxelStackOfflineSymbolizer, editor module) rebuilds the all-LOD / LOD0 /
LOD1+ exclusive / inclusive / source-line tables. A planted cost was found (5 named rows) and then
removed. DbgHelp still never runs in the measured process.
Part 2, "cheap worms": FAILED to deliver, and the default is wrong. No candidate was faster:
lattice-2/4 gave no gain, and value noise was within noise static and ~2.1% WORSE in moving worker
seconds. Yet this commit carries voxel.WormNoiseMode=1 (value noise) as the default, a field change
(29/64 test tiles) with no benefit, selected after value-noise variants were tried until one
passed capability (three were rejected at 20,821-20,828). The next round reverts the default to
exact Perlin (mode 0) and removes the no-gain world-changing experiment modes.
The finding that matters: disabling worms entirely (WormsForceOff A/B, two interleaved runs each)
saves only ~2.5% static and ~3.4% moving worker time. The sampler's "worm line
VoxelGenerator.cpp:3985 = 19.4% of LOD0" was a misattribution of inlined noise code to one PDB
line. Line-level attribution of inlined code must be confirmed by ablation before a round targets
it. That is why the worm block skip (
|
||
|
|
11f0c1335b |
fix(diagnostics): sampler can no longer crash the run it measures; worm skip default off
Harness round. Every launched run is in Saved/HarnessFixAcceptance_20260914.ledger.txt.
- FVoxelStackSampler: two sampler-on runs had died in EndPlay (logs stop after "workers drained",
before the latency summary). The lifecycle teardown is now serialised, registration TLS is
tagged by session, stale worker registrations are cleared safely, and in-process DbgHelp
initialisation/symbolisation is REMOVED. The measured process now writes raw PCs only; symbol
resolution is deferred to after process exit. (An offline symboliser is still needed for
readable tables; it is queued first in the next round.)
- voxel.WormBlockSkip default 1 -> 0: it measured neutral (
|
||
|
|
26b34765d5 |
perf(generation): exact worm block skip with a proven 3D Perlin bound -- measured NEUTRAL
The sampler put the worm carve's first noise call (VoxelGenerator.cpp:3985, FusedWorm) at 19.4% of LOD0. The worm only carves where N1 + N2 < T with N2 >= 0, so where N1 >= T it contributes nothing, bit for bit. A block proven to have N1 >= T everywhere can skip both noise calls exactly. - The Lipschitz bound for this Perlin3D implementation: L = (15/4) * sqrt(3) * 1.25 = 8.118988160, in scaled-N1 units, with a 1e-3 float-rounding margin. Adversarial sampling found a worst ratio of 3.187 (0.39 L). The existing 2D floor-relief bound (8.5) is a component bound of a different field and was confirmed not to transfer. - A soundness test that can fail (VoxelForgeWormBlockSkipTest): 6,476 proved blocks, 355,945 exact lattice samples, 0 violations. A deliberately halved L proves a bad block; the full L rejects it. - voxel.WormBlockSkip, default 1. Field exact: export OBJ byte-identical (b3e5f4c3..9b377), 841/841 keyed tiles identical to NextHeadStaticTrace_20260913, capability 20,830 / 10,909. RESULT, stated plainly: no measurable speed gain. The skip rate is 4.07% of worm-eligible LOD0 samples and 0% at LOD1-4, because the bound is ~2.5x looser than the worst observed ratio at the block size used. Paired on-vs-off, two interleaved runs each: static worker +2.15%, generation p50 +1.8%; moving worker -1.42%, request p50 -2.5%. Within noise. The next round turns the default OFF. The proven bound and its test are kept for a tighter skip later (smaller or hierarchical blocks, or a per-cell bound). Run ledger (from Codex's report): the first build failed on an ambiguous test overload and was fixed; two timing runs are void because the orchestrator stopped the session so the owner could use the PC; the eight valid timing runs, the sampler run and the diagnostics runs completed. No new crash folders. PERF-SAMPLED.md now lists all eight earlier-round UECC crash records. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y |
||
|
|
5489b5e311 |
perf(generation): inline exact IsFinite -- LOD0 ~11-12% faster, field-exact
The sampler's last "unknown" LOD0 bucket (~13.8%) was ucrtbase.dll!_finite: FMath::IsFinite is an
out-of-line CRT call on the MSVC game path, hit from the innermost tunnel, passage, density,
spatial-index and meshing loops.
- VoxelMath::IsFiniteFast(float/double) in VoxelTypes.h: memcpy to uint32/uint64,
(Bits & ExpMask) != ExpMask. It is the same predicate for every bit pattern, with no FP
comparison for fast-math to fold. The disassembly shows it inlined (movd/movq, mask, compare).
- VoxelMath::IsFinite routes through a plain global, voxel.FastIsFinite (default 1), for the A/B.
The CRT call remains only on the switch-off branch. Hot generation-path uses are replaced; no
_finite / std::isfinite remains in Source/VoxelForge. Tool/editor/test code keeps FMath.
- A test that can fail: VoxelForge.Math.IsFiniteExact compares against FMath::IsFinite over 22
explicit float/double patterns (+-0, denormals, min/max normal, +-Inf, quiet and signalling NaN
payloads) plus 2,000,000 hashed patterns each: 0 mismatches. The explicit NaNs make a NaN-wrong
helper fail.
Field exact: 841/841 keyed tiles identical to NextHeadStaticTrace_20260913 (verdict/empty/
triangles), export OBJ unchanged (b3e5f4c3..9b377), capability 20,830 / 10,909.
Within-round A/B, game path, two interleaved runs each, sampler off (off -> on):
static LOD0 request p50 95.5 -> 84.8 ms generation p50/p95 70.3/114.1 -> 62.3/100.6 ms
worker 35.4 -> 31.6 s (-10.5%)
moving LOD0 request p50/p95 70.8/144.2 -> 62.0/126.1 ms
generation p50/p95 62.6/113.4 -> 55.1/101.0 ms
worker 102.6 -> 91.3 s (-11.0%)
Sampler-on moving: the _finite/CRT cluster is gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y
|
||
|
|
22186473f9 |
perf(generation): spatial index default on; the 10 s question and the unknown bucket, answered
Measurement round (Codex; checked against its report):
- Per-tile identity for ab4bbe3's always-on LOD0 changes: a fresh 841-tile HEAD trace matches
traces recorded before them (MemoStaticOn/Off), keyed by (x,y,z,level) with
verdict/empty/triangles compared. 0 differences, 0 missing; LOD totals identical, 669,834 in all.
- The "missing 10 s" is not the spatial index: all six TileCacheWindow x SpatialIndex
combinations are within ~1% for a given window setting. TileCacheWindow=0 at HEAD is not
2990009's path. It still thrashes at L3/L4 (p95 ~250 ms). The default beats
|
||
|
|
ab4bbe3680 |
perf(generation): field-exact LOD0 overhead removal; the unknown bucket is Core.dll
Field-preserving floor round (Codex; reviewed by me against the code):
- exact per-(sample, passage) caching of native/generic floor projections and walkable-air
checks, behind voxel.FloorRound1PassageProjectionCache
- a zero-work disabled path for VoxelDensityProfile::FScopedTimer, unchanged when enabled
- pointer / range-for iteration in the proven-hot tunnel, passage, spatial and candidate loops
(TArray RangeCheck was ~6.5% of LOD0 samples on the Development binaries the owner plays on)
- FVector2D temporaries replaced with scalar doubles, same operations in the same order
- FSurfaceColumn made trivial, so thread-local construction does no work. Verified safe:
ComputeSurfaceColumn writes all five outputs unconditionally before any branch, and every read
of Box.Cols is gated by Box.Computed
Verified: export OBJ byte-identical (b3e5f4c3..9b377); capability 20,830 / 10,909; all 8
interleaved game runs gave identical totals (841 tiles / 669,834 triangles static, 2,348 /
1,985,216 moving, the same as previous rounds). Per-tile identity for the always-on changes still
needs a check against a stored trace; that is queued first in the next round.
Game path (sampler off, two interleaved runs each):
projection-cache switch alone: neutral (static worker +0.41%, moving -0.05%)
vs the previous round (
|
||
|
|
cfad3d1c18 |
perf(generation): gate the tile cache window by LOD; LOD0 back to switch-off speed
2de603c's tile-sized room cache cut worker CPU, but LOD0 generation was ~9% slower. The per-tile
split shows the loss was evaluation (more candidates per sample in a bigger window), not cache
building. Policy, measured per path and LOD:
- fused path: tile window at LOD >= voxel.TileCacheWindowMinLOD (default 3)
- op-stack path: tile window at LOD >= voxel.TileCacheWindowOpStackMinLOD (default 4)
- lower LODs keep the 4-chunk worker window
- voxel.SpatialIndex: -1 auto (off at fused LOD0, on elsewhere), 0 or 1
Proof caveat closed: VF_ComputeTerrainOpEnvelope adds authored terrain-op extents (pits, chimneys,
detail overrides, ...) to the invariance envelope. Ops beyond the proven 256-voxel extent, or
malformed/unloaded ones, force the legacy window, so window invariance holds for every asset.
Sampler fix: the leaf symbol kept pointers into a TMap that FindOrAdd can rehash while later
frames resolve; the leaf is now re-found by program counter after insertion.
Game path, two runs each, sampler off (Codex; checked against its report):
LOD0 request p50/p95 generation p50/p95 worker s
static optimized 106.3 / 175.9 ms 76.6 / 124.3 ms 39.2
static switch-off 106.3 / 173.0 75.9 / 124.1 58.5 (-33%)
moving optimized 79.4 / 158.5 72.4 / 123.7 113.7
moving switch-off 82.8 / 160.4 73.2 / 125.4 141.9 (-20%)
LOD0 is flat within noise static and slightly better moving; worker CPU keeps most of the gain.
Field exact: 841/841 per-tile triangle identity, export OBJ SHA-256 unchanged (b3e5f4c3..9b377),
capability 20,830 / 10,909.
Caution: compare these numbers only within this round. The machine is in daytime use, so
absolute figures drift between rounds.
Current LOD0 map (sampler, moving, inclusive): tunnel SDF 12.5% + tunnel core 12.3% (the same
tunnel in two coordinate spaces); passage carving 11.1%, structural posts 11.4%, native floor
4.1%, with floor projection at 8.9% inside them; cave warp ~8% exclusive. Development-build-only
overheads: TArray RangeCheck ~6.5%, per-sample FScopedTimer ~2.6%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y
|
||
|
|
2de603cd77 |
perf(generation): size the room cache to the tile; spatial index; memo-key guard
Coarse tiles thrashed the per-worker 4-chunk room-cache window: 633 BuildChunkCache calls per LOD3 tile and 1,062 per LOD4 tile. This round was started by one Codex session and finished by a second, after the orchestrating session died at 06:06 and corrupted the first session's record. The second session reviewed and built its predecessor's half-finished tree. - voxel.TileCacheWindow (default 1): the room cache covers the requesting tile's footprint plus a fixed collect margin. It is set with TGuardValue around GenerateMesh and classify, so it never leaks to other work on the thread. Tile windows are capped at 64 chunks per side; unsupported/non-finite params fall back to the legacy window. - Window invariance, argued by Codex: graph decisions depend on at most two bounded MaxTunnelLength hops, and primitives are culled only once their complete bounds are known. So the collect margin 2*MaxTunnelLength + max(room reach, pair/chain/tube reach) does not grow with the window. Caveat: oversized authored terrain-op radii are not yet in the envelope check. - PrepareTunnelCoreCache reuses the graph, rebuilding only the exact support-column table for a crossed chunk. - FChunkSDFSpatialIndex: ordered XY buckets over room/join/tunnel bounds; rejection stays exact and within-bucket order is preserved. Only 9-13% of candidates reach exact evaluation. - static_asserts in VoxelStrateTypes.h: VF_STRATE_PARAM_FIELDS lists 83 fields, and their byte sum + 8 B padding == sizeof(FStrateGenerationParams). This closes the memo key's known gap (it catches omissions and size changes, not same-size replacements or padding-hidden fields). Verified (game path, two runs each, sampler off): - field exact: 841/841 per-tile triangle identity, 669,834 triangles; export OBJ SHA-256 unchanged (b3e5f4c3..9b377); capability 20,830 / 10,909, connected - cache builds: fused 139,187 -> 1,019 (-99.3%), op-stack 60,598 -> 879 - worker CPU: static 58.4 -> 40.8 s (-30%), moving 138.4 -> 121.2 s (-12%) REGRESSION, stated plainly: with the switch on, LOD0 gets slower. Generation p50 is ~9% slower, static 75 -> 82 ms and moving 71 -> 77 ms; request-to-ready p50/p95 is ~5-6% slower. This is consistent across both runs of both cases. The thrash only lived at LOD2+ (LOD0 had 1.66 builds per tile), so the next round gates the tile window by LOD and must bring LOD0 back to switch-off speed while keeping the CPU gain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y |
||
|
|
2990009f3b |
perf(generation): memoise room player-fit points -- 3.8x less worker CPU in game
The sampler (
|
||
|
|
8e40bdd92c |
measure(perf): in-process stack sampler; the game spends ~3/4 of worker CPU on room player-fit
A diagnostic stack-sampling profiler, off by default. A sampler thread captures the stacks of
registered generation workers with FPlatformStackWalk::CaptureThreadStackBackTrace every N us and
tags each sample with the tile's LOD. Symbolisation (inline frames included) happens after the
run, into a bounded CSV plus a summary. Registration is per tile task, never per sample; when the
sampler is off it costs one relaxed atomic load. Game: -voxel.SampleStacks=<us>. Commandlet:
-samplestacks[=us]. The streaming latency line now also prints applied tile, visible tile and
triangle counts.
Instrument checks (Codex, PERF-SAMPLED.md):
- a planted 20 ms busy loop was found at 10.5% inclusive, the expected share, then removed
- closure: 193k static / 251k moving samples, 0 capture failures, <0.75% wait frames,
leaf resolved for 82-84%
- static game output identical with the sampler on or off: 841 tiles, 447 visible, 669,834
triangles
- overhead at 1 ms: +4.7% static worker CPU; moving worker CPU within noise, moving timing +4-5%.
So trust large shares, not single-digit deltas.
The finding, on the game path (the owner's instruction: profile the game, not the export):
all-LOD inclusive static moving
BuildChunkCache 79.5% 80.5%
VF_FindPlayerFitPointForRoom 73.4% 74.0%
About three quarters of in-game worker CPU goes to player-fit searches for rooms. They are
recomputed on every per-worker room-cache rebuild, and coarse tiles span many cache windows. At
LOD0 the costs are the cave warp (3 Perlin per sample), the swept tunnel evaluated both in the
SDF and in EvaluateTunnelCoreWorld (~12% inclusive each, duplication confirmed), and the passage
structural tail (~11-14%). Candidate AABB rejection is cheap (1.5-2.2%). The 74-field params copy
does not show up as a separate cost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZS94UH8bSsBeg5sZaxe2Y
|
||
|
|
c65b015f01 |
perf(streaming): outer tile classifier off by default; delete mode 2
Owner-approved, on the A/B measured in
|
||
|
|
f2044135ea |
fix(mesher): stop forcing the nested block classifier on -- but I cannot reproduce the 3.5x
The code contradicted its own documentation. VoxelMarchingCubesMesher.h:150 says to leave
the optional 8^3-block refinement DISABLED in streaming, "repeating a proof on ..." --
and VoxelWorld.cpp forced `Mesher->bUseBlockEarlyOut = true` regardless.
It is now off by default, behind `voxel.UseBlockEarlyOut` (CVar + command line), so the
capability stays available for the focused diagnostics where it was measured to help.
⚠️ THE HEADLINE NUMBER DOES NOT REPRODUCE, AND I AM NOT CLAIMING IT
Sol's second pass read a 3.5x from the attribution table:
classifier disabled 587.991 ms
classifier enabled 2,056.596 ms
My own clean A/B on this flag, quiet machine, 3 runs each:
nested OFF 0.597 / 0.613 / 0.607 s mean 0.606
nested ON 0.637 / 0.599 / 0.605 s mean 0.614
Identical within noise.
So either the 2,056 ms control exercised more than this flag does, or the 3.5x lives on
a path the 128^3 export does not take. PERF-ATTRIBUTION.md describes that figure as "the
separate classifier control", which is not obviously the same lever. Until the game-path
A/B is run, treat the 3.5x as UNCONFIRMED.
What this commit is worth on its own: the code now does what its documentation says, the
behaviour is switchable rather than hardcoded, and geometry is unchanged --
SHA-256 b3e5f4c3..9b377 identical with the flag on, off, and against
|
||
|
|
bb8ec51939 |
measure(perf): the remaining 4.3x is ALL density evaluation -- and the classifier costs 3.5x in game
Measurement only; no density or meshing decision changed. Full table in
PERF-ATTRIBUTION.md. The profiler self-check finally passes honestly: profiled 606.128
vs unprofiled 602.341 ms, **+0.63%** (it used to inflate 11.5x).
THE ATTRIBUTION, and it is almost embarrassingly one-sided
mesh ledger, additive, sums to the clock exactly (sum_matches_mesh = true)
shared density-grid fill 590.923 ms 97.4913%
tile parallel wall 10.972 ms 1.8102% cells, interp, normals, streams
tile merge 4.232 ms 0.6982%
pre-tile setup / prep 0.002 ms ~0%
remainder 0.0001 ms ~0%
------------------------------------------------
total 606.128 ms
GetDensityAt parent: 580.184 ms worker-wall, nested inside that 590.923 ms.
⇒ **The mesher is exonerated. Marching cubes, vertex interpolation, normals and stream
building are 2.5% combined.** The whole 4.3x is density evaluation, which is where three
of us have been looking and none of us had proven it.
WHAT IS ACTUALLY BEING DONE, per 2,248,091 samples
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
~31 candidates SCANNED per sample, ~3.1 evaluated
fused detail ran on 1,079,777 and skipped 1,168,314 samples (52.0%)
software tunnel cache: 2,248,043 hits / 48 misses -- not the problem
The post-stack structural tail, which I have suspected twice, is small: StructuralTail
20.9 ms CPU / 1.9 ms wall, DensityStructuralPosts 21.9 / 2.0, PassageStructuralPosts
10.5 / 1.0 -- all nested samples against a 590.9 ms wall.
⭐⭐ AND THE FINDING THAT MATTERS MOST FOR THE GAME
clean export, classifier absent 587.991 ms
same export with the classifier 2,056.596 ms
The classifier makes generation **3.5x slower**, it is ABSENT from the export path we
have benchmarked for five days, and the GAME PATH USES IT. Its control route runs
211,289 support-column builds, 2,171,763 candidates, 1,370,314 floor queries and
2,296,243 checks -- none of which the fused export route executes at all.
⇒ Every optimisation this week was measured on a path that skips the single largest
cost the player actually pays. That is a measurement-design failure and it is mine.
Also newly visible: the full export wall is 1,004.557 ms, of which mesh is 606.128 ms
(60.3%) and OBJ WRITING is 394.273 ms (39.2%). My "0.556 s" figures have always been
mesh_seconds alone.
One real hot-path improvement landed with the instrumentation: AreCountersEnabled()
returned an unconditional `true`, so counter hooks were live in shipping builds. It is
now a relaxed load gated on diagnostic mode.
VERIFIED BY ME: build 0 errors; export 0.552 / 0.555 / 0.566 s (
|
||
|
|
cf0319d480 |
perf(streaming): item 3 bought nothing, item 5 removed dead weight -- both worth knowing
Two WORK-NEXT items, one null result and one confirmed removal. Item 4 untouched.
ITEM 3 -- CANCEL OBSOLETE WORK AND PRIORITISE THE FLOOR: NULL RESULT
Clean -nullrhi, diagnostics off, 800 cm/s, 200 m of travel:
before after
request->ready p95 0.369117 s 0.367612 s
queue wait p95 0.066949 s 0.064483 s
generation p95 0.287926 s 0.283642 s
p95 travel distance 2.952938 m 2.940894 m
obsolete aborts 0 12
Flat. Aborting obsolete tiles, sorting from the true pawn position and prioritising the
occupied/next tile changed request-to-ready by 0.4%.
⭐ AND THE NULL RESULT IS ITSELF THE FINDING. Look at the split: queue wait is 65 ms of a
368 ms p95; generation is 284 ms. **The queue was never the problem, so reordering it
cannot help.** Only 12 tiles in a 200 m run were obsolete enough to abort. The floor is
late because generation is slow, not because work is badly ordered -- item 3 establishes
that by exclusion, which is worth more than the 0.4%.
The machinery is kept: it is correct, it costs nothing, and it will matter once
generation is fast enough for ordering to be the binding constraint.
ITEM 5 -- VERDICT CACHE REMOVED. It was pure overhead.
Clean moving-session audit:
outer calls/probes/hits 2,109 / 2,109 / 0
nested calls/probes/hits 19,904 / 19,904 / 0
enclosing hits 0
stores 15,877, evictions 15,365
⇒ It stored 15,877 proofs, evicted 15,365 of them, and hit ZERO times. A moving player
visits unique regions; the cache was filling and thrashing a 512-entry table under a
global lock for no benefit. Same-binary A/B: request p95 0.367645 vs 0.365978,
generation p95 0.285559 vs 0.285992 -- neutral within noise, so its cost was the lock and
the linear scans rather than time we can see.
Removed: the global cache, its lock, the entry table, the invalidation path, the
cache-only profiler fields and the synthetic probe. The proof classifier stays.
⚠️ I asked for this cache two days ago on the strength of a commandlet measurement that
re-visited the same region repeatedly. That access pattern is nothing like a moving
player, and I should have said so when specifying it.
VERIFIED BY ME: build 0 errors; export 0.563 / 0.565 / 0.566 s (
|
||
|
|
35de59bac5 |
perf(opstack): fused evaluator -- 1.05s to 0.56s, byte-identical, and the ~70% early-out is back
WORK-NEXT item 2. The authored operator graph is lowered at chunk-cache build time into a fused scalar evaluator for canonical TunnelNetwork/Underwater stacks. Operators are untouched; this is a lowering, not a de-composition. Custom recipes, composer/lateral stacks and other archetypes stay on the interpreted block path, selected by a build-time EVoxelOpFusedEvaluator marker with `-voxel.UseFusedEvaluator=0` to A/B it. ⭐ MY OWN A/B, same build, 3 runs each: fused 0.545 / 0.563 / 0.561 s mean 0.556 interpreted 1.066 / 1.036 / 1.033 s mean 1.045 -46.8%, 1.88x geometry SHA-256 b3e5f4c3..9b377 IDENTICAL across fused, interpreted, and |
||
|
|
081cc5e459 |
fix(streaming): collision-ready gate -- the pawn waits for a COOKED floor, not a submitted one
WORK-NEXT item 1, plus all three of item 6. Item 2 (the fused evaluator) untouched as
instructed.
⭐ RMC DOES EXPOSE WHAT WE NEEDED, so the gate rests on a real signal:
OnCollisionBodyUpdated (delegate -- diagnostic)
SetCollisionConfig() -> TFuture<ERealtimeMeshCollisionUpdateResult> (AUTHORITATIVE)
RealtimeMeshComponent was READ ONLY, per the owner's grant. Nothing there was modified.
THE INVARIANT
Until now nothing guaranteed a tile the player occupies has collision. apply_s ended at
mesh SUBMISSION; RMC cooks asynchronously and we recorded no completion, so no
measurement this project has ever taken said when physics could actually collide.
- per-tile CollisionReadyTiles state, with stale-callback protection
- HandleTileCollisionCookComplete driven by RMC's cook future
- pawn gated at BeginPlay and pre-physics on the FEET-SUPPORT tile -- deliberately not
the actor-centre tile, because the floor that catches a falling pawn is what must be
cooked -- plus the predicted movement path and solid/air traversal
- movement-component state saved/restored, so a temporary stream wait cannot
permanently change a character's movement mode
- new trace fields: mesh_submit_to_cook_s, collision_request_to_cook_s,
body_event_to_future_s -- three boundaries where we had one blurred number
ITEM 6, all three:
- LoadedTiles is now recorded only after SUCCESSFUL mesh submission, and a failed
mesh init retries. Previously a failed init left a tile marked loaded forever -- a
permanent hole.
- origin spawning fixed (the zero-position guard made a pawn at exactly
FVector::ZeroVector indistinguishable from "no pawn" -- and the owner spawns at 0,0)
- exact core-lattice validation for uniform whole-tile verdicts, which previously had
no safety net while block verdicts did
⚠️ I FIXED TWO COMPILE ERRORS MYSELF rather than spend another Codex round on them:
`GetMovementMode()` / `GetCustomMovementMode()` do not exist on
UCharacterMovementComponent -- MovementMode and CustomMovementMode are public fields.
Two lines. Codex did not build this round, so it could not have caught them.
VERIFIED BY ME: build 0 errors after the patch; export 1.022 / 1.055 s (
|
||
|
|
aa62971047 |
tools(trace): Play-to-steady-state timeline -- startup is 83ms, the other 8.2s is tiles
Measurement only, no optimisation, per the instruction. First time anyone has looked at
the path BEFORE LoadTile, and first time any measurement has used the owner's REAL
assets instead of the commandlet's synthetic strates.
Trace: Saved/StartupTraceRealFinalTrace.startup.jsonl -- 873 JSONL records, 268 KB,
841 tile records, bounded at 4096 with zero suppressed. Structured, own file,
stream-parseable; the 13 GB log pile taught that lesson twice.
TIMELINE
BeginPlay 45.585 ms
StrateManager.Initialize 29.018 ms
GeneratePassages 6.628 ms (5 passages)
first BuildDesiredTiles 2.710 ms
------------------------------------------
all startup stages ~83 ms
BeginPlay -> steady state 8.216 s
process launch -> stop 20.164 s
⚠️ My hypothesis was wrong: I expected layout resolution or GeneratePassages to be a
hidden one-off cost. They are 83 ms combined. **The 8.2 seconds is tiles, all of it.**
A further ~2.05 s sits between BeginPlay finishing and the first desired-set build, and
the trace attributes that to engine/map/player startup, not to us.
REAL ASSET RESOLUTION -- two things worth the owner's attention
settings /Game/VoxelForge/DA_Settings, seed 0
⭐ five slots, ALL resolving to /Game/VoxelForge/DA_Strate3
InterStrateGapChunks 2
MaxClipLevel 4, ClipRadius 3, RenderDistanceChunks 0
bFarSheetRing true, but NO sheet tiles exist because render distance is 0
⭐ bEnableDensityVolume FALSE -- already off, no volume object, no GPU upload
DA_Strate1 (the 10.7 KB surface strate) and DA_Strate2 (rooms + water) are not in play
at all. Every strate in the world is Strate3.
THE DESIRED SET -- 841 tiles, and the shape is the story
LOD0 343 tiles 8 m each 56 x 56 x 56 m
LOD1 218 16 m 112 x 112 x 80 m
LOD2 120 32 m 224 x 224 x 96 m
LOD3 80 64 m 448 x 448 x 128 m
LOD4 80 128 m 896 x 896 x 256 m
⭐ LOD4 covers 896 x 896 x 256 m -- about 205 MILLION cubic metres of cave -- and its
verdicts are 0 AllAir / 0 AllSolid / 80 Mixed. Not one LOD4 tile can be skipped, and
each costs 127.5 ms p50 / 291.6 ms max to generate. That is roughly 12 s of CPU spent on
geometry nobody can see from inside a cave.
TILE DISTRIBUTIONS (p50 / p95 / max ms)
LOD tiles verdicts A/S/M req->ready generation mesh
0 343 98/123/122 44.9/420.8/819.5 9.6/358.2/767.6 0/341.6/726.1
1 218 49/83/86 31.1/148.9/188.6 8.7/134.3/164.2 0/117.8/151.1
2 120 0/39/81 34.0/61.6/76.9 10.9/43.6/63.7 9.9/40.4/60.5
3 80 0/2/78 46.4/85.9/96.3 38.5/80.7/87.8 31.0/67.5/70.7
4 80 0/0/80 148.2/272.8/304.2 127.5/253.8/291.6 127.4/253.8/291.5
totals: 147 AllAir, 247 AllSolid, 447 Mixed, 669,834 triangles
apply never exceeds 0.43 ms at any level
zero classifier-cache and region-cache hits on a fresh process, as expected
⇒ The two costs are LOD0's 122 meshed tiles (~360 ms p95 each) and LOD4's 80
unskippable tiles (~150 ms each). Apply, queue and startup are all noise beside them.
VERIFIED BY ME: build 0 errors; export 1.034 / 1.089 s (
|
||
|
|
5ddd96664d |
perf(opstack): invert the loop + prune per block -- 1.45s to 1.02s, byte-identical
The architecture round. Loop inverted from per-sample-per-op to per-op-per-block, operators pruned per block by interval bounds, profiler moved out of the hot loop. my build, 3 runs: 1.013 / 1.036 / 1.075 s ( |
||
|
|
adb34dffa9 |
feat(passages): passages are D-tunnels now -- legacy support writer fires ZERO
The owner was right and said so plainly: "the tunnels should be a flat D with then a
bit of noise added, as it's most basic shape -- this alone should make all tunnels not
over X steepness to be walkable." Codex had reached the same conclusion from the other
direction ("the native fix is to author the inter-strate passage floor profile during
passage construction"); it had measured the current position while he described the
destination. This is that refactor.
Walkable inter-strate passages now bake their floor profile and bounded relief during
GeneratePassages. Evaluation composes a D-shaped SmoothMax floor, with final MC
ownership after the generic writers -- the same ordering fix that made the tunnel floor
native in
|
||
|
|
4f8b4a9a27 |
fix(gen): the phantom terracing is the PASSAGE support writer -- plus a region verdict cache
The owner played the build and reported four things. Three are answered with evidence, one is disproven, and my own leading hypothesis was wrong. ⭐ 1. THE PHANTOM TERRACING IS NOT MARCHING-CUBES ALIASING He reported stairs surviving with bTunnelFloorTerracingEnabled = false. I suspected MC lattice aliasing on a near-planar floor; I was wrong. An A/B over the same mesher, counted in pixels: graph-tunnel floor removed 2,813 / 147,456 px changed 1.91% passage support removed 25,695 / 147,456 px changed 17.43% ⇒ The bands are the INTER-STRATE PASSAGE SUPPORT WRITER, which is independent of the tunnel terracing flag. That is exactly why turning terracing off did nothing: he was disabling the wrong system. FCaveTerraceMod is inactive (TerraceStepHeight=0) and was never involved. ⚠️ It cannot simply be switched off. With it disabled: fit 20,804 -> 20,511, walk-reachable 10,884 -> 7,525, components 260 -> 272. Route still connected, but a third of reachable space gone. It stays as a correctness backstop. The native fix is the same pattern we just applied to tunnels: author the inter-strate passage floor profile during passage CONSTRUCTION. Codex judged that refactor too large to risk this round and said so rather than attempting it. MC aliasing remains theoretically possible at shallow angles (Lorensen & Cline 1987), but the same-mesher A/B proves our writer dominates. Dual Contouring (Ju et al.) was considered and rejected as a disproportionate mesher/topology change. 2. THE ROUGHNESS COST DEFECT DOES NOT EXIST -- my hypothesis, cleanly disproven FBM 2.91s / 66.92% skips · Ridged 2.85s / 66.53% · Mixed 2.92s / 66.82% Cellular 2.99s / 66.43% · FBM frequency 0.025-0.5: 2.88-2.99s, 66.80-67.04% I predicted a shared bound across noise types collapsing the classifier. The code already carries separate conservative bounds per type. No roughness code was changed. ⇒ Whatever the owner saw is not steady-state export cost. Most likely the in-editor regeneration a parameter change triggers, which is the full-rebuild cost, not the per-sample cost. Worth confirming with him. 3. ROOM MOUTH RISE -- his idea, implemented, off by default RoomMouthRiseBlendVoxels, default 8.0 blend 8 fit 20,804 reach 10,884 40,429 v / 75,416 t capability-NEUTRAL blend 24 fit 20,792 reach 10,874 40,337 v / 75,232 t REJECTED, loses capability Relief preserved at blend 8 (dihedral p50 2.5584, p90 33.7191). The chosen camera did not see a mouth bench, so no visual claim is made. 4. ⭐ VERDICT CACHE WITH REGION REUSE -- answers "why re-prove what we proved?" 130 classification calls -> 1 store, 129 hits, of which 128 ENCLOSING-REGION hits. The region level is the one that matters: deep rock is empty thousands of chunks at a time, and one proof over a large box now serves all of them. Soundness, which is the whole risk (a stale "empty" silently deletes geometry): 512-entry bounded LRU; stores ONLY AllAir/AllSolid, never Mixed; key includes seed, layout/lifetime id, params fingerprint, diff version, density/seal settings, support mode, lattice step, origin and extent; dynamic season/composer/terrain-op/biome configs FAIL CLOSED and bypass reuse. Deliberately session-resident, not on disk -- cross-process persistence cannot serialise all asset/layout/edit state safely yet. VERIFIED BY ME: build 0 errors; export 1.353 / 1.430 / 1.432 s ( |
||
|
|
2260c4d47a |
perf(gen): the floor is finally NATIVE -- clamp 29,634/29,634 -> 0, and the 551ms empty tile is dead
Two targets, both hit, and the second is the biggest in-game win since the interval work. 1. THE FLOOR IS NO LONGER PATCHED IN The audit proved the backstop was doing the work: TunnelAuthoredFloorProtectionClamps fired on EVERY authored floor sample while the named fallbacks fired zero. The cause, in its own words, was that the stack composed the floor before the common disturbance/passage writers, so every sample looked like a post-hoc clamp. The floor is now composed ONCE, after those writers, at the final MC boundary. The clamp inside the operator stack is deleted; the metadata stays on the sample for the hand-off and for diagnostics but may no longer modify density. clamp fires 29,634 / 29,634 -> 0 player-fit cells 20,804 (unchanged) walk reachable 10,884 (unchanged) connectivity arrival -> departure, Connected geometry SHA-256 b3e5f4c3..9b377 IDENTICAL to |
||
|
|
5156084cac |
audit(gen): which post-hoc ops are tunnel-floors in disguise -- and the X+2A budget is unsound
The owner's framing for the night: "if it sees performance opportunity by unifying
some process that should be, or like the tunnels, have it be native more than patching
it after generation". This is the audit. No generator behaviour changed; the only code
here is request/generation timing instrumentation in VoxelWorld.
STRUCTURAL-POST AUDIT -- verdict per operator
FBoundarySealOp 0.000011 us/sample KEEP. A boundary condition, not a workaround.
FXYEdgeSealOp 0.000054 us/sample KEEP. Domain constraint; identity today with
WorldRadiusVoxels=0.
FOriginSpineOp 0.000588 us/sample PARTLY family, but origin anchoring and
ordering are special.
FPassageCarveOp 0.019977 us/sample Could share the volume/floor representation,
but its boundary-crossing order and
current-density carve are load-bearing.
floor protection 0.061298 us/sample ⭐ THIS IS STILL THE REAL PATCH.
TunnelAuthoredFloorProtectionClamps fired 29,634 / 29,634 -- every authored floor
sample -- while the named support/room fallbacks fired ZERO. So the backstop is not
a rare safety net; it is doing the work, and the tunnel floor is not yet native.
⭐ AND IT NAMED THE PATH OUT, which I had not seen: the floor is authored at build time
but LATER WRITERS can still refill or erase the clearance, so a clamp re-asserts it.
Native means either composing the floor AFTER all writers, or making every later
writer floor-aware with a proof it cannot violate the invariant. Removing the clamp
today would be unsafe. It did not remove it.
PRIMITIVE FAMILY -- valid, but my proposal was too crude
Algebraically sound: solid + floor via SmoothMax + bounded relief + smooth union.
Rooms, swept tunnels, landing rooms and some passages can share a cached
walkable-volume DESCRIPTOR (shape/profile, baked floor, relief params, mouth/room
ownership, capability metadata).
⛔ But they should NOT become one undifferentiated operator: tunnels are swept
multi-segment chains, passages cross structural boundaries, origin landings have
special finite geometry, seals are domain conditions. Flattening those would WEAKEN
interval proofs and operation ordering -- the opposite of what I hoped unification
would buy. Renaming the passage carve would not help either; it Lerps against
incoming density, so its global bound stays unbounded regardless.
X + 2A CLEARANCE BUDGET -- SETTLED, AND IT IS UNSOUND AS SPECIFIED
A surface-displacement budget needs BOTH an upper bound on field perturbation AND a
lower bound on the gradient near the zero surface. The second fails at flat or
degenerate crossings, and the current-density Lerp/Min operators make a global bound
unavailable. Perlin derivative bounds alone do not fix it.
⇒ Usable only inside separately proven primitive regions; elsewhere the validator and
backstop must stay. The owner and I both assumed interval propagation already gave
us this. It does not, and now we know why rather than just that.
ITEM ZERO -- the suspected in-game regression: NARROWED, NOT CONFIRMED
Queue wait is real, but meshed latency is primarily GENERATION time, not queue. One
EMPTY LOD0 generation took 551.7 ms, so it is not pure queue saturation either.
RoomGraphSource measures 0.278939 us/sample, which cannot explain a 400-1000 ms mesh.
Codex declined to call it a confirmed code regression on this data. Still open, and
it now points at the known-unsolved LOD0 mixed-classifier fallback rather than at the
floor work.
REFERENCE ARCHETYPE -- costs, and a correction to my arithmetic
CorridorRadius 12.5 voxels is a 6.25 m DIAMETER (I said 3.1 m), and validation caps
it at 16 voxels. The 15-20 m reference hall needs a separate gallery archetype or a
larger cell-size budget; raising the value violates current lattice/wall assumptions.
FCaveTerraceMod is a true no-op disabled (0.000531 us/sample); enabled it adds two
cached SDF probes, estimated lower bound ~0.56 us for a qualifying sample -- estimated,
not measured. Columns belong to slab generation; TunnelNetwork needs a baked column
op. Water is presentation, not rock density, and null-RHI cannot price it.
Build 0 errors. No RMC files, links, historical checkouts or logs touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
|
||
|
|
8e42d6d807 |
feat(tunnels): floor authored at BUILD time, room clips tunnel, climb tier added
The round the owner asked for, with three corrections from Codex that are worth more
than the features.
WHAT LANDED
- The complete tunnel floor profile is authored into FCachedTunnel during
BuildChunkCache, not derived per sample. This was his point: "it should be done at
creation time, since it knows its destination."
- Room floor ownership blends/clips the tunnel floor at mouths -- his A > B boolean.
- The traversal validator gained a climb tier, structured exactly as agreed:
bWalkOnlyCanReachDeparture / bClimbCanReachDeparture kept SEPARATE, with
WalkOnly / Climb / MineRequired cell counts reported alongside.
The comment in the header states the contract: "The primordial capability contract
is walk-only... climb is reported as a separate tier and never makes the hard
connectivity verdict pass." That is the distinction we wanted: reporting tiers, not
permission.
- Floor controls exposed under Cave Morphology|Tunnels|Floor.
⚠️ CORRECTION 1 -- LARGE LEDGES ARE NOT CAPABILITY-SAFE YET, so they stay opt-in.
default (preference 0) fit 20,804 walk reach 10,884 0 ledges
preference 2 fit 20,765 walk reach 10,712 360 ledges
Ledges cost 172 walk-reachable cells. Codex kept preference 0 (the old staircase) as
the default rather than shipping the owner's preferred look at the price of walkable
space, and led its report with that. Correct call.
⚠️ CORRECTION 2 -- THE BACKSTOP STILL CANNOT GO. Support/room fallback counters were
zero, but the structural protection clamp fired on 31,017 of 31,017 authored floor
samples -- 100%. So the shape is still not correct by construction; the clamp is
doing the work. Instrumenting it was the right move: we now have a number instead of
an argument.
⚠️ CORRECTION 3 -- THE "X + 2A" CLEARANCE BUDGET IS NOT IMPLEMENTABLE AS I SPECIFIED.
Interval propagation bounds FIELD VALUES, not SPATIAL DISPLACEMENT. Deriving A needs
a proven Lipschitz/influence bound for every post-operation, which we do not have.
Both the owner and I assumed the interval work already gave us this; it does not.
The idea is still right, the prerequisite is a displacement bound.
MEASURED BY ME: build 0 errors; export 1.365 / 1.357 / 1.420 s (checkpoint was
1.321 / 1.357), flat within noise; 153,346 triangles.
ALSO MEASURED THIS ROUND
- Edit cost: 8 affected chunks, 195.5 ms total -- generation 195.471 ms, apply 0.059,
diff 0.020, queue 0.001. It is full level-0 regeneration; a dirty-mesh path needs
partial section support that is unavailable without modifying RMC.
- Streaming caps: effective generation cap 14, pending max 14, CAP HIT. Mesh applies
per frame 4 (not hit), 2 ms apply budget (not hit), unloads 6 (not hit).
⇒ The generation core cap is what binds during startup fill.
- Peak export RSS 1.70 GB (was 19.5 GB before the memory work).
- LOD4 empty outliers gone. LOD0 not solved: 4,600 empty tiles at 3 us median, but 11
mixed-classifier fallbacks over 100 ms, max 554 ms -- verdict=Mixed, then the full
mesher, producing no triangles.
⚠️ UNVERIFIED AND CONCERNING: the reported in-game meshed times are far worse than
412a7ad's -- p50 498.8 ms vs 115 ms, p95 1.227 s vs 410 ms, max 1.974 s vs 844 ms.
The samples differ (9,336 ready events vs 841 requests, different traversal, possibly
queue wait included), and the export path is flat, so I do not know whether this is a
real regression or a different measurement. I have NOT reproduced it myself. It is
the first thing the next round must settle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
|
||
|
|
e91f1c44bb |
wip(tunnels): checkpoint -- floor parameters exposed, round cut short by Codex credit limit
⚠️ THIS IS A CHECKPOINT, NOT A FINISHED CHANGE. The agent hit its usage limit
mid-round (locked out until 2026-09-15) after compacting its context, and never
filed a report or capability numbers. Committing so 1,344 lines are not left
uncommitted for five days. Verified by me only to the extent stated below.
WHAT LANDED
The tunnel floor controls are now real strate parameters under
"Cave Morphology|Tunnels|Floor", which is what the owner asked for -- hand-made
assets can tune them and the composer can roll them later:
bTunnelFloorEnabled true
bTunnelFloorTerracingEnabled true
TunnelFloorTerraceStepHeight 1.71 voxels (the existing 42.75 cm riser)
TunnelFloorMaxLedgeHeight 12.0 voxels (3 m -- climbable, not walkable)
TunnelFloorGentleSlopeThreshold tan(44 deg)
TunnelFloorLedgeCountPreference 0 (0 = old per-segment staircase;
>0 = whole-chain ledge policy)
TunnelFloorMaxLedges 4096
⭐ So the staircase-vs-ledges question the owner raised is now answerable IN THE
EDITOR without a rebuild: set LedgeCountPreference to 2-3 and the whole-chain
policy takes over from the per-segment one.
VERIFIED BY ME
build 0 errors
export, 2 runs 1.321 / 1.357 s (
|
||
|
|
ce53552543 |
perf(relief): column cache for floor noise -- 8.5%, and my "it's 2D" premise was wrong
I predicted this would recover the D-section's 30% outright, on the grounds that the
floor relief is FMath::PerlinNoise2D and therefore a function of (X,Y) only, so every
voxel in a column recomputes the same pair of octaves. Codex checked the premise and
it does not hold:
production applies a 3D CAVE WARP before the floor is evaluated, so the warped X/Y
fed to the noise change with Z. A world-XY cache would have changed the field.
That is the third arithmetic prediction of mine that did not survive contact with the
code (after the double->float round-trips, and "the candidate scan is the last big
win"). Recording it because the pattern is consistent: I keep reasoning about cost
from a call site instead of from the data flow reaching it.
WHAT WORKS INSTEAD
Cache keyed on actual SDF-domain integer columns plus feature, owner, seed and
frequency -- correct in the warped domain, where world-XY is not. Fixed size, 32,768
entries, ~1.25 MiB per worker, no per-tile or per-region retention.
28,154,092 column lookups
25,842,374 hits / 2,311,718 misses (91.8% hit rate)
71,133,837 conservative relief skips (amplitude bound, only where SmoothMax is
provably the identity)
77,109,353 actual two-octave evaluations
my build, 3 runs: 1.384 / 1.405 / 1.415 s (
|
||
|
|
ff8bd7a8bc |
feat(tunnels): tunnels get the floor rooms always had -- capability up, 30% slower (fix known)
The owner asked the question that unlocked this: "isn't the floor cut a modification
made to rooms afterward, instead of having them be naturally like so?"
Reading the code answered it, and the answer was that rooms and tunnels were built on
OPPOSITE principles:
room = SmoothMax(sphereSDF, floorHalfSpace), floor part of the SHAPE, evaluated
in one pass, with two octaves of Perlin relief already on the floor height
(FloorReliefStrength / RoomFloorCutMin=0.70, hash-rolled per room)
tunnel = a bare capsule with NO floor, then CaveTunnelFloorOp + support columns +
IsTunnelSupportFloorWorldPoint asserting a slab in afterwards
That is why tunnels read as "forced flat" and rooms never did, and why the previous
conditional-floor round could only find 1,539 skips in 1.5M tests: it was optimising
a correction instead of removing the need for one.
WHAT LANDED
Corridors now use a swept SmoothMax(capsule, floor): endpoint floor heights derived
from the rooms they join, deterministically interpolated along the control chain, the
existing two-octave relief reused (not reimplemented) and faded at mouths, and
TERRACES where a room-to-room transition is too steep -- risers capped at 1.71 voxels
(42.75 cm), with relief disabled on risers so the step bound holds.
player-fit cells 20,153 -> 20,835 (+682)
reachable cells 10,483 -> 10,901 (+418)
connectivity connected, both; mouth gap 0
vertices/tris 40,623/75,776 -> 40,429/75,416
dihedral p50/p90 2.5429/33.8499 -> 2.5529/33.7191 deg
Capability went UP, which is the point: the shape now provides what the correction
was faking, and it reaches places the slab did not.
RETIRED: the 1.5M-call natural-floor validator and its counters, plus the now-dead
CaveTunnelFloorOp / CaveTunnelAirOp implementations and profiler labels.
NOT retired: the finite support-column backstop. Removing it failed hard --
19,097 fit, 396 reachable, DISCONNECTED. It stays, and Codex was right not to force it.
⚠️ THE HONEST COST: 30% SLOWER
my own build, 3 runs: 1.509 / 1.542 / 1.543 s (was 1.118 / 1.131 / 1.197 s)
Well above the 17% noise floor. The relief evaluation is what costs.
⭐ And the fix is already identified: the relief is `FMath::PerlinNoise2D` -- a
function of (X,Y) ONLY -- yet it is evaluated per 3D sample. That is exactly the bug
pattern the support floor had, where making the decision per-column took it from
2.29 to 0.138 us/sample (16x). Caching relief per XY column should recover most or
all of the 30%. Next round.
Renders confirm the floors now undulate gently instead of presenting a flat plane;
the global relief metric is wall-dominated and inconclusive, and Codex correctly
declined to claim the visual complaint solved on that basis.
Determinism holds: both final runs hash 4520DC6F with canonical repeat equality.
Analytically the authored floor profile is bounded by tan(44 deg) including the
Perlin derivative and mouth envelope; the full post-modifier SDF still is not
provable, so the player-fit audit stays.
Build 0 errors. Nothing outside Plugins/VoxelForge; RealtimeMeshComponent untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
|
||
|
|
383ec86c37 |
perf(apply): stutter is NOT the game thread -- and the flat floor cannot be relaxed
Two things the owner reported from playing |
||
|
|
412a7ad219 |
perf(game): the path he actually walks -- meshed p95 4.91s -> 0.41s, RSS 19.5GB -> 1.7GB
The owner: "I want real performance gain for the ingame path... not in deletion, but
in optimization and tricks." Three rounds of ours had improved the commandlet export
he never runs. This one is the streaming path.
MEMORY -- it was retention, not the new lattice
19.5 GB was worker FChunkSDFCache arrays keeping capacity when keys were replaced
(946 MB across 15 workers in diagnostics), shared graph caches with no byte or
lifetime budget, and batch commandlets holding transient UObject worlds until exit.
BlockSkip is bounded to 8^3 bytes per tile; interval scratch is worker-local and
capped. Fixes: release storage on key replacement, LRU with a 256 MiB resident
budget, limit in-flight graph builds, drop oversized entries, CollectGarbage between
batch cases.
peak RSS 19.5 GB -> 1.643 GB (their runs) / 1.71 GB (mine, 3x128^3) -91%
a 3-case stress batch showed no accumulation
WHY SOME EMPTY TILES COST 5.6s AND OTHERS 3us -- a 1.9 MILLION x spread
Fast ones returned straight from the layout/edge proof (verdict AllAir/AllSolid,
refine=0, mesh=0). Slow ones returned Mixed, so the caller ran the FULL mesher grid
and only discovered emptiness by brute force -- logs show empty=1 after ~5 s of mesh
work. The interval machinery now reaches a verdict before a mesh job is enqueued.
IN GAME, 841 requests at streaming radius:
count p50 p95 max
empty 394 6.087 ms 12.113 ms 25.049 ms
meshed 447 114.748 ms 410.314 ms 844.043 ms
LOD0 empty 221 7.126 ms 11.901 ms 25.049 ms
LOD0 meshed 122 355.794 ms 600.945 ms 844.043 ms
against
|
||
|
|
d38d4cabef |
perf(gen): interval propagation -- blocks can finally be PROVEN empty. 1.55s -> 1.21s
Task 4 ran 11 hours and was killed by system memory pressure before it could file a report. The work survived, builds clean (0 errors), and is verified faster with byte-identical output. This commit is my verification, not its summary -- it never got to write one. THE STRUCTURAL FIX |
||
|
|
0e9df9e5a7 |
perf(gen): the game path is NOTHING like the export path -- and empty tiles cost 5.6s
Overnight task 3. The headline is not a speedup: it is that we have been measuring
the wrong thing all along.
⭐ IN-GAME MEASUREMENT, FINALLY (headless -game -nullrhi, TunnelNetwork PIE map)
46,078 profiled tile calls
46,025 EMPTY (99.885%)
53 actually meshed
meshed p50 0.6085 s p95 4.9075 s max 5.0199 s mean 1.3515 s
LOD1-3 empty medians ~2-3 us
LOD4 empty OUTLIERS up to 5.618 s
The pawn fell through successive strata during the run, reproducing the owner's
"I can outrun the generation and fall" exactly.
Two facts to sit with:
1. **Almost nothing is meshed. 99.9% of tile requests produce nothing** -- and some
of those empty tiles take FIVE AND A HALF SECONDS. A tile that generates no
geometry costing seconds is the bug, and it is classification cost, not meshing.
2. The export path we have optimised for three rounds does 64 tiles in ~1.5 s
total. The game does 46,078 calls with multi-second tile latency. **They share
the density function and nothing else about their cost profile.**
⇒ Every optimisation so far was aimed at the path the owner does not play.
WHY NO BLOCK CAN BE SKIPPED -- answered, and it is structural
Per-op bound-vs-brute-force table produced (op_bounds.json). Most ops bound fine
once given proven suprema, and those were tightened:
FBM envelope 1.5, cellular 6.0, SdfConvertOp exact 2*|BaseDensity| = 16,
Lerp radius envelope max(Min,Max), plus state-aware room/column/floor-bias/worm/
terrace/cliff/arch/dome/pinch/roughness bounds. Structural ops now return exact
zero outside their geometric bands.
But OriginSpineOp, PassageCarveOp and the active seals **cannot** take a finite
generic density bound: they Lerp/Min/Max against the CURRENT density, so with
arbitrary incoming density their supremum is mathematically unbounded. This is not
a missing EffectOverBox, it is the operator semantics.
⇒ Hierarchical evaluation requires propagating a density INTERVAL through the
stack, or changing those operators. That is the real prerequisite, now named.
Early-out with tightened bounds still skips 0 of 4,096 blocks and costs 96.6%
(3.305 -> 6.497 s). Left opt-in and DISABLED. ClassifyTile's TerrainZ/overhang
guard untouched.
SPATIAL INDEX REMOVED (ee89c4d's, which never earned its keep)
wall clock 3.1407 s on -> 3.1894 s off = 1.5%, below the 17% floor
cache bytes 557,626,564 -> 246,049,920 = -297 MiB
candidates 19.5M -> 81.6M scanned (4.17x), evaluated +25.6%
Memory the owner can feel beats counters he cannot. Per-primitive broadphase stays.
VERIFIED END-TO-END on a correctly built binary: 1.515 / 1.554 / 1.569 s, geometry
hash 98eaad61..d3c4f unchanged from
|
||
|
|
2f33870a84 |
perf(mesher): adjacent tiles were re-evaluating 18% of the field -- 2.69s to 1.49s
Overnight task 2. Every claim here is end-to-end wall clock on a real 128^3 export,
not a profiled bucket, because the last two rounds shipped large bucket wins worth
0% in reality. Measurement floor is ~17% run-to-run; only Part 2 cleared it.
PART 2 -- KEPT. Density-grid reuse.
The mesher samples an expanded grid (-1..Cells+1), so adjacent tiles re-evaluated
their shared boundary planes. Measured: 2,744,000 requested samples contained
495,909 duplicates -- 18.07% of the field generated twice. A run-scoped read-only
lattice (FVoxelSharedDensityGrid) owns each world sample once and hands it to both
adjacent tile grids. Duplicates now 0.
128^3 TunnelNetwork seed 1337 slot 1, freshly built binary:
before 2.603 / 2.693 / 2.697 s
after 1.488 / 1.499 / 1.486 s -44.6%, 1.80x
Geometry SHA-256 98eaad61..d3c4f IDENTICAL to the pre-change binary's output.
153,740 triangles either way. Pure win, no field change.
The lattice is deliberately not a UObject and not a cache with eviction: ownership,
lifetime and iteration order stay explicit. A malformed or incomplete lattice is
ignored and falls back to the canonical generator path rather than altering geometry.
PART 1 -- REVERTED. Block-level empty-space early-out.
**0 of 4,096 blocks were safely classifiable**, and the attempt made it 85% slower
(3.632 -> 6.735 s). No unsafe bound was retained.
⇒ This is a finding, not just a failure: our conservative bounds are too loose to
prove ANY 8^3 block uniform. Hierarchical evaluation of any kind is blocked until
we know which operator has effectively unbounded reach. Next task.
PART 3 -- REVERTED. Task granularity.
15 workers, 64 tile jobs, mean job 0.594 s, launch fringe ~14.1 us. MinBatchSize=4
was 1.09% SLOWER end-to-end despite a better bucket time -- exactly the trap this
task's rule exists to catch. Default stays 1. ParallelForWithExistingTaskContext
not adopted.
PART 4 -- NO CHANGE. Renderer audited over 3 runs (8.736 s total, 1.483 s raster).
Mesh and BVH are already reused across viewpoints; no per-pixel heap allocation, no
redundant remeshing. Nothing gross to fix; it benefits from the above automatically.
STILL OPEN: the spatial index from
|
||
|
|
e63f016835 |
tools(profile): the instrument lied by 11.5x -- now it checks itself, and batching kills startup cost
Overnight task 1 of the owner's mandate: "better diagnostic, profiler and tools, so we can batch test way more quicker." WHY THIS CAME BEFORE ANY MORE OPTIMISATION |
||
|
|
ee89c4d8c2 |
perf(gen): 5x fewer candidates, 16x cheaper floor -- and 0% end-to-end. Read the last section.
Owner, after playing
|
||
|
|
0576e800c4 |
perf(gen): the landing work was per-voxel -- 342s to 2.6s, same world
The owner played |
||
|
|
64f5d61e08 |
fix(crash): EndPlay tore the world down under live workers -- the cache made it lethal
PIE crashed with EXCEPTION_ACCESS_VIOLATION reading 0xffffffffffffffff:
FRoomGraphSource::Eval() VoxelDensityOpStack.cpp:3056 (Manager->GetLayoutVersion())
UVoxelGenerator::GetDensityAt() VoxelGenerator.cpp:1103 (ActiveOpStack->EvalMC)
... AVoxelWorld::LoadTile lambda, on a task-graph worker
Two lifetime bugs, combined:
1. EndPlay gave up after a 3-second deadline and destroyed UObjects while workers
were still inside GenerateTileResult. The crash log records 4, 3 and 7 tasks
still running during teardown. ARCHITECTURE has always said EndPlay blocks on
ActiveTaskCount -> 0; it did not.
2. e08b49a's worker-local cache keeps 128 built op stacks alive per thread, and
those stacks hold a raw UVoxelStrateManager*. Before the cache, a stack was
rebuilt at every chunk crossing and never outlived its owner for long. After
it, a freed manager stayed reachable and got dereferenced.
The cache did not create the teardown race. It made an existing one lethal.
Fixes:
- EndPlay now waits unconditionally for ActiveTaskCount == 0. The 3s deadline is
demoted to a log line: a slow drain is reported, teardown is never torn out
from under a reader.
- UVoxelStrateManager carries a process-unique CacheLifetimeId, folded into the
cache key. Deliberately not a UObject pointer: allocator address reuse must not
let a stale worker-local stack look current across PIE sessions. PassagesVersion
restarts per layout and cannot make that distinction.
- Cached passage/room manager references are weak pointers, fail-closed.
- Verified FStrateGenerationParams is copied BY VALUE into tunnel operators, and
stack moves preserve owned operator allocations, so no operator references a
previous cache entry's parameters.
TunnelNetwork mesh 342.332 s -> 16.199 s (21.1x)
per tile, 64 tiles 5.349 s -> 0.253 s
NOT verified, and it matters here:
- No PIE measurement. The owner restricted this session to analysis and fixes, so
the numbers above are the commandlet's direct-mesher path, NOT the streaming
path the game uses. They are not game timings and are not presented as such.
- The 128-slot cache may churn at distance: an L0 tile crosses at most 27 exact
chunks, but L2/L3/L4 tiles touch 216 / 1,000 / 6,859 chunk keys. Whether that
helps or hurts in game is unmeasured.
- EndPlay waiting forever converts a crash into a hang if a task never observes
bShuttingDown. Workers do check it, but this trade is deliberate and worth
watching.
Named, not changed: the straight "roads" the owner still sees are inter-strate
passages. FStratePassageConfig defaults Connections=1 and Style=Straight
(VoxelStrateTypes.h:1781); GeneratePassages maps Straight -> SlopedTunnel and
builds a two-leg switchback that FPassageCarveOp carves
(VoxelStrateManager.cpp:819).
|
||
|
|
e08b49a183 |
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 |
||
|
|
d97373c5a8 |
feat(gen): Jahni's playtest corrections -- rooms flattened, roads deleted
From walking a TunnelNetwork preset at (0,0). Four corrections, and two of
them were the same mistake.
2a. The (0,0) landing room is sized to host a FUTURE straight vertical
shaft from the surface, with flooring around the origin itself. The
room is the landing for a shaft the player digs later.
2b. The inter-strate descent no longer starts at the spine -- placed by
deterministic hash within a radius around (0,0). The spine room is for
the surface shaft; the descent is elsewhere.
2c. ⛔ DELETED the landing->hub connector roads from
|
||
|
|
55bf807f60 |
feat(descent): landing ROOMS and a walkable tunnel -- per Jahni's correction
Jahni corrected the design: "the connection between level is a tiny tunnel
connecting both strate, not vertical, it needs to be walkable" and "'the
spine' shouldn't be carved except optionally from the top first strate,
then it's used to have a landing room".
COMPOSER-NOTES §6.8 had said the spine "provides a clean landing space, not
a continuous open shaft" -- and the implementation carved a vertical
cylinder anyway. A vertical cylinder has no walkable floor except at its
bottom, so a carve guaranteeing open AIR guaranteed nothing STANDABLE.
That one omission is behind five of my dead hypotheses this week.
Now:
origin spine a finite rounded ROOM per strate, no vertical bore.
radius 14 -> 8 m wide, 7.5 m tall
descent a walkable tunnel, single-file by design: 3 m mouth,
2.5 m waist, gradient capped at a NAMED 15 deg
exotic styles VerticalShaft / SpiralDescent / CascadingDrops /
CrackCrevice preserved, no longer the default
⚠️ THE GEOMETRY BITES, and it is worth Jahni's eye: at 15 deg a 32 m drop
needs 119.4 m of horizontal run -- about 59.7 m per switchback leg. That is
a long walk between strates. It is the honest consequence of "walkable",
not a tuning artifact, and the alternative is a steeper number.
⭐ AND THE ROOT CAUSE GOT NAMED A THIRD TIME, by Codex mid-run: "the round
tube was hollow air but had no authored support strip, so 'open' still did
not mean 'standable'." Tunnels now carry a floor band, and the root join
terminates in the finite origin room rather than an annular shaft hub.
Measured, commandlet sweep, 8 archetypes x 8 seeds, 64/64 valid:
origin column has player-fit cells 17/64 -> 63/64
origin column reachable 0/64 -> 7/64
arrival landing probes 315/320 fit AND in the arrival component
tunnel audit 5,916/5,916 floor and capsule samples pass; zero gradient,
floor, capsule, geometry or seal failures
narrow-gap events 0 box verdicts 14 checked, 0 violations
⚠️ arrival->departure is 6/64. The landings and tunnels are now provably
walkable; what remains is the INTRA-STRATE vertical connectivity problem
found in
|
||
|
|
6b916699af |
test(explore): 81 runs in an afternoon -- every failure is a DROP, not a squeeze
First real use of the commandlet, and it paid for itself immediately: 64
sweep runs + failure renders + origin probes, which at the old 73-minutes-
per-automation-run would have been days.
⭐ FINDING 1: THE WORLD IS FRAGMENTED VERTICALLY, NOT PINCHED.
Across all 64 runs: ZERO narrow-gap events. Every failed run has separate
arrival/departure fit components with a positive gap, 12.75 m to 72.98 m.
So corridors are not too tight anywhere -- the space is severed.
And for the first time in this project we LOOKED at a failure instead of
inferring from aggregates:
FlatPlain/42 "a broad flat floor sheet with a sharp diagonal ledge
descending into black open space" -- a drop, gap 63.9 m
SurfaceWorld/7 "terrain shelves and a large black void between them.
There is no connecting floor" -- gap 51.8 m
Maze/42 "a massive solid rock wall/overhang fills the separation"
-- gap 27.7 m
I checked the images myself. FlatPlain shows two floor LEVELS 27 m apart
in Z with nothing joining them.
⇒ The missing thing is VERTICAL CONNECTION between floor levels. Ramps,
slopes, ledges, stairs. Every archetype builds floors; nothing builds a way
between them.
⭐ FINDING 2: THE ORIGIN HUB IS NOT REACHABLE, which contradicts
|
||
|
|
bd547c35c4 |
feat(explore): a commandlet -- 91x faster, and the first real PICTURE
Every experiment in this project cost a build + full-suite cycle. The
landing task took 52 of them over ~9 hours. And nobody could SEE the world:
every visual judgement all session was made on density SLICES -- diagrams,
not pictures. That is how a floor heaving 1.4 m every two metres got
described as "roughness visible but modest" while Jahni spotted it in
thirty seconds by walking it.
-run=VoxelForgeExplore, editor-only module, three modes, one build many runs:
render raymarched PNGs, fixed 0.25-voxel step + 10 bisection iterations
(NOT sphere tracing -- the op stack yields density, not a signed
distance, so a long step tunnels through walls). Three views, each
with a 1.76 m human marker in frame, because every scale mistake
here came from numbers with no body beside them.
walk an agent using the EXISTING VF player-fit stencil, never its own
movement model -- two definitions of "can a player stand here"
would drift and we could not tell which was lying.
export OBJ through the canonical UVoxelMarchingCubesMesher. No second
mesher; a forked meshing invariant is the bug this project keeps
paying for. 23.2 MB, 106,762 verts, 200,842 tris for 128^3.
⭐ THE NUMBER THAT JUSTIFIES IT: the full automation run takes 4,396 s
(73m16s). The commandlet does all three modes in 48.1 s wall, 5.9 s of
which is engine startup. ~91x faster. And I had guessed the suite cost
~90 s -- it is 73 MINUTES. The iteration loop was far more expensive than
I knew.
First real walk metrics on the Maze (seed 0, slot 4):
distance 214 m · tortuosity 4.98 · dead ends 0.935/100 m
narrow-gap events 0 · reachable fit fraction 3.01%
result NotConnectedAtThisResolution
Tortuosity ~5 is a genuinely winding route, which is right for a maze.
3% reachable is not.
Editor Development build clean. SHIPPING build clean with the receipt
containing VoxelForge and NOT VoxelForgeEditor -- the instrument cannot
reach a packaged game. Automation 32/32, 0 failed.
⚠️ Codex initially shipped this UNBUILT, and correctly: I omitted the
## Validation section from that spec, so it fell back to CLAUDE.md's
"NEVER build or compile". 100+ compile errors from three causes -- bare
TCondensedJsonPrintPolicy and TJsonWriter needing their template
arguments, and a missing UObject/Package.h. That include is the SECOND
miss this session, so CODEMAP §6 now carries the gotcha.
⚠️ Unrelated, found in passing: VoxelForge.uplugin does not declare
RealtimeMeshComponent although the runtime module uses it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
|
||
|
|
413c15de24 |
feat(passages): §6.5 landings -- you arrive somewhere you can stand
The last unbuilt Tier 1 item. A passage was a bore through rock with no floor inside the walkable angle, so its mouth was not a standing position and neither was anything near it -- the departure mouth measured a FOUR-CELL player-fit component. You arrived and there was nowhere to stand. Both ends of all 7 passages now open into a landing, sized from the body: capsule diameter 2.72 vox floor >= 12 vox (3 m) to stand and turn capsule height 7.04 vox room >= 12 vox tall, stock mouth gives 18 (4.5 m) 3-voxel floor slab, tube tangent to it -- no lip to climb out of connector 5-vox radius: 2.5 m clear width, 3 m clear height ramps over 44 deg get a deterministic level dog-leg ⭐ THE MOUTHS ARE NO LONGER SPECKS: archetype components largest arrival-departure Maze 309 -> 340 176 -> 3210 43-81 -> 3210-3210 TunnelNetwork 467 -> 227 1292 -> 3841 2-1 -> 3841-269 Underwater 86 -> 117 35 -> 3838 3-11 -> 3838-267 FlatPlain 45 -> 11 259 -> 2130 1-1 -> 268-273 CrystalChamber 197 -> 90 24 -> 286 1-1 -> 286-274 FloatingIslands 2 -> 1 3 -> 72 1-1 -> 72-72 Law 1/8 -> 2/7 connected (1 refused on MaxCells), and critically ZERO connections now run through a four-cell pocket. Every landing floor was verified against the FINAL density, after all posts: 14/14 pass the 44 deg slope bracket, worst room gradient .059, worst connector .522. 14/14 deterministic root joins. 0 seal failures. 0 box-verdict violations across 82 proved + 174 mixed, 109,142 voxels. Op-stack bit-identical, order-independence green over 10 slots / 31 passages / 3 pool orders. Cache 0.665 us rebuild, 0.646 us hot. Post order: origin spine -> vertical seal -> passage tube + landing floor -> XY edge seal. Landings clamp 4 voxels inside the vertical seal; the XY seal stays last and wins at world edges. ⚠️ ARCHITECTURAL NOTE, not a detail: landings join a deterministic annular ROOT HUB at (0,0), built identically for every endpoint -- no nearest- component search, no passage ordering. That is what makes the join guaranteed rather than likely, and it extends the (0,0) spine concept from a landmark into a structural network. It also means routes tend to pass through the centre, which is a gameplay consequence worth Jahni's eye. ⚠️ AND IT DOES NOT CLOSE THE LAW. Codex is explicit: the landings guarantee membership in the new root network, NOT in the archetype's pre-existing fit component. The whole-strate Maze check still shows a 71.9-voxel gap between 1,235 and 2,938-cell components. So the native corridor networks remain disconnected from the root network in places. Real, separate, next. 52 build-and-measure cycles, ~9 hours, finding real geometry bugs on the way (a landing floor overlapping the ramp cells and hiding a floor discontinuity; the disturbance pass refilling connector headroom). Build clean, 32/32 success, 0 failed. Showcase regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV |
||
|
|
e4e159f0cd |
test(measure): the window was NOT the problem -- my sixth hypothesis dies
I thought the fitted mouth-AABB window was cropping an origin-routed path and producing a false NotConnected. Codex tested it three ways at step 1 on the Maze candidate Jahni walked: mouth-AABB + margin 842,996 cells NotConnected origin-inclusive 3,251,556 cells NotConnected WHOLE STRATE 31,881,528 cells NotConnected 115,578 fit cells, 2,903 components, largest 11,418 -- and the mouths sit in components of 172 and FOUR cells, 274 voxels apart. The window was never the problem. Sixth measurement hypothesis, first one that was wrong in the other direction: this time the instrument was fine and I doubted it. The window rule still improved: bIncludeOriginInCoverWindow now covers the route an origin-rooted topology actually implies (Maze, VerticalShafts, TunnelNetwork, Underwater), expansion happens BEFORE MaxCells, and an over-cap window is REFUSED honestly instead of silently cropping back to a mouth-only box. VerticalShafts refused at the 4M production cap and said so. Corrected law across eight: 0/7 measured Connected, 1 unknown (CoarseLiedBudgetExhausted), 1 refused. ⇒ FIVE CAUSES NOW ELIMINATED: size (Jahni walked it), the fit stencil (fixed, 309 pockets -> 96), placement (mouths land on legal poses), corridor connectivity (spanning tree, 0 disconnected cells over 64 seeds), and the measurement window (whole-strate says the same thing). ⭐ AND THE REMAINING CAUSE IS NAMED BY ITS OWN NUMBER: the departure mouth sits in a FOUR-CELL player-fit component. A passage tube is a bore through rock -- sloped or vertical, no floor within the walkable angle -- so the mouth point is not a standing position and nothing around it is either. You arrive and there is nowhere to stand. That is precisely COMPOSER-NOTES §6.5 / Tier 1's unbuilt item: "the passage mouth becomes a room plus a distinctive landmark". It was never started. The law cannot pass while a mouth opens into a bore instead of a landing. Build clean, 32/32 successful, 0 failed, same two known warnings. WorldRadiusVoxels=0, lateral gating, determinism, equivalence all intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV |
||
|
|
2eaceb5984 |
feat(maze): a spanning tree -- it is a maze now, not a grid
Jahni: "mostly a cube full of straight pathway along the edges... basically
just a repeating pattern of tunnel connecting to each other, definitely
unwanted". And the measurement: the arrival mouth in a 30-cell pocket while
the network held 1,217. SAME CAUSE -- edge selection was independent hash
gating per +X/+Y/+Z edge, i.e. a random subgraph of the cubic lattice.
Every junction identical, and connectivity left to chance.
Fixed the way VerticalShafts was fixed in
|
||
|
|
4378385b37 |
fix(passages): land mouths where a BODY fits -- and the cause is now cornered
VF_SuggestLandingPoint aimed at open AIR. Air is not where a body fits: a mouth could sit in a perfectly open pocket no capsule can stand in. It now requires a legal capsule pose -- clearance (r 1.36, half-height 3.52), support within a 1.8-voxel / 45 cm step height, a walkable support PATCH (4 of 5 columns, centre required, slope <= 44.8 deg), positive density throughout the capsule, strict 2x seal interior, bounded lateral snap. It stays a PURE free function, which was the hard constraint -- GeneratePassages runs inside Initialize while the layout is half-built. So it cannot flood-fill, and Codex correctly did not pretend otherwise: it aims at deterministic guaranteed features instead (room centre/floor, slab floor, horizontal maze edge, shaft feature-core ledge pose, island top). Both mouths query their own strate independently; no passage chaining, so LayoutOrderIndependence stays green. Placement measurably improved: Maze comps 309 -> 82, largest 176 -> 1217, gap 206 -> 122 VerticalShafts comps 112 -> 36, largest 96 -> 619 CrystalChamber gap 47.5 -> 19.1, departure mouth now in a 717-cell comp Underwater comps 86 -> 13 ⚠️ AND THE LAW IS STILL 1/8. Reported honestly rather than tuned. Mouths now land on legal body-sized poses; the poses just are not connected to each other. ⇒ BY ELIMINATION, THE CAUSE IS GENERATION. Four candidates, three dead: size DEAD -- Jahni walked it; bore measures 3.62 m vs 3.677 m spec the stencil FIXED -- fragmentation was 3x overstated by an exact-Z-1 floor placement FIXED -- mouths now sit on legal poses, several in large comps GENERATION STANDS -- body-sized space exists in big components that do not reach each other ⭐ AND THE MAZE TIES TWO OF JAHNI'S COMPLAINTS TOGETHER. Its arrival mouth lands in a 30-cell pocket while the main network holds 1217. Edge selection is INDEPENDENT HASH GATING per +X/+Y/+Z edge (VoxelDensityOpStack.cpp:582/584/586) -- so the corridor graph is a random subgraph with no connectivity guarantee, exactly the percolation failure VerticalShafts had before its drainage tree. That is the same root cause as "mostly a cube full of straight pathway along the edges": a spanning tree would make it BOTH feel like a maze (dead ends, uneven runs) AND be connected by construction. One fix, two complaints. Codex found a real error in my spec: an infinite shaft cylinder has no support surface AT its axis, so the old exact-axis assertion was unsatisfiable. It now requires a deterministic feature core and audits tree connectivity from the resolved axis. Build clean, 29 succeeded / 0 failed / 2 known deliberate warnings. Box audits 0 violations. Shaft refinement 16/16. Order-independence green across 10 slots, 31 passages, 3 pool orders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV |
||
|
|
110ae9d0ed |
fix(measure): the fragmentation was the instrument -- 309 pockets -> 96
Jahni walked the maze end to end while the metric called it 309 disconnected pockets whose largest held 176 cells, with the arrival mouth in a THREE-CELL pocket. Direct observation beat the instrument, again. Cause, found exactly: the stencil demanded a solid cell at precisely (X,Y,Z-1). No supporting-surface search, no slope tolerance, no step height. At step 1 a floor that slopes even slightly loses its solid cell under most of its air, so a walkable corridor dissolved into specks. Now: a bounded downward search for a COHERENT SUPPORTING SURFACE within a step-height tolerance (MaxStepHeight 45 cm = 1.8 voxels), a walkable-slope normal test (WalkableFloorAngle 44 deg), then the full capsule re-tested at the resolved support height. Deliberately NOT "any solid within N cells", which would accept ledges and isolated pixels. ⭐ ON THE WALKED MAZE (seed 0, index 50, slot 4): components 309 -> 96 largest 176 -> 1401 fit cells 2801 -> 6668 mouth comps 43/81 -> 147/1401 The 300-pocket world was a measurement artifact. Fifth time this session the instrument was the suspect; fourth time it was the culprit. ⚠️ AND IT IS NOT SELF-FLATTERING -- a metric change that only ever makes numbers rise is worthless, so it ships with controls that must REJECT: vertical shaft wall 0 fit cells isolated 1-voxel ledge 0 fit cells overhang underside 0 fit cells ramp steeper than 44 deg 0 fit cells coarse step 4 refused, not guessed Open flat-floor control: 6.13% of air cells fit, ONE component of 12,168 -- sensible, since only near-floor poses qualify in a tall room. ⚠️ BUT THE ROUTE STILL DOES NOT EXIST, and that part of H3 survives. The law is 1/8 before and after -- what changed is its honesty: the one connection used to be FloatingIslands through FOUR cells, and is now FlatPlain through 2,221 with a zero gap. Seven archetypes still have both mouths in separate player-fit components 56-169 voxels (14-42 m) apart, and those components are now large and real (663-2902 cells), not specks. ⇒ So the world HAS substantial connected player-sized space. The passage MOUTHS are not landing in it. That is the Tier 1 landing problem again, one level down: VF_SuggestLandingPoint aims at open AIR, and air is not where a body fits. Next task. Codex found three errors in my baseline quoting, all mine: I wrote "0/8 connected" while also saying one connects through four cells (it was 1/8), misquoted SurfaceWorld's gap as 1101 when the saved value is 110.648, and mixed provenance on the VerticalShafts row. Build clean, 31 succeeded / 0 failed. The 2 warnings are both deliberate: the lateral-regions gate, and a test reporting that its own sample "barely asked its question". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV |
||
|
|
cf5f83e9f0 |
test(diag): H1 dead, H3 measured -- player-sized space is SHATTERED
Diagnosis only, no generation or parameter changes. ⭐ H1 IS DEAD, confirmed by Jahni's playtest and by measurement. He walked the maze candidate: "i CAN go through it, the tunnels are just the right size with very tiny wiggle room." The radial probe agrees: measured bore span 3.62 m against a 3.677 m parameter. The carve produces what it claims; the scale change worked. ⭐ H3 IS WHAT THE NUMBERS SHOW. Player-sized volume EXISTS but is broken into hundreds of disconnected pockets, with the two mouths in different ones, tens to hundreds of metres apart (step 1, fitted window): archetype fit cells components largest arrival/departure gap Maze 2801 309 176 43 / 81 206 vox (51 m) TunnelNetwork 2748 467 1292 2 / 1 164 vox (41 m) VerticalShafts 1156 112 96 3 / 36 94 vox (24 m) SurfaceWorld 713 520 6 1 / 1 1101 vox (275 m) CrystalChamber 655 197 24 1 / 1 48 vox Only FloatingIslands connects, through FOUR cells. ⚠️ BUT I DO NOT ACCEPT "H3, therefore the world is shattered" YET, and the reason is Jahni. He WALKED a maze end to end that this metric says is 309 disconnected pockets whose largest holds 176 cells. Direct observation contradicts the instrument. An arrival mouth sitting in a THREE-CELL pocket is not a description of a cave; it is the signature of a stencil that fragments a continuous floor. H2 is the likely cause of that fragmentation, and Codex found it exactly: the fit test demands a solid cell at EXACTLY (X,Y,Z-1) -- no supporting- surface search, no slope tolerance, no step height. HeadroomCells applies only to the legacy walkable metric, not the capsule mask. VoxelStrateMeasure.cpp:697 (stencil), :729 (the exact-floor test) At step 1, any floor that slopes or is rough loses its solid cell at Z-1 under most of its air, so a walkable corridor dissolves into specks. ⇒ SEQUENCING: fix the stencil first, THEN re-measure H3. Until the instrument stops shredding continuous floors its component counts cannot be trusted. Fifth time this session that the measurement was the suspect. Recommended stencil, Codex's design and I agree: a bounded downward search for a COHERENT SUPPORTING SURFACE within a step-height tolerance, then re-test capsule clearance at that support height -- explicitly NOT "any solid within N cells", which would accept ledges and isolated support pixels. Fixed the mixed-resolution reporting that nearly cost me a false finding: fields are now coarse_step4_* and *_step1, never both unlabelled on one line. Recorded from the playtest, not acted on: - The maze is a GRID, not a maze. Edge selection is independent hash gating per +X/+Y/+Z edge (VoxelDensityOpStack.cpp:582/584/586), so every junction looks alike. A spanning subset would give dead ends and uneven runs. - TunnelNetwork DOES have explicit room-to-room connections (VoxelCaveMorphology.cpp:1406 backbone, :1465 tunnels) -- so Jahni's "lack of real passageway" applies to FlatPlain/CrystalChamber, which are slab voids with no room graph at all. - Two-abreast wants 2.5-3.0 m usable floor; a 4 m circular bore gives ~2 m. Build clean, 31 succeeded / 0 failed, run by me after Jahni closed the editor (the previous attempt could not link -- the DLL was held by his running editor). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV |
||
|
|
fc43a86709 |
feat(scale): size the world to a human body -- and it is NOT enough
Jahni: "can't you try and make the world to scale?" Every length parameter is now derived from the body and annotated with its metre value, so nobody has to do the arithmetic to notice something is absurd. CorridorRadius 4 -> 8 1 m -> 2 m radius (4 m bore) CellSize 40 -> 64 keeps a real wall between wider corridors TunnelMinRadius 3 -> 6 0.75 m -> 1.5 m ShaftMinRadius 5 -> 8 1.25 m -> 2 m IslandMinRadius 18 -> 24 4.5 m -> 6 m MouthRadius 6 -> 8 passage mouths 4 m plus passage reach, spiral, cascade, bridges, ridges, chasms The reasoning that motivated it still looks right: a 2 m circular bore for a 1.76 m player clears the head only along the exact centreline, so almost no cell has a floor with 7.04 voxels of clear air above it. A 4 m bore is genuinely walkable. ⚠️ BUT IT DID NOT FIX TRAVERSABILITY, and this time it is not the instrument. The player-fit law is measured at STEP 1 on a mouth-fitted window -- full resolution, both mouths inside the box -- and across the eight showcase archetypes it reports: 6 NotConnectedAtThisResolution (real negatives at full resolution) 2 OutOfWindow (unmeasurable, not passing) 1 StartCellNotPlayerFit (the arrival mouth itself is too small) 0 CONNECTED And every archetype reports hard-gate survivors=0, so the showcase is displaying the least-bad of an entirely failing set. ⛔ DO NOT READ player_fit_fraction / player_clearance FROM THE SHOWCASE LINE. Those come from the COARSE step-4 measurement in the same log line, where the 1.36-voxel player is sub-cell and fit is unmeasurable -- player_clearance reads a uniform "1.00 voxels" for all eight archetypes, which is one cell, not one voxel. Only restricted_law is at step 1 and only it is trustworthy. Fixing that mixed-resolution reporting is next. So scale was necessary and insufficient. My hypothesis was that bore diameter versus body height was the binding constraint; doubling it moved nothing measurable. Something else is preventing player-sized volume from existing at all -- worth finding before tuning any further. Codex applied these edits and then died mid-poll without validating them (third time a killed agent left a clean-looking unbuilt diff). Build and suite run by me: build clean, 30 succeeded / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV |