fix(streaming): commit EnvCell landblocks atomically (#214)

Carry one complete cell payload with each streaming completion and publish visibility, physics, and render state on the render thread. Remove the obsolete post-readiness login reload that triggered a duplicate dungeon build.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-13 18:45:24 +02:00
parent 85980fe13f
commit b0c175afc0
26 changed files with 973 additions and 617 deletions

View file

@ -20,7 +20,7 @@ public sealed class StreamingController
private readonly Action<uint, LandblockStreamJobKind> _enqueueLoad;
private readonly Action<uint> _enqueueUnload;
private readonly Func<int, IReadOnlyList<LandblockStreamResult>> _drainCompletions;
private readonly Action<LoadedLandblock, LandblockMeshData> _applyTerrain;
private readonly Action<LandblockBuild, LandblockMeshData> _applyTerrain;
private readonly Action<uint>? _removeTerrain;
private readonly Action? _clearPendingLoads;
@ -140,7 +140,7 @@ public sealed class StreamingController
Action<uint, LandblockStreamJobKind> enqueueLoad,
Action<uint> enqueueUnload,
Func<int, IReadOnlyList<LandblockStreamResult>> drainCompletions,
Action<LoadedLandblock, LandblockMeshData> applyTerrain,
Action<LandblockBuild, LandblockMeshData> applyTerrain,
GpuWorldState state,
int nearRadius,
int farRadius,
@ -233,6 +233,20 @@ public sealed class StreamingController
/// re-sent spawn or a same-frame double call costs nothing. Render-thread only,
/// same as <see cref="Tick"/>.</para>
/// </summary>
public void InitializeKnownLoginCenter(int cx, int cy, bool isSealedDungeon)
{
// StreamingReadinessGate keeps the worker stopped until the player's
// real spawn supplies this center. There is therefore no guessed-center
// window to force-reload here. A sealed dungeon is the one special case:
// pin radius zero before the first Tick so no ocean-grid neighbours are
// ever enqueued.
if (isSealedDungeon)
PreCollapseToDungeon(cx, cy);
}
/// <summary>
/// Pins streaming to a sealed dungeon before the first normal Tick.
/// </summary>
public void PreCollapseToDungeon(int cx, int cy)
{
uint centerId = StreamingRegion.EncodeLandblockId(cx, cy);
@ -438,7 +452,7 @@ public sealed class StreamingController
switch (result)
{
case LandblockStreamResult.Loaded loaded:
_applyTerrain(loaded.Landblock, loaded.MeshData);
_applyTerrain(loaded.Build, loaded.MeshData);
_state.AddLandblock(loaded.Landblock);
// #138: after the landblock is in _loaded (so AppendLiveEntity
// hot-paths), restore any retained server objects ACE won't
@ -446,7 +460,7 @@ public sealed class StreamingController
_onLandblockLoaded?.Invoke(loaded.Landblock.LandblockId);
break;
case LandblockStreamResult.Promoted promoted:
_applyTerrain(promoted.Landblock, promoted.MeshData);
_applyTerrain(promoted.Build, promoted.MeshData);
_state.AddEntitiesToExistingLandblock(promoted.LandblockId, promoted.Entities);
_onLandblockLoaded?.Invoke(promoted.LandblockId);
break;