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
|
|
@ -37,6 +37,12 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
private readonly Dictionary<uint, Queue<PendingEffect>> _pendingByServerGuid = new();
|
||||
private readonly Dictionary<uint, WorldEntity> _staticOwners = new();
|
||||
private readonly HashSet<uint> _syntheticOwners = new();
|
||||
private readonly HashSet<LiveEntityRecord> _dirtyLiveOwners =
|
||||
new(ReferenceEqualityComparer.Instance);
|
||||
private readonly List<LiveEntityRecord> _dirtyLiveOwnerOrder = [];
|
||||
private readonly List<LiveEntityRecord> _dirtyLiveOwnerSnapshot = [];
|
||||
private readonly List<uint> _readyGuidSnapshot = [];
|
||||
private uint _posePublishLocalId;
|
||||
private Action<string>? _diagnosticSink = Console.WriteLine;
|
||||
|
||||
public EntityEffectController(
|
||||
|
|
@ -58,6 +64,8 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
_ownerUnregistered = ownerUnregistered ?? (_ => { });
|
||||
_ownerSoundTableChanged = ownerSoundTableChanged ?? ((_, _) => { });
|
||||
_runner.DiagnosticSink = message => _diagnosticSink?.Invoke(message);
|
||||
_poses.EffectPoseChanged += OnEffectPoseChanged;
|
||||
_liveEntities.ProjectionVisibilityChanged += OnProjectionVisibilityChanged;
|
||||
}
|
||||
|
||||
public Action<string>? DiagnosticSink
|
||||
|
|
@ -68,6 +76,7 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
|
||||
public int PendingPacketCount => _pendingByServerGuid.Values.Sum(queue => queue.Count);
|
||||
public int ReadyOwnerCount => _profilesByLocalId.Count;
|
||||
internal int LastPoseRefreshOwnerVisitCount { get; private set; }
|
||||
|
||||
public void HandleDirect(PlayPhysicsScript message)
|
||||
{
|
||||
|
|
@ -222,7 +231,9 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
|
||||
public void ClearNetworkState()
|
||||
{
|
||||
foreach (uint serverGuid in _readyGenerationByServerGuid.Keys.ToArray())
|
||||
_readyGuidSnapshot.Clear();
|
||||
_readyGuidSnapshot.AddRange(_readyGenerationByServerGuid.Keys);
|
||||
foreach (uint serverGuid in _readyGuidSnapshot)
|
||||
{
|
||||
if (_liveEntities.TryGetLocalEntityId(serverGuid, out uint localId))
|
||||
{
|
||||
|
|
@ -233,20 +244,54 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
}
|
||||
_readyGenerationByServerGuid.Clear();
|
||||
_pendingByServerGuid.Clear();
|
||||
_dirtyLiveOwners.Clear();
|
||||
_dirtyLiveOwnerOrder.Clear();
|
||||
_dirtyLiveOwnerSnapshot.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Refreshes every live root after animation/movement.</summary>
|
||||
/// <summary>
|
||||
/// Publishes only roots dirtied by movement, animation, cell projection,
|
||||
/// or an authoritative position update. The queue is detached before
|
||||
/// callbacks run, so a callback-produced mutation is retained for the
|
||||
/// following update rather than invalidating this traversal.
|
||||
/// </summary>
|
||||
public void RefreshLiveOwnerPoses()
|
||||
{
|
||||
foreach (uint serverGuid in _readyGenerationByServerGuid.Keys.ToArray())
|
||||
LastPoseRefreshOwnerVisitCount = 0;
|
||||
if (_dirtyLiveOwnerOrder.Count == 0)
|
||||
return;
|
||||
|
||||
_dirtyLiveOwnerSnapshot.Clear();
|
||||
_dirtyLiveOwnerSnapshot.AddRange(_dirtyLiveOwnerOrder);
|
||||
_dirtyLiveOwnerOrder.Clear();
|
||||
_dirtyLiveOwners.Clear();
|
||||
foreach (LiveEntityRecord record in _dirtyLiveOwnerSnapshot)
|
||||
{
|
||||
if (!TryGetReadyLocalId(serverGuid, out uint localId))
|
||||
if (!_liveEntities.TryGetRecord(record.ServerGuid, out LiveEntityRecord current)
|
||||
|| !ReferenceEquals(current, record)
|
||||
|| !TryGetReadyLocalId(record.ServerGuid, out uint localId)
|
||||
|| record.WorldEntity?.Id != localId)
|
||||
{
|
||||
continue;
|
||||
RefreshLiveAnchor(serverGuid, localId);
|
||||
TryReplayPending(serverGuid, localId);
|
||||
}
|
||||
|
||||
LastPoseRefreshOwnerVisitCount++;
|
||||
RefreshLiveAnchor(record.ServerGuid, localId);
|
||||
TryReplayPending(record.ServerGuid, localId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an accepted authoritative root mutation. The record reference,
|
||||
/// rather than only its server GUID, isolates a queued update from later
|
||||
/// delete/recreate reuse of that GUID.
|
||||
/// </summary>
|
||||
public void MarkLiveOwnerPoseDirty(uint serverGuid)
|
||||
{
|
||||
if (_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record))
|
||||
MarkLiveOwnerPoseDirty(record);
|
||||
}
|
||||
|
||||
public bool PlayDirect(uint ownerLocalId, uint scriptDid)
|
||||
{
|
||||
if (!CanStartOwner(ownerLocalId))
|
||||
|
|
@ -411,6 +456,37 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
return false;
|
||||
}
|
||||
|
||||
private void OnEffectPoseChanged(uint localId)
|
||||
{
|
||||
if (_posePublishLocalId == localId)
|
||||
return;
|
||||
if (_liveEntities.TryGetServerGuid(localId, out uint serverGuid)
|
||||
&& _liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record))
|
||||
{
|
||||
MarkLiveOwnerPoseDirty(record);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
|
||||
{
|
||||
if (visible)
|
||||
MarkLiveOwnerPoseDirty(record);
|
||||
}
|
||||
|
||||
private void MarkLiveOwnerPoseDirty(LiveEntityRecord record)
|
||||
{
|
||||
if (!_readyGenerationByServerGuid.TryGetValue(
|
||||
record.ServerGuid,
|
||||
out ushort readyGeneration)
|
||||
|| readyGeneration != record.Generation
|
||||
|| !_dirtyLiveOwners.Add(record))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dirtyLiveOwnerOrder.Add(record);
|
||||
}
|
||||
|
||||
private void RefreshLiveAnchor(uint serverGuid, uint localId)
|
||||
{
|
||||
if (_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
|
|
@ -423,7 +499,18 @@ public sealed class EntityEffectController : IAnimationHookSink
|
|||
// bookkeeping Position would collapse a weapon effect to the
|
||||
// parent's origin.
|
||||
if (record.ProjectionKind is LiveEntityProjectionKind.World)
|
||||
_poses.UpdateRoot(entity);
|
||||
{
|
||||
uint previousPublish = _posePublishLocalId;
|
||||
_posePublishLocalId = localId;
|
||||
try
|
||||
{
|
||||
_poses.UpdateRoot(entity);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_posePublishLocalId = previousPublish;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 anchor = _poses.TryGetRootPose(localId, out Matrix4x4 rootWorld)
|
||||
? rootWorld.Translation
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ namespace AcDream.App.Rendering.Vfx;
|
|||
/// (<c>0x0051D180</c>). This registry is the modern, read-only seam exposing
|
||||
/// those same final frames without coupling Core effects to the renderer.
|
||||
/// </remarks>
|
||||
public sealed class EntityEffectPoseRegistry : IEntityEffectPoseSource, IEntityEffectCellSource
|
||||
public sealed class EntityEffectPoseRegistry :
|
||||
IEntityEffectPoseSource,
|
||||
IEntityEffectCellSource,
|
||||
IEntityEffectPoseChangeSource
|
||||
{
|
||||
private sealed class PoseRecord
|
||||
{
|
||||
|
|
@ -27,6 +30,8 @@ public sealed class EntityEffectPoseRegistry : IEntityEffectPoseSource, IEntityE
|
|||
|
||||
private readonly Dictionary<uint, PoseRecord> _poses = new();
|
||||
|
||||
public event Action<uint>? EffectPoseChanged;
|
||||
|
||||
public int Count => _poses.Count;
|
||||
|
||||
public void Publish(WorldEntity entity, IReadOnlyList<Matrix4x4> partLocal)
|
||||
|
|
@ -60,17 +65,24 @@ public sealed class EntityEffectPoseRegistry : IEntityEffectPoseSource, IEntityE
|
|||
Matrix4x4 rootWorld = Matrix4x4.CreateFromQuaternion(entity.Rotation)
|
||||
* Matrix4x4.CreateTranslation(entity.Position);
|
||||
|
||||
bool changed;
|
||||
if (!_poses.TryGetValue(entity.Id, out PoseRecord? record))
|
||||
{
|
||||
record = new PoseRecord();
|
||||
_poses.Add(entity.Id, record);
|
||||
changed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
changed = record.RootWorld != rootWorld
|
||||
|| record.CellId != (entity.EffectCellId ?? entity.ParentCellId ?? 0u);
|
||||
}
|
||||
|
||||
record.RootWorld = rootWorld;
|
||||
record.CellId = entity.EffectCellId ?? entity.ParentCellId ?? 0u;
|
||||
if (entity.IndexedPartTransforms.Count > 0)
|
||||
{
|
||||
CopyParts(
|
||||
changed |= CopyParts(
|
||||
record,
|
||||
entity.IndexedPartTransforms,
|
||||
entity.IndexedPartAvailable);
|
||||
|
|
@ -78,15 +90,26 @@ public sealed class EntityEffectPoseRegistry : IEntityEffectPoseSource, IEntityE
|
|||
else
|
||||
{
|
||||
if (record.PartLocal.Length != entity.MeshRefs.Count)
|
||||
{
|
||||
record.PartLocal = new Matrix4x4[entity.MeshRefs.Count];
|
||||
changed = true;
|
||||
}
|
||||
if (record.PartAvailable.Length != entity.MeshRefs.Count)
|
||||
{
|
||||
record.PartAvailable = new bool[entity.MeshRefs.Count];
|
||||
changed = true;
|
||||
}
|
||||
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||
{
|
||||
changed |= record.PartLocal[i] != entity.MeshRefs[i].PartTransform
|
||||
|| !record.PartAvailable[i];
|
||||
record.PartLocal[i] = entity.MeshRefs[i].PartTransform;
|
||||
record.PartAvailable[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
EffectPoseChanged?.Invoke(entity.Id);
|
||||
}
|
||||
|
||||
public void Publish(
|
||||
|
|
@ -100,15 +123,23 @@ public sealed class EntityEffectPoseRegistry : IEntityEffectPoseSource, IEntityE
|
|||
return;
|
||||
ArgumentNullException.ThrowIfNull(partLocal);
|
||||
|
||||
bool changed;
|
||||
if (!_poses.TryGetValue(localEntityId, out PoseRecord? record))
|
||||
{
|
||||
record = new PoseRecord();
|
||||
_poses.Add(localEntityId, record);
|
||||
changed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
changed = record.RootWorld != rootWorld || record.CellId != cellId;
|
||||
}
|
||||
|
||||
record.RootWorld = rootWorld;
|
||||
record.CellId = cellId;
|
||||
CopyParts(record, partLocal, availability);
|
||||
changed |= CopyParts(record, partLocal, availability);
|
||||
if (changed)
|
||||
EffectPoseChanged?.Invoke(localEntityId);
|
||||
}
|
||||
|
||||
/// <summary>Refresh only the moving root while retaining current part poses.</summary>
|
||||
|
|
@ -117,15 +148,36 @@ public sealed class EntityEffectPoseRegistry : IEntityEffectPoseSource, IEntityE
|
|||
ArgumentNullException.ThrowIfNull(entity);
|
||||
if (!_poses.TryGetValue(entity.Id, out PoseRecord? record))
|
||||
return false;
|
||||
record.RootWorld = Matrix4x4.CreateFromQuaternion(entity.Rotation)
|
||||
Matrix4x4 rootWorld = Matrix4x4.CreateFromQuaternion(entity.Rotation)
|
||||
* Matrix4x4.CreateTranslation(entity.Position);
|
||||
record.CellId = entity.EffectCellId ?? entity.ParentCellId ?? 0u;
|
||||
uint cellId = entity.EffectCellId ?? entity.ParentCellId ?? 0u;
|
||||
if (record.RootWorld == rootWorld && record.CellId == cellId)
|
||||
return true;
|
||||
|
||||
record.RootWorld = rootWorld;
|
||||
record.CellId = cellId;
|
||||
EffectPoseChanged?.Invoke(entity.Id);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Remove(uint localEntityId) => _poses.Remove(localEntityId);
|
||||
public bool Remove(uint localEntityId)
|
||||
{
|
||||
if (!_poses.Remove(localEntityId))
|
||||
return false;
|
||||
EffectPoseChanged?.Invoke(localEntityId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Clear() => _poses.Clear();
|
||||
public void Clear()
|
||||
{
|
||||
if (_poses.Count == 0)
|
||||
return;
|
||||
|
||||
uint[] removedOwners = _poses.Keys.ToArray();
|
||||
_poses.Clear();
|
||||
foreach (uint owner in removedOwners)
|
||||
EffectPoseChanged?.Invoke(owner);
|
||||
}
|
||||
|
||||
public bool TryGetRootPose(uint localEntityId, out Matrix4x4 rootWorld)
|
||||
{
|
||||
|
|
@ -196,21 +248,32 @@ public sealed class EntityEffectPoseRegistry : IEntityEffectPoseSource, IEntityE
|
|||
return false;
|
||||
}
|
||||
|
||||
private static void CopyParts(
|
||||
private static bool CopyParts(
|
||||
PoseRecord record,
|
||||
IReadOnlyList<Matrix4x4> partLocal,
|
||||
IReadOnlyList<bool>? availability)
|
||||
{
|
||||
if (availability is not null && availability.Count != partLocal.Count)
|
||||
throw new ArgumentException("Part pose and availability counts must match.");
|
||||
bool changed = false;
|
||||
if (record.PartLocal.Length != partLocal.Count)
|
||||
{
|
||||
record.PartLocal = new Matrix4x4[partLocal.Count];
|
||||
changed = true;
|
||||
}
|
||||
if (record.PartAvailable.Length != partLocal.Count)
|
||||
{
|
||||
record.PartAvailable = new bool[partLocal.Count];
|
||||
changed = true;
|
||||
}
|
||||
for (int i = 0; i < partLocal.Count; i++)
|
||||
{
|
||||
bool partAvailable = availability is null || availability[i];
|
||||
changed |= record.PartLocal[i] != partLocal[i]
|
||||
|| record.PartAvailable[i] != partAvailable;
|
||||
record.PartLocal[i] = partLocal[i];
|
||||
record.PartAvailable[i] = availability is null || availability[i];
|
||||
record.PartAvailable[i] = partAvailable;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue