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
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.Types;
|
||||
|
|
@ -29,17 +28,35 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
private readonly ParticleSystem _system;
|
||||
private readonly IEntityEffectPoseSource _poses;
|
||||
private readonly IEntityEffectCellSource? _cells;
|
||||
private readonly ConcurrentDictionary<(uint EntityId, uint EmitterId), int> _handlesByKey = new();
|
||||
private readonly ConcurrentDictionary<uint, ConcurrentDictionary<int, byte>> _handlesByEntity = new();
|
||||
private readonly ConcurrentDictionary<int, EmitterBinding> _bindingsByHandle = new();
|
||||
private readonly ConcurrentDictionary<uint, ParticleRenderPass> _renderPassByEntity = new();
|
||||
private readonly ConcurrentDictionary<uint, byte> _examinationOwners = new();
|
||||
private readonly ConcurrentDictionary<uint, byte> _hiddenPresentationOwners = new();
|
||||
private readonly IEntityEffectPoseChangeSource? _poseChanges;
|
||||
|
||||
// Game-state, pose, script, and particle mutation is update-thread owned.
|
||||
// Ordinary collections avoid ConcurrentDictionary's per-binding node and
|
||||
// enumeration overhead while preserving that single-writer contract.
|
||||
private readonly Dictionary<(uint EntityId, uint EmitterId), int> _handlesByKey = new();
|
||||
private readonly Dictionary<uint, HashSet<int>> _handlesByEntity = new();
|
||||
private readonly Dictionary<int, EmitterBinding> _bindingsByHandle = new();
|
||||
private readonly Dictionary<uint, ParticleRenderPass> _renderPassByEntity = new();
|
||||
private readonly HashSet<uint> _examinationOwners = [];
|
||||
private readonly HashSet<uint> _hiddenPresentationOwners = [];
|
||||
private readonly HashSet<uint> _stoppingOwners = [];
|
||||
|
||||
// Production pose registries publish actual root/part/cell changes. Only
|
||||
// those owners are recomposed; static world emitters do no refresh work.
|
||||
// Sources without the optional notification interface retain the safe
|
||||
// owner-scoped polling fallback used by tests and plugins.
|
||||
private readonly HashSet<uint> _spatialOwners = [];
|
||||
private readonly HashSet<uint> _dirtyOwnerSet = [];
|
||||
private readonly List<uint> _dirtyOwnerOrder = [];
|
||||
private readonly List<uint> _refreshOwnerSnapshot = [];
|
||||
public ParticleHookSink(ParticleSystem system, IEntityEffectPoseSource poses)
|
||||
{
|
||||
_system = system ?? throw new ArgumentNullException(nameof(system));
|
||||
_poses = poses ?? throw new ArgumentNullException(nameof(poses));
|
||||
_cells = poses as IEntityEffectCellSource;
|
||||
_poseChanges = poses as IEntityEffectPoseChangeSource;
|
||||
if (_poseChanges is not null)
|
||||
_poseChanges.EffectPoseChanged += OnEffectPoseChanged;
|
||||
_system.EmitterDied += OnEmitterDied;
|
||||
}
|
||||
|
||||
|
|
@ -57,23 +74,28 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
public int RenderPassOwnerCount => _renderPassByEntity.Count;
|
||||
public int ExaminationOwnerCount => _examinationOwners.Count;
|
||||
public int HiddenPresentationOwnerCount => _hiddenPresentationOwners.Count;
|
||||
internal int LastRefreshOwnerVisitCount { get; private set; }
|
||||
internal int LastRefreshBindingVisitCount { get; private set; }
|
||||
|
||||
private void OnEmitterDied(int handle)
|
||||
{
|
||||
if (!_bindingsByHandle.TryRemove(handle, out EmitterBinding binding))
|
||||
if (!_bindingsByHandle.Remove(handle, out EmitterBinding binding))
|
||||
return;
|
||||
|
||||
if (binding.LogicalId != 0
|
||||
&& _handlesByKey.TryGetValue((binding.OwnerLocalId, binding.LogicalId), out int current)
|
||||
&& current == handle)
|
||||
{
|
||||
_handlesByKey.TryRemove((binding.OwnerLocalId, binding.LogicalId), out _);
|
||||
_handlesByKey.Remove((binding.OwnerLocalId, binding.LogicalId));
|
||||
}
|
||||
if (_handlesByEntity.TryGetValue(binding.OwnerLocalId, out var handles))
|
||||
{
|
||||
handles.TryRemove(handle, out _);
|
||||
if (handles.IsEmpty)
|
||||
_handlesByEntity.TryRemove(binding.OwnerLocalId, out _);
|
||||
handles.Remove(handle);
|
||||
if (handles.Count == 0)
|
||||
{
|
||||
_handlesByEntity.Remove(binding.OwnerLocalId);
|
||||
_spatialOwners.Remove(binding.OwnerLocalId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +149,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
|
||||
public void ClearEntityRenderPass(uint entityId)
|
||||
{
|
||||
_renderPassByEntity.TryRemove(entityId, out _);
|
||||
_renderPassByEntity.Remove(entityId);
|
||||
RefreshOwnerVisibilityPolicy(entityId);
|
||||
}
|
||||
|
||||
|
|
@ -139,9 +161,9 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
public void SetEntityExaminationObject(uint entityId, bool isExaminationObject)
|
||||
{
|
||||
if (isExaminationObject)
|
||||
_examinationOwners[entityId] = 0;
|
||||
_examinationOwners.Add(entityId);
|
||||
else
|
||||
_examinationOwners.TryRemove(entityId, out _);
|
||||
_examinationOwners.Remove(entityId);
|
||||
|
||||
RefreshOwnerVisibilityPolicy(entityId);
|
||||
}
|
||||
|
|
@ -154,16 +176,27 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
public void SetEntityPresentationVisible(uint entityId, bool visible)
|
||||
{
|
||||
if (visible)
|
||||
_hiddenPresentationOwners.TryRemove(entityId, out _);
|
||||
_hiddenPresentationOwners.Remove(entityId);
|
||||
else
|
||||
_hiddenPresentationOwners[entityId] = 0;
|
||||
_hiddenPresentationOwners.Add(entityId);
|
||||
|
||||
if (visible)
|
||||
{
|
||||
if (_handlesByEntity.ContainsKey(entityId))
|
||||
_spatialOwners.Add(entityId);
|
||||
MarkOwnerDirty(entityId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_spatialOwners.Remove(entityId);
|
||||
}
|
||||
|
||||
// Withdrawal is immediate. Re-entry is enabled by the next pose
|
||||
// refresh so an attached owner cannot flash for one frame at its old
|
||||
// anchor before the composed child pose is published.
|
||||
if (!visible && _handlesByEntity.TryGetValue(entityId, out var handles))
|
||||
{
|
||||
foreach (int handle in handles.Keys)
|
||||
foreach (int handle in handles)
|
||||
{
|
||||
_system.SetEmitterPresentationVisible(handle, false);
|
||||
_system.SetEmitterSimulationEnabled(handle, false);
|
||||
|
|
@ -178,57 +211,141 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
/// </summary>
|
||||
public void RefreshAttachedEmitters()
|
||||
{
|
||||
foreach ((int handle, EmitterBinding binding) in _bindingsByHandle)
|
||||
LastRefreshOwnerVisitCount = 0;
|
||||
LastRefreshBindingVisitCount = 0;
|
||||
_refreshOwnerSnapshot.Clear();
|
||||
if (_poseChanges is null)
|
||||
{
|
||||
bool presentationVisible =
|
||||
!_hiddenPresentationOwners.ContainsKey(binding.OwnerLocalId);
|
||||
if (TryResolveAnchor(binding.OwnerLocalId, binding.PartIndex,
|
||||
binding.HookOffsetOrigin,
|
||||
out Vector3 anchor,
|
||||
out Quaternion rotation,
|
||||
out Vector3 ownerPosition))
|
||||
foreach (uint ownerLocalId in _handlesByEntity.Keys)
|
||||
_refreshOwnerSnapshot.Add(ownerLocalId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_refreshOwnerSnapshot.AddRange(_dirtyOwnerOrder);
|
||||
_dirtyOwnerOrder.Clear();
|
||||
_dirtyOwnerSet.Clear();
|
||||
}
|
||||
|
||||
for (int ownerIndex = 0; ownerIndex < _refreshOwnerSnapshot.Count; ownerIndex++)
|
||||
{
|
||||
uint ownerLocalId = _refreshOwnerSnapshot[ownerIndex];
|
||||
if (!_handlesByEntity.TryGetValue(ownerLocalId, out HashSet<int>? handles))
|
||||
{
|
||||
_system.UpdateEmitterAnchor(handle, anchor, rotation);
|
||||
_system.UpdateEmitterOwnerPosition(handle, ownerPosition);
|
||||
_system.UpdateEmitterOwnerCell(
|
||||
handle,
|
||||
_cells is not null && _cells.TryGetCellId(binding.OwnerLocalId, out uint cellId)
|
||||
? cellId
|
||||
: 0u);
|
||||
// Keep the anchor current while spatially paused; re-entry
|
||||
// resumes at the authoritative pose without generating the
|
||||
// absent interval's time- or distance-driven emissions.
|
||||
_system.SetEmitterPresentationVisible(handle, presentationVisible);
|
||||
_system.SetEmitterSimulationEnabled(handle, presentationVisible);
|
||||
continue;
|
||||
}
|
||||
|
||||
LastRefreshOwnerVisitCount++;
|
||||
bool presentationVisible =
|
||||
!_hiddenPresentationOwners.Contains(ownerLocalId);
|
||||
foreach (int handle in handles)
|
||||
{
|
||||
if (!_bindingsByHandle.TryGetValue(handle, out EmitterBinding binding))
|
||||
continue;
|
||||
|
||||
LastRefreshBindingVisitCount++;
|
||||
if (TryResolveAnchor(ownerLocalId, binding.PartIndex,
|
||||
binding.HookOffsetOrigin,
|
||||
out Vector3 anchor,
|
||||
out Quaternion rotation,
|
||||
out Vector3 ownerPosition))
|
||||
{
|
||||
_system.UpdateEmitterAnchor(handle, anchor, rotation);
|
||||
_system.UpdateEmitterOwnerPosition(handle, ownerPosition);
|
||||
_system.UpdateEmitterOwnerCell(
|
||||
handle,
|
||||
_cells is not null && _cells.TryGetCellId(ownerLocalId, out uint cellId)
|
||||
? cellId
|
||||
: 0u);
|
||||
// Keep the anchor current while spatially paused; re-entry
|
||||
// resumes at the authoritative pose without generating the
|
||||
// absent interval's time- or distance-driven emissions.
|
||||
_system.SetEmitterPresentationVisible(handle, presentationVisible);
|
||||
_system.SetEmitterSimulationEnabled(handle, presentationVisible);
|
||||
}
|
||||
else
|
||||
{
|
||||
_system.SetEmitterPresentationVisible(handle, false);
|
||||
_system.SetEmitterSimulationEnabled(handle, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
_system.SetEmitterPresentationVisible(handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAllForEntity(uint entityId, bool fadeOut)
|
||||
{
|
||||
if (_handlesByEntity.TryRemove(entityId, out var handles))
|
||||
// EmitterDied callbacks may execute arbitrary hook routing. Keep one
|
||||
// owner-scoped teardown gate active until the initial bag is drained;
|
||||
// a callback cannot resurrect an emitter after its logical owner has
|
||||
// already been accepted for destruction.
|
||||
if (!_stoppingOwners.Add(entityId))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (int handle in handles.Keys)
|
||||
_spatialOwners.Remove(entityId);
|
||||
List<Exception>? failures = null;
|
||||
if (_handlesByEntity.TryGetValue(entityId, out HashSet<int>? handles))
|
||||
{
|
||||
// A fading emitter needs its clock to retire naturally. A
|
||||
// hard destroy is removed synchronously by ParticleSystem.
|
||||
if (fadeOut)
|
||||
_system.SetEmitterSimulationEnabled(handle, true);
|
||||
_system.StopEmitter(handle, fadeOut);
|
||||
_bindingsByHandle.TryRemove(handle, out _);
|
||||
int[] snapshot = [.. handles];
|
||||
for (int i = 0; i < snapshot.Length; i++)
|
||||
{
|
||||
int handle = snapshot[i];
|
||||
if (_bindingsByHandle.TryGetValue(handle, out EmitterBinding binding)
|
||||
&& binding.LogicalId != 0)
|
||||
{
|
||||
_handlesByKey.Remove((entityId, binding.LogicalId));
|
||||
}
|
||||
|
||||
// A fading emitter needs its clock to retire naturally. A
|
||||
// hard destroy is removed synchronously by ParticleSystem.
|
||||
bool stopCommitted = false;
|
||||
try
|
||||
{
|
||||
if (fadeOut)
|
||||
_system.SetEmitterSimulationEnabled(handle, true);
|
||||
_system.StopEmitter(handle, fadeOut);
|
||||
stopCommitted = true;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
// DestroyParticleEmitter removes the table entry before
|
||||
// broadcasting EmitterDied. A subscriber failure therefore
|
||||
// reports after the stop mutation committed; never replay
|
||||
// that dead handle, but retain genuinely live failures for
|
||||
// the next owner-teardown attempt.
|
||||
stopCommitted = !_system.IsEmitterAlive(handle);
|
||||
(failures ??= []).Add(error);
|
||||
}
|
||||
|
||||
if (!stopCommitted)
|
||||
continue;
|
||||
|
||||
handles.Remove(handle);
|
||||
_bindingsByHandle.Remove(handle);
|
||||
}
|
||||
|
||||
if (handles.Count == 0
|
||||
&& _handlesByEntity.TryGetValue(entityId, out HashSet<int>? current)
|
||||
&& ReferenceEquals(current, handles))
|
||||
{
|
||||
_handlesByEntity.Remove(entityId);
|
||||
}
|
||||
}
|
||||
ClearEntityRenderPass(entityId);
|
||||
_examinationOwners.Remove(entityId);
|
||||
_hiddenPresentationOwners.Remove(entityId);
|
||||
|
||||
if (failures is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
$"One or more particle emitters for owner 0x{entityId:X8} failed to stop cleanly.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in _handlesByKey.Keys)
|
||||
finally
|
||||
{
|
||||
if (key.EntityId == entityId)
|
||||
_handlesByKey.TryRemove(key, out _);
|
||||
_stoppingOwners.Remove(entityId);
|
||||
}
|
||||
ClearEntityRenderPass(entityId);
|
||||
_examinationOwners.TryRemove(entityId, out _);
|
||||
_hiddenPresentationOwners.TryRemove(entityId, out _);
|
||||
}
|
||||
|
||||
private void DestroyLogical(uint ownerLocalId, uint logicalId, bool fadeOut)
|
||||
|
|
@ -244,7 +361,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
// A blocking create with the same logical ID must therefore continue
|
||||
// to see it. DestroyParticleEmitter (0x0051B770) removes it at once.
|
||||
if (!fadeOut)
|
||||
_handlesByKey.TryRemove((ownerLocalId, logicalId), out _);
|
||||
_handlesByKey.Remove((ownerLocalId, logicalId));
|
||||
if (fadeOut)
|
||||
_system.SetEmitterSimulationEnabled(handle, true);
|
||||
_system.StopEmitter(handle, fadeOut);
|
||||
|
|
@ -258,13 +375,20 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
uint logicalId,
|
||||
bool isBlocking)
|
||||
{
|
||||
if (_stoppingOwners.Contains(ownerLocalId))
|
||||
{
|
||||
DiagnosticSink?.Invoke(
|
||||
$"Particle creation for owner 0x{ownerLocalId:X8} was ignored while its emitters were stopping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (logicalId != 0
|
||||
&& _handlesByKey.TryGetValue((ownerLocalId, logicalId), out int existing))
|
||||
{
|
||||
if (isBlocking && _system.IsEmitterAlive(existing))
|
||||
return;
|
||||
|
||||
_handlesByKey.TryRemove((ownerLocalId, logicalId), out _);
|
||||
_handlesByKey.Remove((ownerLocalId, logicalId));
|
||||
_system.StopEmitter(existing, fadeOut: false);
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +430,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
_cells is not null && _cells.TryGetCellId(ownerLocalId, out uint cellId)
|
||||
? cellId
|
||||
: 0u);
|
||||
if (_hiddenPresentationOwners.ContainsKey(ownerLocalId))
|
||||
if (_hiddenPresentationOwners.Contains(ownerLocalId))
|
||||
{
|
||||
_system.SetEmitterPresentationVisible(handle, false);
|
||||
_system.SetEmitterSimulationEnabled(handle, false);
|
||||
|
|
@ -320,16 +444,21 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
logicalId,
|
||||
renderPass);
|
||||
_bindingsByHandle[handle] = binding;
|
||||
_handlesByEntity
|
||||
.GetOrAdd(ownerLocalId, _ => new ConcurrentDictionary<int, byte>())
|
||||
.TryAdd(handle, 0);
|
||||
if (!_handlesByEntity.TryGetValue(ownerLocalId, out HashSet<int>? handles))
|
||||
{
|
||||
handles = [];
|
||||
_handlesByEntity.Add(ownerLocalId, handles);
|
||||
}
|
||||
handles.Add(handle);
|
||||
if (!_hiddenPresentationOwners.Contains(ownerLocalId))
|
||||
_spatialOwners.Add(ownerLocalId);
|
||||
if (logicalId != 0)
|
||||
_handlesByKey[(ownerLocalId, logicalId)] = handle;
|
||||
}
|
||||
|
||||
private ParticleVisibilityPolicy ResolveVisibilityPolicy(uint ownerLocalId)
|
||||
{
|
||||
if (_examinationOwners.ContainsKey(ownerLocalId))
|
||||
if (_examinationOwners.Contains(ownerLocalId))
|
||||
return ParticleVisibilityPolicy.Examination;
|
||||
|
||||
return _renderPassByEntity.TryGetValue(ownerLocalId, out ParticleRenderPass pass)
|
||||
|
|
@ -344,10 +473,24 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
return;
|
||||
|
||||
ParticleVisibilityPolicy policy = ResolveVisibilityPolicy(ownerLocalId);
|
||||
foreach (int handle in handles.Keys)
|
||||
foreach (int handle in handles)
|
||||
_system.SetEmitterVisibilityPolicy(handle, policy);
|
||||
}
|
||||
|
||||
private void OnEffectPoseChanged(uint ownerLocalId)
|
||||
=> MarkOwnerDirty(ownerLocalId);
|
||||
|
||||
private void MarkOwnerDirty(uint ownerLocalId)
|
||||
{
|
||||
if (!_handlesByEntity.ContainsKey(ownerLocalId)
|
||||
|| !_dirtyOwnerSet.Add(ownerLocalId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dirtyOwnerOrder.Add(ownerLocalId);
|
||||
}
|
||||
|
||||
private bool TryResolveAnchor(
|
||||
uint ownerLocalId,
|
||||
int partIndex,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue