fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
|
|
@ -6,8 +6,20 @@ namespace AcDream.Core.Plugins;
|
|||
public sealed class WorldEvents : IEvents
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private readonly List<WorldEntitySnapshot> _alreadySpawned = new();
|
||||
private Action<WorldEntitySnapshot>? _subscribers;
|
||||
// Late subscribers replay the current projected world, not every entity
|
||||
// encountered since process start. Spawn notifications themselves remain
|
||||
// ephemeral; the public plugin API does not expose a despawn event yet.
|
||||
private readonly Dictionary<uint, WorldEntitySnapshot> _current = new();
|
||||
private readonly List<Subscription> _subscriptions = new();
|
||||
private Subscription[] _liveSnapshot = Array.Empty<Subscription>();
|
||||
|
||||
private sealed class Subscription(Action<WorldEntitySnapshot> handler)
|
||||
{
|
||||
public Action<WorldEntitySnapshot> Handler { get; } = handler;
|
||||
public Queue<WorldEntitySnapshot> Pending { get; } = new();
|
||||
public bool Replaying { get; set; } = true;
|
||||
public bool Active { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the host as each entity is hydrated into the world. Records the
|
||||
|
|
@ -15,42 +27,155 @@ public sealed class WorldEvents : IEvents
|
|||
/// </summary>
|
||||
public void FireEntitySpawned(WorldEntitySnapshot snapshot)
|
||||
{
|
||||
Action<WorldEntitySnapshot>? toNotify;
|
||||
Subscription[] toNotify;
|
||||
lock (_lock)
|
||||
{
|
||||
_alreadySpawned.Add(snapshot);
|
||||
toNotify = _subscribers;
|
||||
_current[snapshot.Id] = snapshot;
|
||||
for (int i = 0; i < _subscriptions.Count; i++)
|
||||
{
|
||||
Subscription subscription = _subscriptions[i];
|
||||
if (subscription.Active && subscription.Replaying)
|
||||
subscription.Pending.Enqueue(snapshot);
|
||||
}
|
||||
toNotify = _liveSnapshot;
|
||||
}
|
||||
|
||||
if (toNotify is null) return;
|
||||
foreach (Action<WorldEntitySnapshot> handler in toNotify.GetInvocationList())
|
||||
for (int i = 0; i < toNotify.Length; i++)
|
||||
{
|
||||
try { handler(snapshot); }
|
||||
try { toNotify[i].Handler(snapshot); }
|
||||
catch { /* plugin errors don't propagate out of event dispatch */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore current replay membership without emitting another logical
|
||||
/// spawn notification. Used when a still-live object re-enters the world.
|
||||
/// </summary>
|
||||
public void UpsertCurrent(WorldEntitySnapshot snapshot)
|
||||
{
|
||||
lock (_lock)
|
||||
_current[snapshot.Id] = snapshot;
|
||||
}
|
||||
|
||||
public bool ForgetEntity(uint id)
|
||||
{
|
||||
lock (_lock)
|
||||
return _current.Remove(id);
|
||||
}
|
||||
|
||||
public void ClearCurrent()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_current.Clear();
|
||||
for (int i = 0; i < _subscriptions.Count; i++)
|
||||
{
|
||||
Subscription subscription = _subscriptions[i];
|
||||
if (subscription.Replaying)
|
||||
subscription.Pending.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<WorldEntitySnapshot> EntitySpawned
|
||||
{
|
||||
add
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
var subscription = new Subscription(value);
|
||||
WorldEntitySnapshot[] replay;
|
||||
lock (_lock)
|
||||
{
|
||||
_subscribers += value;
|
||||
replay = _alreadySpawned.ToArray();
|
||||
_subscriptions.Add(subscription);
|
||||
replay = _current.Values.ToArray();
|
||||
}
|
||||
// Replay outside the lock to avoid deadlock if a handler re-enters.
|
||||
|
||||
// Replay outside the lock. Live events that arrive meanwhile queue
|
||||
// behind this snapshot and drain before the subscription joins the
|
||||
// direct multicast, preserving one monotonic delivery order.
|
||||
foreach (var s in replay)
|
||||
{
|
||||
try { value(s); }
|
||||
lock (_lock)
|
||||
{
|
||||
if (!subscription.Active)
|
||||
return;
|
||||
if (!_current.TryGetValue(s.Id, out WorldEntitySnapshot current)
|
||||
|| current != s)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try { subscription.Handler(s); }
|
||||
catch { /* plugin errors don't propagate out of += */ }
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
WorldEntitySnapshot pending;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!subscription.Active)
|
||||
return;
|
||||
if (!subscription.Pending.TryDequeue(out pending))
|
||||
{
|
||||
subscription.Replaying = false;
|
||||
RebuildLiveSnapshotLocked();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try { subscription.Handler(pending); }
|
||||
catch { /* plugin errors don't propagate out of += */ }
|
||||
}
|
||||
}
|
||||
remove
|
||||
{
|
||||
if (value is null)
|
||||
return;
|
||||
lock (_lock)
|
||||
_subscribers -= value;
|
||||
{
|
||||
for (int i = _subscriptions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Subscription subscription = _subscriptions[i];
|
||||
if (subscription.Handler != value)
|
||||
continue;
|
||||
|
||||
subscription.Active = false;
|
||||
subscription.Pending.Clear();
|
||||
_subscriptions.RemoveAt(i);
|
||||
if (!subscription.Replaying)
|
||||
RebuildLiveSnapshotLocked();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildLiveSnapshotLocked()
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < _subscriptions.Count; i++)
|
||||
{
|
||||
Subscription subscription = _subscriptions[i];
|
||||
if (subscription.Active && !subscription.Replaying)
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
_liveSnapshot = Array.Empty<Subscription>();
|
||||
return;
|
||||
}
|
||||
|
||||
var rebuilt = new Subscription[count];
|
||||
int write = 0;
|
||||
for (int i = 0; i < _subscriptions.Count; i++)
|
||||
{
|
||||
Subscription subscription = _subscriptions[i];
|
||||
if (subscription.Active && !subscription.Replaying)
|
||||
rebuilt[write++] = subscription;
|
||||
}
|
||||
_liveSnapshot = rebuilt;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,25 @@ namespace AcDream.Core.Plugins;
|
|||
public sealed class WorldGameState : IGameState
|
||||
{
|
||||
private readonly List<WorldEntitySnapshot> _entities = new();
|
||||
private readonly Dictionary<uint, int> _indexById = new();
|
||||
|
||||
public IReadOnlyList<WorldEntitySnapshot> Entities => _entities;
|
||||
|
||||
/// <summary>Called by the host as each entity is hydrated.</summary>
|
||||
public void Add(WorldEntitySnapshot snapshot) => _entities.Add(snapshot);
|
||||
/// <summary>
|
||||
/// Publish the current projection for an entity. Re-hydration replaces the
|
||||
/// prior snapshot instead of turning the current-state API into history.
|
||||
/// </summary>
|
||||
public void Add(WorldEntitySnapshot snapshot)
|
||||
{
|
||||
if (_indexById.TryGetValue(snapshot.Id, out int index))
|
||||
{
|
||||
_entities[index] = snapshot;
|
||||
return;
|
||||
}
|
||||
|
||||
_indexById.Add(snapshot.Id, _entities.Count);
|
||||
_entities.Add(snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove any snapshot with the given local <c>Id</c>. Used when the
|
||||
|
|
@ -18,5 +32,25 @@ public sealed class WorldGameState : IGameState
|
|||
/// acdream — the host deletes the old snapshot before adding the new
|
||||
/// one so plugins don't see stale duplicates.
|
||||
/// </summary>
|
||||
public void RemoveById(uint id) => _entities.RemoveAll(e => e.Id == id);
|
||||
public bool RemoveById(uint id)
|
||||
{
|
||||
if (!_indexById.Remove(id, out int index))
|
||||
return false;
|
||||
|
||||
int lastIndex = _entities.Count - 1;
|
||||
if (index != lastIndex)
|
||||
{
|
||||
WorldEntitySnapshot moved = _entities[lastIndex];
|
||||
_entities[index] = moved;
|
||||
_indexById[moved.Id] = index;
|
||||
}
|
||||
_entities.RemoveAt(lastIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_entities.Clear();
|
||||
_indexById.Clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue