fix(streaming): reload terrain on an outdoor teleport (sky-arcs)
Terrain vertices are baked into the GPU relative to the render origin (_liveCenter),
which only moves on a teleport. A FAR jump unloads/reloads everything → fine. But a
NEARBY outdoor jump (e.g. (170,168)->(169,180), 12 landblocks) leaves the old and new
streaming windows overlapping, so StreamingRegion.RecenterTo KEEPS the ~330 overlapping
blocks (correct — they're in the new window) — yet they still hold vertices baked at the
OLD origin. The instant _liveCenter moves they render shifted by the jump distance: a
band of terrain hanging in the sky ("terrain in the sky" arcs). Confirmed by probe: every
stale slot was offset by EXACTLY deltaLB*192 ((-1,12)*192 = (-192,2304)), 330 of them
persisting after the hop.
Fix: StreamingController.ForceReloadWindow() — on an OUTDOOR teleport, SYNCHRONOUSLY drop
every resident landblock (render slot + physics + state) so none survives the frame stale,
then null the region so NormalTick re-bootstraps the whole window fresh at the new origin
(the near ring is priority-applied behind the fade; the rest streams). Called from
OnLivePositionUpdated's outdoor recenter branch; sealed dungeons keep PreCollapseToDungeon.
Verified live: the exact nearby hop that produced 330 stale slots now produces 0 across
the whole session, and the horizon is clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
15e320490d
commit
9945d46280
2 changed files with 31 additions and 0 deletions
|
|
@ -5470,6 +5470,12 @@ public sealed class GameWindow : IDisposable
|
||||||
// player position can't drift the observer off the dungeon and re-expand.
|
// player position can't drift the observer off the dungeon and re-expand.
|
||||||
if (_streamingController is not null && IsSealedDungeonCell(p.LandblockId))
|
if (_streamingController is not null && IsSealedDungeonCell(p.LandblockId))
|
||||||
_streamingController.PreCollapseToDungeon(lbX, lbY);
|
_streamingController.PreCollapseToDungeon(lbX, lbY);
|
||||||
|
else
|
||||||
|
// Outdoor teleport: the render origin (_liveCenter) just moved. Drop ALL
|
||||||
|
// resident terrain so overlapping blocks on a NEARBY jump re-bake at the
|
||||||
|
// new origin instead of rendering shifted — the confirmed "terrain in the
|
||||||
|
// sky" arcs (stale slots offset by exactly deltaLB×192). 2026-06-22.
|
||||||
|
_streamingController?.ForceReloadWindow();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -344,6 +344,31 @@ public sealed class StreamingController
|
||||||
_region = rebuilt;
|
_region = rebuilt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 2026-06-22: an OUTDOOR teleport moved the render origin (<c>_liveCenter</c>). Every
|
||||||
|
/// resident terrain block was baked relative to the OLD origin, so any block that survives
|
||||||
|
/// an INCREMENTAL recenter — which happens on a NEARBY jump where the old and new windows
|
||||||
|
/// overlap, e.g. (170,168)→(169,180) — renders shifted by the jump distance: the confirmed
|
||||||
|
/// "terrain in the sky" arcs (the stale slots were all offset by exactly deltaLB×192).
|
||||||
|
/// SYNCHRONOUSLY drop every resident landblock (render slot + physics + state) so none
|
||||||
|
/// survives into the next frame stale, and no async unload can race a re-bake, then null
|
||||||
|
/// the region so the next <see cref="NormalTick"/> re-bootstraps the WHOLE window fresh at
|
||||||
|
/// the new origin (the near ring is priority-applied behind the fade; the rest streams in).
|
||||||
|
/// A sealed-dungeon destination uses <see cref="PreCollapseToDungeon"/> instead.
|
||||||
|
/// </summary>
|
||||||
|
public void ForceReloadWindow()
|
||||||
|
{
|
||||||
|
_collapsed = false;
|
||||||
|
// Snapshot — RemoveLandblock mutates the loaded set we're iterating.
|
||||||
|
var ids = new List<uint>(_state.LoadedLandblockIds);
|
||||||
|
foreach (var id in ids)
|
||||||
|
{
|
||||||
|
_state.RemoveLandblock(id);
|
||||||
|
_removeTerrain?.Invoke(id); // frees the render slot + physics + cell registries
|
||||||
|
}
|
||||||
|
_region = null; // NormalTick re-creates + bootstraps the full window next frame
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply streamed completions for this frame. LOADS (terrain mesh GPU uploads) are the
|
/// Apply streamed completions for this frame. LOADS (terrain mesh GPU uploads) are the
|
||||||
/// expensive part, so they are metered at <see cref="MaxCompletionsPerFrame"/> to avoid a
|
/// expensive part, so they are metered at <see cref="MaxCompletionsPerFrame"/> to avoid a
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue