diff --git a/src/AcDream.App/Streaming/GpuWorldState.cs b/src/AcDream.App/Streaming/GpuWorldState.cs new file mode 100644 index 0000000..1e7702b --- /dev/null +++ b/src/AcDream.App/Streaming/GpuWorldState.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Linq; +using AcDream.Core.World; + +namespace AcDream.App.Streaming; + +/// +/// Render-thread-owned registry of currently-loaded landblocks and their +/// entities. All mutation happens in +/// on the render thread; the renderer reads once per +/// frame. +/// +/// +/// Replaces GameWindow._entities, which was a flat list updated in +/// multiple places. This class is the single point of truth for "what's in +/// the world right now" and the only thing that mutates it. +/// +/// +/// +/// Threading: not thread-safe. All calls must happen on the render thread. +/// The streaming worker never touches this type. +/// +/// +public sealed class GpuWorldState +{ + private readonly Dictionary _loaded = new(); + + // Cached flat view over all entities across all loaded landblocks, + // rebuilt on each add/remove. The renderer holds a reference to this + // list, so rebuilding it replaces the reference atomically. + private IReadOnlyList _flatEntities = System.Array.Empty(); + + public IReadOnlyList Entities => _flatEntities; + public IReadOnlyCollection LoadedLandblockIds => _loaded.Keys; + + public bool IsLoaded(uint landblockId) => _loaded.ContainsKey(landblockId); + + public void AddLandblock(LoadedLandblock landblock) + { + _loaded[landblock.LandblockId] = landblock; + RebuildFlatView(); + } + + public void RemoveLandblock(uint landblockId) + { + if (_loaded.Remove(landblockId)) + RebuildFlatView(); + } + + /// + /// Append an entity to a specific landblock's slot. Used by the live + /// CreateObject path where the server spawns entities into an already- + /// loaded landblock after the initial hydration pass. + /// + public void AppendLiveEntity(uint landblockId, WorldEntity entity) + { + if (!_loaded.TryGetValue(landblockId, out var lb)) + return; + + // LoadedLandblock.Entities is an IReadOnlyList. Rebuild the + // landblock record with the new entity appended. We accept the + // allocation here because live spawns are rare compared to frame + // iteration. + var newEntities = new List(lb.Entities.Count + 1); + newEntities.AddRange(lb.Entities); + newEntities.Add(entity); + _loaded[landblockId] = new LoadedLandblock(lb.LandblockId, lb.Heightmap, newEntities); + RebuildFlatView(); + } + + private void RebuildFlatView() + { + _flatEntities = _loaded.Values.SelectMany(lb => lb.Entities).ToArray(); + } +}