feat(app): Phase A.1 — GpuWorldState render-thread entity registry
Replaces GameWindow._entities flat list with a per-landblock dict keyed by landblock id. The flat entity view is rebuilt on add/remove so the renderer keeps its simple "iterate Entities" loop. Also provides AppendLiveEntity for the CreateObject path that spawns entities into an already-loaded landblock after hydration. Not thread-safe — all mutation is render-thread only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
495f87a4ad
commit
6b70b1201d
1 changed files with 75 additions and 0 deletions
75
src/AcDream.App/Streaming/GpuWorldState.cs
Normal file
75
src/AcDream.App/Streaming/GpuWorldState.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// Render-thread-owned registry of currently-loaded landblocks and their
|
||||
/// entities. All mutation happens in <see cref="StreamingController.Tick"/>
|
||||
/// on the render thread; the renderer reads <see cref="Entities"/> once per
|
||||
/// frame.
|
||||
///
|
||||
/// <para>
|
||||
/// Replaces <c>GameWindow._entities</c>, 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.
|
||||
/// </para>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Threading: not thread-safe. All calls must happen on the render thread.
|
||||
/// The streaming worker never touches this type.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public sealed class GpuWorldState
|
||||
{
|
||||
private readonly Dictionary<uint, LoadedLandblock> _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<WorldEntity> _flatEntities = System.Array.Empty<WorldEntity>();
|
||||
|
||||
public IReadOnlyList<WorldEntity> Entities => _flatEntities;
|
||||
public IReadOnlyCollection<uint> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<WorldEntity>(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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue