fix(G.3): hysteresis on the dungeon streaming gate — stop collapse↔expand thrash (#133)
The first cut of the dungeon gate keyed expand on the per-frame insideDungeon signal (CurrCell is a sealed EnvCell). Live, CurrCell momentarily resolves to null mid-frame while the player stays put in the dungeon landblock, so the gate flipped collapse→expand→collapse every few frames. Each expand re-streamed the full 25×25 window; the unloads couldn't keep up (MaxCompletionsPerFrame=4), so registered lights leaked to 212k and FPS spiked to single digits between the ~199 fps collapsed frames. Fix: once collapsed, key the gate on the STABLE observer landblock, not CurrCell. Stay collapsed while the player remains in the dungeon landblock (_collapsedCenter); expand only when the observer actually moves to a different landblock (portal/ teleport out). CurrCell flicker no longer thrashes. Regression test added (Collapsed_CurrCellFlickersToNull_SameLandblock_DoesNotExpand). Build green; 60 streaming tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56860501b6
commit
d9e7dd65e9
2 changed files with 39 additions and 7 deletions
|
|
@ -32,6 +32,14 @@ public sealed class StreamingController
|
||||||
// are never visible, so we stop loading the 25×25 window entirely.
|
// are never visible, so we stop loading the 25×25 window entirely.
|
||||||
private bool _collapsed;
|
private bool _collapsed;
|
||||||
|
|
||||||
|
// The dungeon landblock id we collapsed onto. Once collapsed we key the
|
||||||
|
// gate on this STABLE landblock, not the per-frame insideDungeon signal:
|
||||||
|
// CurrCell can momentarily resolve to null/outdoor mid-frame, and gating
|
||||||
|
// expand on that flicker thrashes collapse↔expand (reload storms + a light
|
||||||
|
// leak). We only expand when the observer actually moves to a different
|
||||||
|
// landblock (teleport/portal out).
|
||||||
|
private uint _collapsedCenter;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Near-tier radius (LBs from observer that load full detail: terrain +
|
/// Near-tier radius (LBs from observer that load full detail: terrain +
|
||||||
/// scenery + entities). Set at construction; readable thereafter.
|
/// scenery + entities). Set at construction; readable thereafter.
|
||||||
|
|
@ -110,18 +118,22 @@ public sealed class StreamingController
|
||||||
{
|
{
|
||||||
uint centerId = StreamingRegion.EncodeLandblockId(observerCx, observerCy);
|
uint centerId = StreamingRegion.EncodeLandblockId(observerCx, observerCy);
|
||||||
|
|
||||||
if (insideDungeon)
|
if (_collapsed)
|
||||||
{
|
{
|
||||||
if (!_collapsed)
|
// Hysteresis: stay collapsed while the player remains in the dungeon
|
||||||
EnterDungeonCollapse(observerCx, observerCy, centerId);
|
// landblock, regardless of CurrCell flicker. Expand only on an actual
|
||||||
|
// landblock change (the player left through a portal / was teleported).
|
||||||
|
if (centerId != _collapsedCenter)
|
||||||
|
ExitDungeonExpand(observerCx, observerCy);
|
||||||
else
|
else
|
||||||
SweepCollapsed(centerId);
|
SweepCollapsed(centerId);
|
||||||
}
|
}
|
||||||
|
else if (insideDungeon)
|
||||||
|
{
|
||||||
|
EnterDungeonCollapse(observerCx, observerCy, centerId);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_collapsed)
|
|
||||||
ExitDungeonExpand(observerCx, observerCy);
|
|
||||||
else
|
|
||||||
NormalTick(observerCx, observerCy);
|
NormalTick(observerCx, observerCy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,6 +176,7 @@ public sealed class StreamingController
|
||||||
private void EnterDungeonCollapse(int cx, int cy, uint centerId)
|
private void EnterDungeonCollapse(int cx, int cy, uint centerId)
|
||||||
{
|
{
|
||||||
_collapsed = true;
|
_collapsed = true;
|
||||||
|
_collapsedCenter = centerId;
|
||||||
_clearPendingLoads?.Invoke();
|
_clearPendingLoads?.Invoke();
|
||||||
|
|
||||||
foreach (var id in _state.LoadedLandblockIds)
|
foreach (var id in _state.LoadedLandblockIds)
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,25 @@ public class StreamingControllerDungeonGateTests
|
||||||
Assert.Empty(h.Loads); // no spurious center reloads
|
Assert.Empty(h.Loads); // no spurious center reloads
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Collapsed_CurrCellFlickersToNull_SameLandblock_DoesNotExpand()
|
||||||
|
{
|
||||||
|
// Regression: the live run thrashed collapse↔expand because CurrCell
|
||||||
|
// momentarily resolved to null (insideDungeon=false) while the player
|
||||||
|
// stayed in the dungeon landblock — leaking lights via reload storms.
|
||||||
|
// The landblock-hysteresis must hold the collapse.
|
||||||
|
var h = Make();
|
||||||
|
h.State.AddLandblock(MakeLb(0, 7));
|
||||||
|
h.Ctrl.Tick(0, 7, insideDungeon: true); // collapse
|
||||||
|
h.Loads.Clear();
|
||||||
|
h.Unloads.Clear();
|
||||||
|
|
||||||
|
h.Ctrl.Tick(0, 7, insideDungeon: false); // CurrCell flicker, same landblock
|
||||||
|
|
||||||
|
Assert.Empty(h.Loads); // NO full-window reload
|
||||||
|
Assert.Empty(h.Unloads); // only the center is resident → nothing to sweep
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ExitsDungeon_RebuildsFullWindow_UnloadsStaleDungeonLandblock()
|
public void ExitsDungeon_RebuildsFullWindow_UnloadsStaleDungeonLandblock()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue