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.
|
||||
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>
|
||||
/// Near-tier radius (LBs from observer that load full detail: terrain +
|
||||
/// scenery + entities). Set at construction; readable thereafter.
|
||||
|
|
@ -110,19 +118,23 @@ public sealed class StreamingController
|
|||
{
|
||||
uint centerId = StreamingRegion.EncodeLandblockId(observerCx, observerCy);
|
||||
|
||||
if (insideDungeon)
|
||||
if (_collapsed)
|
||||
{
|
||||
if (!_collapsed)
|
||||
EnterDungeonCollapse(observerCx, observerCy, centerId);
|
||||
// Hysteresis: stay collapsed while the player remains in the dungeon
|
||||
// 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
|
||||
SweepCollapsed(centerId);
|
||||
}
|
||||
else if (insideDungeon)
|
||||
{
|
||||
EnterDungeonCollapse(observerCx, observerCy, centerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_collapsed)
|
||||
ExitDungeonExpand(observerCx, observerCy);
|
||||
else
|
||||
NormalTick(observerCx, observerCy);
|
||||
NormalTick(observerCx, observerCy);
|
||||
}
|
||||
|
||||
DrainAndApply();
|
||||
|
|
@ -164,6 +176,7 @@ public sealed class StreamingController
|
|||
private void EnterDungeonCollapse(int cx, int cy, uint centerId)
|
||||
{
|
||||
_collapsed = true;
|
||||
_collapsedCenter = centerId;
|
||||
_clearPendingLoads?.Invoke();
|
||||
|
||||
foreach (var id in _state.LoadedLandblockIds)
|
||||
|
|
|
|||
|
|
@ -109,6 +109,25 @@ public class StreamingControllerDungeonGateTests
|
|||
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]
|
||||
public void ExitsDungeon_RebuildsFullWindow_UnloadsStaleDungeonLandblock()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue