perf(rendering): reconcile only changed attachments
This commit is contained in:
parent
6b56f4bef2
commit
b9cbf5e040
8 changed files with 246 additions and 14 deletions
|
|
@ -49,9 +49,20 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingPoseLossRemovalByChild = new();
|
||||
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingOrphanRemovalByChild = new();
|
||||
private readonly List<uint> _pendingProjectionChildrenScratch = new();
|
||||
private readonly List<KeyValuePair<uint, PendingOrdinaryRemoval>>
|
||||
_pendingOrdinaryRemovalScratch = new();
|
||||
private readonly List<KeyValuePair<uint, PendingProjectionSubtree>>
|
||||
_pendingProjectionSubtreeScratch = new();
|
||||
private readonly List<KeyValuePair<uint, PendingUnparentTransition>>
|
||||
_pendingUnparentScratch = new();
|
||||
private readonly AttachmentUpdateOrder<AttachedChild> _updateOrder = new();
|
||||
private readonly Func<AttachedChild, uint?> _parentOfAttached;
|
||||
private readonly Func<uint, bool> _tickAttached;
|
||||
private readonly Func<uint, bool> _reconcileAttached;
|
||||
private int _activePoseCompositionVisits;
|
||||
|
||||
internal int LastFullPoseCompositionVisits { get; private set; }
|
||||
internal int LastReconcilePoseCompositionVisits { get; private set; }
|
||||
|
||||
public IEnumerable<uint> AttachedEntityIds
|
||||
{
|
||||
|
|
@ -82,6 +93,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
?? throw new ArgumentNullException(nameof(withdrawProjection));
|
||||
_parentOfAttached = static child => child.ParentGuid;
|
||||
_tickAttached = TickChild;
|
||||
_reconcileAttached = ReconcileChild;
|
||||
|
||||
_objects.ObjectMoved += OnObjectMoved;
|
||||
_objects.MoveRolledBack += OnMoveRolledBack;
|
||||
|
|
@ -265,25 +277,53 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
public void Tick()
|
||||
{
|
||||
RetryPendingProjectionTransitions();
|
||||
_activePoseCompositionVisits = 0;
|
||||
IReadOnlyList<uint> failed = _updateOrder.ForEachParentFirst(
|
||||
_attachedByChild,
|
||||
_parentOfAttached,
|
||||
_tickAttached);
|
||||
LastFullPoseCompositionVisits = _activePoseCompositionVisits;
|
||||
for (int i = 0; i < failed.Count; i++)
|
||||
WithdrawForPoseLoss(failed[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Preserve the post-network reconciliation boundary while recomposing
|
||||
/// only attachment branches whose published parent pose/presentation
|
||||
/// changed after the full animation-order pass.
|
||||
/// </summary>
|
||||
public void ReconcileSpatialMutations()
|
||||
{
|
||||
RetryPendingProjectionTransitions();
|
||||
_activePoseCompositionVisits = 0;
|
||||
IReadOnlyList<uint> failed = _updateOrder.ForEachParentFirst(
|
||||
_attachedByChild,
|
||||
_parentOfAttached,
|
||||
_reconcileAttached);
|
||||
LastReconcilePoseCompositionVisits = _activePoseCompositionVisits;
|
||||
for (int i = 0; i < failed.Count; i++)
|
||||
WithdrawForPoseLoss(failed[i]);
|
||||
}
|
||||
|
||||
private void RetryPendingProjectionTransitions()
|
||||
{
|
||||
foreach ((uint rootGuid, PendingOrdinaryRemoval pending) in
|
||||
_pendingOrdinaryRemovalByRoot.ToArray())
|
||||
CopyEntries(
|
||||
_pendingOrdinaryRemovalByRoot,
|
||||
_pendingOrdinaryRemovalScratch);
|
||||
for (int i = 0; i < _pendingOrdinaryRemovalScratch.Count; i++)
|
||||
{
|
||||
(uint rootGuid, PendingOrdinaryRemoval pending) =
|
||||
_pendingOrdinaryRemovalScratch[i];
|
||||
AdvanceOrdinaryRemoval(rootGuid, pending);
|
||||
}
|
||||
|
||||
foreach ((uint childGuid, PendingProjectionSubtree pending) in
|
||||
_pendingDetachedRemovalByChild.ToArray())
|
||||
CopyEntries(
|
||||
_pendingDetachedRemovalByChild,
|
||||
_pendingProjectionSubtreeScratch);
|
||||
for (int i = 0; i < _pendingProjectionSubtreeScratch.Count; i++)
|
||||
{
|
||||
(uint childGuid, PendingProjectionSubtree pending) =
|
||||
_pendingProjectionSubtreeScratch[i];
|
||||
AdvanceDetachedRemoval(childGuid, pending);
|
||||
}
|
||||
|
||||
|
|
@ -291,9 +331,11 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
RetryProjectionSubtrees(_pendingPoseLossRemovalByChild);
|
||||
RetryProjectionSubtrees(_pendingOrphanRemovalByChild);
|
||||
|
||||
foreach ((uint childGuid, PendingUnparentTransition pending) in
|
||||
_pendingUnparentByChild.ToArray())
|
||||
CopyEntries(_pendingUnparentByChild, _pendingUnparentScratch);
|
||||
for (int i = 0; i < _pendingUnparentScratch.Count; i++)
|
||||
{
|
||||
(uint childGuid, PendingUnparentTransition pending) =
|
||||
_pendingUnparentScratch[i];
|
||||
if (!_liveEntities.IsCurrentRecord(pending.Record)
|
||||
|| pending.Record.PositionAuthorityVersion
|
||||
!= pending.PositionAuthorityVersion)
|
||||
|
|
@ -309,11 +351,21 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
ResolveAndTryRealize(_pendingProjectionChildrenScratch[i]);
|
||||
}
|
||||
|
||||
private static void CopyEntries<T>(
|
||||
Dictionary<uint, T> source,
|
||||
List<KeyValuePair<uint, T>> destination)
|
||||
{
|
||||
destination.Clear();
|
||||
foreach (KeyValuePair<uint, T> entry in source)
|
||||
destination.Add(entry);
|
||||
}
|
||||
|
||||
private bool TickChild(uint childGuid)
|
||||
{
|
||||
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child))
|
||||
return false;
|
||||
|
||||
_activePoseCompositionVisits++;
|
||||
if (TryResolveExactAttachment(child, out WorldEntity parent)
|
||||
&& _poses.TryGetRootPose(parent.Id, out Matrix4x4 parentWorld)
|
||||
&& _poses.TryGetPartPoseSnapshot(
|
||||
|
|
@ -339,6 +391,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
return false;
|
||||
ApplyParentDrawVisibility(child.Entity, parent);
|
||||
child.Entity.ParentCellId = parent.ParentCellId;
|
||||
CaptureParentPresentation(child, parent);
|
||||
PublishChildPose(child.Entity, parentWorld, parent.ParentCellId, pose);
|
||||
if (TryResolveExactAttachment(child, out parent)
|
||||
&& parent.ParentCellId is { } parentCellId)
|
||||
|
|
@ -349,6 +402,39 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool ReconcileChild(uint childGuid)
|
||||
{
|
||||
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child)
|
||||
|| !TryResolveExactAttachment(child, out WorldEntity parent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ParentPresentationMatches(child, parent) || TickChild(childGuid);
|
||||
}
|
||||
|
||||
private bool ParentPresentationMatches(
|
||||
AttachedChild child,
|
||||
WorldEntity parent)
|
||||
=> ReferenceEquals(child.LastParentEntity, parent)
|
||||
&& child.LastParentPoseVersion
|
||||
== _poses.GetPoseChangeVersion(parent.Id)
|
||||
&& child.LastParentDrawVisible == parent.IsDrawVisible
|
||||
&& child.LastParentAncestorDrawVisible
|
||||
== parent.IsAncestorDrawVisible
|
||||
&& child.LastParentCellId == parent.ParentCellId;
|
||||
|
||||
private void CaptureParentPresentation(
|
||||
AttachedChild child,
|
||||
WorldEntity parent)
|
||||
{
|
||||
child.LastParentEntity = parent;
|
||||
child.LastParentPoseVersion = _poses.GetPoseChangeVersion(parent.Id);
|
||||
child.LastParentDrawVisible = parent.IsDrawVisible;
|
||||
child.LastParentAncestorDrawVisible = parent.IsAncestorDrawVisible;
|
||||
child.LastParentCellId = parent.ParentCellId;
|
||||
}
|
||||
|
||||
private bool TryRealize(
|
||||
ParentAttachmentRelation pending,
|
||||
ParentProjectionCandidateKind candidateKind)
|
||||
|
|
@ -473,7 +559,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
? changes.Select(change => new PartOverride(change.PartIndex, change.NewModelId)).ToArray()
|
||||
: Array.Empty<PartOverride>());
|
||||
entity.SetIndexedPartPoses(pose.PartLocal, childPartAvailability);
|
||||
_attachedByChild[childGuid] = new AttachedChild(
|
||||
var attached = new AttachedChild(
|
||||
parentRecord,
|
||||
childRecord,
|
||||
pending.ParentGuid,
|
||||
|
|
@ -488,6 +574,8 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
pose.AttachedParts,
|
||||
scale,
|
||||
entity);
|
||||
CaptureParentPresentation(attached, parentEntity);
|
||||
_attachedByChild[childGuid] = attached;
|
||||
_pendingUnparentByChild.Remove(childGuid);
|
||||
if ((parentRecord.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0)
|
||||
{
|
||||
|
|
@ -1421,9 +1509,11 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
private void RetryProjectionSubtrees(
|
||||
Dictionary<uint, PendingProjectionSubtree> pendingByRoot)
|
||||
{
|
||||
foreach ((uint rootGuid, PendingProjectionSubtree pending) in
|
||||
pendingByRoot.ToArray())
|
||||
CopyEntries(pendingByRoot, _pendingProjectionSubtreeScratch);
|
||||
for (int i = 0; i < _pendingProjectionSubtreeScratch.Count; i++)
|
||||
{
|
||||
(uint rootGuid, PendingProjectionSubtree pending) =
|
||||
_pendingProjectionSubtreeScratch[i];
|
||||
AdvanceProjectionSubtree(pendingByRoot, rootGuid, pending);
|
||||
}
|
||||
}
|
||||
|
|
@ -1476,7 +1566,14 @@ public sealed class EquippedChildRenderController : IDisposable
|
|||
Matrix4x4[] PartPoseBuffer,
|
||||
MeshRef[] AttachedPartBuffer,
|
||||
float Scale,
|
||||
WorldEntity Entity);
|
||||
WorldEntity Entity)
|
||||
{
|
||||
public WorldEntity? LastParentEntity { get; set; }
|
||||
public ulong LastParentPoseVersion { get; set; }
|
||||
public bool LastParentDrawVisible { get; set; }
|
||||
public bool LastParentAncestorDrawVisible { get; set; }
|
||||
public uint? LastParentCellId { get; set; }
|
||||
}
|
||||
|
||||
private readonly record struct PendingUnparentTransition(
|
||||
LiveEntityRecord Record,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public sealed class EntityEffectPoseRegistry :
|
|||
public bool[] PartAvailable = Array.Empty<bool>();
|
||||
public uint CellId;
|
||||
public ulong LifetimeVersion;
|
||||
public ulong ChangeVersion;
|
||||
}
|
||||
|
||||
private readonly Dictionary<uint, PoseRecord> _poses = new();
|
||||
|
|
@ -112,7 +113,10 @@ public sealed class EntityEffectPoseRegistry :
|
|||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
record.ChangeVersion++;
|
||||
EffectPoseChanged?.Invoke(entity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public void Publish(
|
||||
|
|
@ -142,7 +146,10 @@ public sealed class EntityEffectPoseRegistry :
|
|||
record.CellId = cellId;
|
||||
changed |= CopyParts(record, partLocal, availability);
|
||||
if (changed)
|
||||
{
|
||||
record.ChangeVersion++;
|
||||
EffectPoseChanged?.Invoke(localEntityId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Refresh only the moving root while retaining current part poses.</summary>
|
||||
|
|
@ -159,6 +166,7 @@ public sealed class EntityEffectPoseRegistry :
|
|||
|
||||
record.RootWorld = rootWorld;
|
||||
record.CellId = cellId;
|
||||
record.ChangeVersion++;
|
||||
EffectPoseChanged?.Invoke(entity.Id);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -258,6 +266,16 @@ public sealed class EntityEffectPoseRegistry :
|
|||
? record.LifetimeVersion
|
||||
: 0UL;
|
||||
|
||||
/// <summary>
|
||||
/// Monotonic version of the current lifetime's root, part, availability,
|
||||
/// or cell pose. Synchronous presentation owners use it to avoid repeating
|
||||
/// a derived composition when the published parent pose is unchanged.
|
||||
/// </summary>
|
||||
public ulong GetPoseChangeVersion(uint localEntityId) =>
|
||||
_poses.TryGetValue(localEntityId, out PoseRecord? record)
|
||||
? record.ChangeVersion
|
||||
: 0UL;
|
||||
|
||||
private ulong NextLifetimeVersion()
|
||||
{
|
||||
_nextLifetimeVersion++;
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcileP
|
|||
public void Reconcile()
|
||||
{
|
||||
_entityEffects.RefreshLiveOwnerPoses();
|
||||
_equippedChildren.Tick();
|
||||
_equippedChildren.ReconcileSpatialMutations();
|
||||
_renderProjections?.SynchronizeActiveSources();
|
||||
_particleSink.RefreshAttachedEmitters();
|
||||
_lights.Refresh();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue