The city's hole is not at world (0,0), so the AVoxelWorld actor has to move.
It couldn't: the plugin was only HALF actor-relative, and had been for a long
time without symptoms because the actor always sat at the origin.
Already correct: GetBiomeAtWorldLocation, GetVoxelSurfaceHeightAt,
UpdateDecorations, UpdateLandmarks, UpdateWater, UVoxelDensityVolume::Update,
the passage debug draw, the clipmap material.
Not correct: the streaming centre, the streaming anchors, all six carve/fill
entry points, GetStrateAtPosition, and UVoxelAtmosphereManager::UpdateForPlayer
-- whose own comment documented the assumption out loud ("actor at
origin/identity"). Carving would have landed offset; the strate lookup would
have been quietly wrong by the actor's Z.
The trap worth recording: because the content managers already convert
internally, converting PlayerLastPos once at the source would have DOUBLE-
converted four call sites. The fix has to be per-boundary. That was only
visible after reading each consumer -- the estimate before that read was wrong
in both directions.
WorldToLocalCm / WorldToLocalVoxel / LocalVoxelToWorld are now declared as the
only sanctioned boundary between Unreal coordinates and voxel coordinates, and
the real deliverable is the invariant next to them: no `/ VOXEL_SIZE` applied
to a parameter named `World*` outside those three functions. A grep can check
that. The risk was never a missed site among the ones found -- it is the next
one nobody notices.
Also adds a translation-only guard in BeginPlay. Tiles, ClassifyBox verdicts,
the clipmap window, culling and streaming distance all assume world-axis-
aligned boxes; a rotation or non-unit scale breaks that structurally rather
than gracefully, so it logs an Error naming what was set.
With the actor at the origin every conversion is identity, so this is a no-op
until the actor actually moves. Built and confirmed working.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
Initialize does StrateLayout.Empty() and Passages.Empty()/Add() -- freeing and
reallocating both -- with no guard, while mesher workers read them through
AnyPassageNearBox, EvaluateModifierSDF and FindSlotIndexForChunkZ. The epoch is
bumped AFTER, so previous-epoch workers are live during the mutation; the epoch
rejects a finished result, it cannot make a read of a freed allocation safe.
Same class as the DiffLayer.ChunkMods carve-vs-stream AV that ModsLock fixed.
Chose the drain over the two alternatives:
- FRWLock on the arrays: correct, exact in-repo precedent, but a read lock on
the per-voxel hot path (~43k/tile) would contaminate the perf measurement
CODEX-TASK-001/002 are queued to take. Worst possible timing.
- Immutable generation snapshot (Sol's proposal): right long-term, refactors
the whole StrateManager API surface. A design conversation, not an agent's
unprompted call.
- Drain: zero hot-path cost, reuses the machinery EndPlay already proved, and
its stall lands only on human-initiated editor actions, never during play.
FScopedGenerationPause raises a new bGenerationPaused (deliberately NOT
bShuttingDown, which means teardown) and drains both reader populations: chunk
tasks via ActiveTaskCount and decoration tasks via a new
WaitForDecorationTasks, whose refactor also removed NotifyShutdown's duplicate
spin loop. On timeout it does NOT mutate -- logs an Error and leaves the world
consistent. A timeout that proceeds is what the bug already does.
RebuildStrates, OnObjectModifiedInEditor and ChangeSeed cannot reach Initialize
without the pause. BeginPlay untouched (no tasks yet). EndPlay untouched --
VF-02's 3s timeout is a separate deliberate decision.
Verified: four files, no density/mesher file, so terrain cannot move.
ShouldAbortWork replaced three existing checks, adding none. No worker path
waits on the game thread, so the drain is bounded.
Not built -- Jahni builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plugin had ZERO stat counters, so every claim this refactor makes was
harness-only: "skipped correctly" and "skipped nothing" render identically.
CODEX-TASK-001 and -002, executed by Codex (gpt-5.6-luna xhigh), reviewed
against the source.
Eight DWORD counters in a new stat group:
GenerateTileResult -- TilesClassified / SkippedAllSolid / SkippedAllAir / Meshed
ClassifyTile bAnyCave exit -- TilesOpStackSolid / TilesOpStackAir
FSurfaceColumnSource::GetColumn -- ColumnMemoHit / ColumnMemoMiss
The op-stack counters are separate from the lumped skip counters on purpose:
ClassifyTile also proves AllSolid on its hand-written bedrock-gap path with no
strate opted in, so the lumped number cannot show a before/after. The op-stack
pair is zero BY CONSTRUCTION until a strate ticks the flag.
GetColumn is instrumented and deliberately NOT fixed -- the instrument and the
fix in one build would make each other unreadable.
Verified against UE_5.7 Stats.h rather than assumed (first stats use in this
plugin, no in-repo precedent): all four macro arities, and INC_DWORD_STAT ->
FThreadStats::AddMessage, so the worker-thread safety both sites need holds by
mechanism. Not built -- Jahni builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VoxelWorld.cpp:526 dereferences the pawn, so APawn must be complete; Casts.h only
forward-declares it. Adds GameFramework/Pawn.h, and PlayerController.h which was
complete transitively only — the same fragility this change removes.
My earlier scan covered Public/ only. The shared PCH served .cpp files too, and
APawn was named in Build.cs's own error list. Everything else in the module
compiled, so this is the entire tail.
UNVERIFIED: not compiled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>