perf(rendering): reconcile only changed attachments
This commit is contained in:
parent
6b56f4bef2
commit
b9cbf5e040
8 changed files with 246 additions and 14 deletions
|
|
@ -328,7 +328,7 @@ graph corrupts collision state, which is worse than the allocation.
|
||||||
|
|
||||||
## #238 — EquippedChildRenderController ticks twice per frame with six ToArray snapshots
|
## #238 — EquippedChildRenderController ticks twice per frame with six ToArray snapshots
|
||||||
|
|
||||||
**Status:** OPEN
|
**Status:** DONE — 2026-07-25, Modern Runtime Slice H-a3
|
||||||
**Severity:** MEDIUM
|
**Severity:** MEDIUM
|
||||||
**Filed:** 2026-07-24
|
**Filed:** 2026-07-24
|
||||||
**Component:** render / live entities
|
**Component:** render / live entities
|
||||||
|
|
@ -350,6 +350,14 @@ the ordering and skip the redundant recomposition instead. Plan Slice H-a.
|
||||||
**Files:** `src/AcDream.App/Rendering/EquippedChildRenderController.cs:265-310,1421-1429`;
|
**Files:** `src/AcDream.App/Rendering/EquippedChildRenderController.cs:265-310,1421-1429`;
|
||||||
`src/AcDream.App/Update/LiveObjectFrameController.cs:204,241`.
|
`src/AcDream.App/Update/LiveObjectFrameController.cs:204,241`.
|
||||||
|
|
||||||
|
**Resolution:** Both required authority boundaries remain. The first is still a
|
||||||
|
complete parent-first pose pass; the post-network boundary now compares the
|
||||||
|
exact parent's pose version, identity, visibility, and cell and recomposes only
|
||||||
|
changed branches. Publishing a changed child dirties its descendants naturally
|
||||||
|
within the same parent-first walk. Per-heartbeat pending dictionaries use
|
||||||
|
retained typed snapshots instead of arrays. Evidence:
|
||||||
|
`docs/research/2026-07-25-slice-h-a3-attached-pose-reconcile.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## #239 — Inbound net path allocates 3-4 arrays/objects per packet
|
## #239 — Inbound net path allocates 3-4 arrays/objects per packet
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ cooldown step.
|
||||||
Landed evidence:
|
Landed evidence:
|
||||||
[`../research/2026-07-25-slice-h-a2-cooldown-scope.md`](../research/2026-07-25-slice-h-a2-cooldown-scope.md).
|
[`../research/2026-07-25-slice-h-a2-cooldown-scope.md`](../research/2026-07-25-slice-h-a2-cooldown-scope.md).
|
||||||
|
|
||||||
### H-a3 — equipped-child transition work
|
### H-a3 — equipped-child transition work — COMPLETE
|
||||||
|
|
||||||
1. Preserve both frame ordering points: pre-network presentation and
|
1. Preserve both frame ordering points: pre-network presentation and
|
||||||
post-network spatial reconciliation have different authority boundaries.
|
post-network spatial reconciliation have different authority boundaries.
|
||||||
|
|
@ -77,6 +77,9 @@ Landed evidence:
|
||||||
Gate: equip, unparent, reparent, pose loss, rollback, delete, GUID reuse, and
|
Gate: equip, unparent, reparent, pose loss, rollback, delete, GUID reuse, and
|
||||||
subtree-withdrawal suites preserve exact event and resource counts.
|
subtree-withdrawal suites preserve exact event and resource counts.
|
||||||
|
|
||||||
|
Landed evidence:
|
||||||
|
[`../research/2026-07-25-slice-h-a3-attached-pose-reconcile.md`](../research/2026-07-25-slice-h-a3-attached-pose-reconcile.md).
|
||||||
|
|
||||||
### H-a4 — diagnostics and frame scratch
|
### H-a4 — diagnostics and frame scratch
|
||||||
|
|
||||||
1. Reuse one synchronous `RetailPViewFrameInput` owned by
|
1. Reuse one synchronous `RetailPViewFrameInput` owned by
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Slice H-a3 — equipped-child post-network reconciliation
|
||||||
|
|
||||||
|
## Preserved ordering
|
||||||
|
|
||||||
|
Retail live-object presentation still has two authority boundaries:
|
||||||
|
|
||||||
|
1. the complete parent-first attachment update after animation;
|
||||||
|
2. post-network spatial reconciliation after accepted inbound mutations.
|
||||||
|
|
||||||
|
The second boundary was not deleted or delayed.
|
||||||
|
|
||||||
|
## Change
|
||||||
|
|
||||||
|
`EntityEffectPoseRegistry` now carries a monotonic change version for the
|
||||||
|
current pose-owner lifetime. It advances only when root, indexed parts, part
|
||||||
|
availability, or cell changes.
|
||||||
|
|
||||||
|
Each attached projection remembers the exact parent entity, pose version,
|
||||||
|
draw/ancestor visibility, and parent cell used for its last composition.
|
||||||
|
|
||||||
|
- The animation-order pass remains a complete parent-first walk.
|
||||||
|
- The post-network pass keeps transition retries but skips a child whose
|
||||||
|
remembered parent presentation is unchanged.
|
||||||
|
- A changed root recomposes its direct child; publishing that child's pose
|
||||||
|
advances its version, so descendants update later in the same parent-first
|
||||||
|
pass.
|
||||||
|
- Hidden/NoDraw ancestry and cell changes participate even when the geometric
|
||||||
|
pose itself is unchanged.
|
||||||
|
- Pose failure still withdraws the same complete subtree.
|
||||||
|
|
||||||
|
The per-frame transition retry path no longer allocates dictionary `ToArray`
|
||||||
|
snapshots. It copies key/value pairs into retained typed scratch lists, keeping
|
||||||
|
the old snapshot-before-callback semantics.
|
||||||
|
|
||||||
|
## Gate
|
||||||
|
|
||||||
|
- Existing equip/unparent/reparent/rollback/pose-loss/teardown tests pass.
|
||||||
|
- A root → child → grandchild test proves unchanged post-network reconcile
|
||||||
|
performs zero compositions and a changed root performs exactly two in
|
||||||
|
parent-first order.
|
||||||
|
- 1,000 stable post-network reconciles allocate zero managed bytes on the
|
||||||
|
calling thread.
|
||||||
|
|
||||||
|
Release gate: 3,783 App tests passed / 3 skipped; 8,267 complete-solution
|
||||||
|
tests passed / 5 skipped.
|
||||||
|
|
@ -49,9 +49,20 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingPoseLossRemovalByChild = new();
|
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingPoseLossRemovalByChild = new();
|
||||||
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingOrphanRemovalByChild = new();
|
private readonly Dictionary<uint, PendingProjectionSubtree> _pendingOrphanRemovalByChild = new();
|
||||||
private readonly List<uint> _pendingProjectionChildrenScratch = 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 AttachmentUpdateOrder<AttachedChild> _updateOrder = new();
|
||||||
private readonly Func<AttachedChild, uint?> _parentOfAttached;
|
private readonly Func<AttachedChild, uint?> _parentOfAttached;
|
||||||
private readonly Func<uint, bool> _tickAttached;
|
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
|
public IEnumerable<uint> AttachedEntityIds
|
||||||
{
|
{
|
||||||
|
|
@ -82,6 +93,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
?? throw new ArgumentNullException(nameof(withdrawProjection));
|
?? throw new ArgumentNullException(nameof(withdrawProjection));
|
||||||
_parentOfAttached = static child => child.ParentGuid;
|
_parentOfAttached = static child => child.ParentGuid;
|
||||||
_tickAttached = TickChild;
|
_tickAttached = TickChild;
|
||||||
|
_reconcileAttached = ReconcileChild;
|
||||||
|
|
||||||
_objects.ObjectMoved += OnObjectMoved;
|
_objects.ObjectMoved += OnObjectMoved;
|
||||||
_objects.MoveRolledBack += OnMoveRolledBack;
|
_objects.MoveRolledBack += OnMoveRolledBack;
|
||||||
|
|
@ -265,25 +277,53 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
public void Tick()
|
public void Tick()
|
||||||
{
|
{
|
||||||
RetryPendingProjectionTransitions();
|
RetryPendingProjectionTransitions();
|
||||||
|
_activePoseCompositionVisits = 0;
|
||||||
IReadOnlyList<uint> failed = _updateOrder.ForEachParentFirst(
|
IReadOnlyList<uint> failed = _updateOrder.ForEachParentFirst(
|
||||||
_attachedByChild,
|
_attachedByChild,
|
||||||
_parentOfAttached,
|
_parentOfAttached,
|
||||||
_tickAttached);
|
_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++)
|
for (int i = 0; i < failed.Count; i++)
|
||||||
WithdrawForPoseLoss(failed[i]);
|
WithdrawForPoseLoss(failed[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RetryPendingProjectionTransitions()
|
private void RetryPendingProjectionTransitions()
|
||||||
{
|
{
|
||||||
foreach ((uint rootGuid, PendingOrdinaryRemoval pending) in
|
CopyEntries(
|
||||||
_pendingOrdinaryRemovalByRoot.ToArray())
|
_pendingOrdinaryRemovalByRoot,
|
||||||
|
_pendingOrdinaryRemovalScratch);
|
||||||
|
for (int i = 0; i < _pendingOrdinaryRemovalScratch.Count; i++)
|
||||||
{
|
{
|
||||||
|
(uint rootGuid, PendingOrdinaryRemoval pending) =
|
||||||
|
_pendingOrdinaryRemovalScratch[i];
|
||||||
AdvanceOrdinaryRemoval(rootGuid, pending);
|
AdvanceOrdinaryRemoval(rootGuid, pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ((uint childGuid, PendingProjectionSubtree pending) in
|
CopyEntries(
|
||||||
_pendingDetachedRemovalByChild.ToArray())
|
_pendingDetachedRemovalByChild,
|
||||||
|
_pendingProjectionSubtreeScratch);
|
||||||
|
for (int i = 0; i < _pendingProjectionSubtreeScratch.Count; i++)
|
||||||
{
|
{
|
||||||
|
(uint childGuid, PendingProjectionSubtree pending) =
|
||||||
|
_pendingProjectionSubtreeScratch[i];
|
||||||
AdvanceDetachedRemoval(childGuid, pending);
|
AdvanceDetachedRemoval(childGuid, pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,9 +331,11 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
RetryProjectionSubtrees(_pendingPoseLossRemovalByChild);
|
RetryProjectionSubtrees(_pendingPoseLossRemovalByChild);
|
||||||
RetryProjectionSubtrees(_pendingOrphanRemovalByChild);
|
RetryProjectionSubtrees(_pendingOrphanRemovalByChild);
|
||||||
|
|
||||||
foreach ((uint childGuid, PendingUnparentTransition pending) in
|
CopyEntries(_pendingUnparentByChild, _pendingUnparentScratch);
|
||||||
_pendingUnparentByChild.ToArray())
|
for (int i = 0; i < _pendingUnparentScratch.Count; i++)
|
||||||
{
|
{
|
||||||
|
(uint childGuid, PendingUnparentTransition pending) =
|
||||||
|
_pendingUnparentScratch[i];
|
||||||
if (!_liveEntities.IsCurrentRecord(pending.Record)
|
if (!_liveEntities.IsCurrentRecord(pending.Record)
|
||||||
|| pending.Record.PositionAuthorityVersion
|
|| pending.Record.PositionAuthorityVersion
|
||||||
!= pending.PositionAuthorityVersion)
|
!= pending.PositionAuthorityVersion)
|
||||||
|
|
@ -309,11 +351,21 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
ResolveAndTryRealize(_pendingProjectionChildrenScratch[i]);
|
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)
|
private bool TickChild(uint childGuid)
|
||||||
{
|
{
|
||||||
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child))
|
if (!_attachedByChild.TryGetValue(childGuid, out AttachedChild? child))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
_activePoseCompositionVisits++;
|
||||||
if (TryResolveExactAttachment(child, out WorldEntity parent)
|
if (TryResolveExactAttachment(child, out WorldEntity parent)
|
||||||
&& _poses.TryGetRootPose(parent.Id, out Matrix4x4 parentWorld)
|
&& _poses.TryGetRootPose(parent.Id, out Matrix4x4 parentWorld)
|
||||||
&& _poses.TryGetPartPoseSnapshot(
|
&& _poses.TryGetPartPoseSnapshot(
|
||||||
|
|
@ -339,6 +391,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
return false;
|
return false;
|
||||||
ApplyParentDrawVisibility(child.Entity, parent);
|
ApplyParentDrawVisibility(child.Entity, parent);
|
||||||
child.Entity.ParentCellId = parent.ParentCellId;
|
child.Entity.ParentCellId = parent.ParentCellId;
|
||||||
|
CaptureParentPresentation(child, parent);
|
||||||
PublishChildPose(child.Entity, parentWorld, parent.ParentCellId, pose);
|
PublishChildPose(child.Entity, parentWorld, parent.ParentCellId, pose);
|
||||||
if (TryResolveExactAttachment(child, out parent)
|
if (TryResolveExactAttachment(child, out parent)
|
||||||
&& parent.ParentCellId is { } parentCellId)
|
&& parent.ParentCellId is { } parentCellId)
|
||||||
|
|
@ -349,6 +402,39 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
return false;
|
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(
|
private bool TryRealize(
|
||||||
ParentAttachmentRelation pending,
|
ParentAttachmentRelation pending,
|
||||||
ParentProjectionCandidateKind candidateKind)
|
ParentProjectionCandidateKind candidateKind)
|
||||||
|
|
@ -473,7 +559,7 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
? changes.Select(change => new PartOverride(change.PartIndex, change.NewModelId)).ToArray()
|
? changes.Select(change => new PartOverride(change.PartIndex, change.NewModelId)).ToArray()
|
||||||
: Array.Empty<PartOverride>());
|
: Array.Empty<PartOverride>());
|
||||||
entity.SetIndexedPartPoses(pose.PartLocal, childPartAvailability);
|
entity.SetIndexedPartPoses(pose.PartLocal, childPartAvailability);
|
||||||
_attachedByChild[childGuid] = new AttachedChild(
|
var attached = new AttachedChild(
|
||||||
parentRecord,
|
parentRecord,
|
||||||
childRecord,
|
childRecord,
|
||||||
pending.ParentGuid,
|
pending.ParentGuid,
|
||||||
|
|
@ -488,6 +574,8 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
pose.AttachedParts,
|
pose.AttachedParts,
|
||||||
scale,
|
scale,
|
||||||
entity);
|
entity);
|
||||||
|
CaptureParentPresentation(attached, parentEntity);
|
||||||
|
_attachedByChild[childGuid] = attached;
|
||||||
_pendingUnparentByChild.Remove(childGuid);
|
_pendingUnparentByChild.Remove(childGuid);
|
||||||
if ((parentRecord.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0)
|
if ((parentRecord.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0)
|
||||||
{
|
{
|
||||||
|
|
@ -1421,9 +1509,11 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
private void RetryProjectionSubtrees(
|
private void RetryProjectionSubtrees(
|
||||||
Dictionary<uint, PendingProjectionSubtree> pendingByRoot)
|
Dictionary<uint, PendingProjectionSubtree> pendingByRoot)
|
||||||
{
|
{
|
||||||
foreach ((uint rootGuid, PendingProjectionSubtree pending) in
|
CopyEntries(pendingByRoot, _pendingProjectionSubtreeScratch);
|
||||||
pendingByRoot.ToArray())
|
for (int i = 0; i < _pendingProjectionSubtreeScratch.Count; i++)
|
||||||
{
|
{
|
||||||
|
(uint rootGuid, PendingProjectionSubtree pending) =
|
||||||
|
_pendingProjectionSubtreeScratch[i];
|
||||||
AdvanceProjectionSubtree(pendingByRoot, rootGuid, pending);
|
AdvanceProjectionSubtree(pendingByRoot, rootGuid, pending);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1476,7 +1566,14 @@ public sealed class EquippedChildRenderController : IDisposable
|
||||||
Matrix4x4[] PartPoseBuffer,
|
Matrix4x4[] PartPoseBuffer,
|
||||||
MeshRef[] AttachedPartBuffer,
|
MeshRef[] AttachedPartBuffer,
|
||||||
float Scale,
|
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(
|
private readonly record struct PendingUnparentTransition(
|
||||||
LiveEntityRecord Record,
|
LiveEntityRecord Record,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ public sealed class EntityEffectPoseRegistry :
|
||||||
public bool[] PartAvailable = Array.Empty<bool>();
|
public bool[] PartAvailable = Array.Empty<bool>();
|
||||||
public uint CellId;
|
public uint CellId;
|
||||||
public ulong LifetimeVersion;
|
public ulong LifetimeVersion;
|
||||||
|
public ulong ChangeVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Dictionary<uint, PoseRecord> _poses = new();
|
private readonly Dictionary<uint, PoseRecord> _poses = new();
|
||||||
|
|
@ -112,7 +113,10 @@ public sealed class EntityEffectPoseRegistry :
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changed)
|
if (changed)
|
||||||
|
{
|
||||||
|
record.ChangeVersion++;
|
||||||
EffectPoseChanged?.Invoke(entity.Id);
|
EffectPoseChanged?.Invoke(entity.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Publish(
|
public void Publish(
|
||||||
|
|
@ -142,7 +146,10 @@ public sealed class EntityEffectPoseRegistry :
|
||||||
record.CellId = cellId;
|
record.CellId = cellId;
|
||||||
changed |= CopyParts(record, partLocal, availability);
|
changed |= CopyParts(record, partLocal, availability);
|
||||||
if (changed)
|
if (changed)
|
||||||
|
{
|
||||||
|
record.ChangeVersion++;
|
||||||
EffectPoseChanged?.Invoke(localEntityId);
|
EffectPoseChanged?.Invoke(localEntityId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Refresh only the moving root while retaining current part poses.</summary>
|
/// <summary>Refresh only the moving root while retaining current part poses.</summary>
|
||||||
|
|
@ -159,6 +166,7 @@ public sealed class EntityEffectPoseRegistry :
|
||||||
|
|
||||||
record.RootWorld = rootWorld;
|
record.RootWorld = rootWorld;
|
||||||
record.CellId = cellId;
|
record.CellId = cellId;
|
||||||
|
record.ChangeVersion++;
|
||||||
EffectPoseChanged?.Invoke(entity.Id);
|
EffectPoseChanged?.Invoke(entity.Id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -258,6 +266,16 @@ public sealed class EntityEffectPoseRegistry :
|
||||||
? record.LifetimeVersion
|
? record.LifetimeVersion
|
||||||
: 0UL;
|
: 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()
|
private ulong NextLifetimeVersion()
|
||||||
{
|
{
|
||||||
_nextLifetimeVersion++;
|
_nextLifetimeVersion++;
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcileP
|
||||||
public void Reconcile()
|
public void Reconcile()
|
||||||
{
|
{
|
||||||
_entityEffects.RefreshLiveOwnerPoses();
|
_entityEffects.RefreshLiveOwnerPoses();
|
||||||
_equippedChildren.Tick();
|
_equippedChildren.ReconcileSpatialMutations();
|
||||||
_renderProjections?.SynchronizeActiveSources();
|
_renderProjections?.SynchronizeActiveSources();
|
||||||
_particleSink.RefreshAttachedEmitters();
|
_particleSink.RefreshAttachedEmitters();
|
||||||
_lights.Refresh();
|
_lights.Refresh();
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,67 @@ public sealed class EquippedChildProjectionWithdrawalTests
|
||||||
Assert.True(record.IsSpatiallyProjected);
|
Assert.True(record.IsSpatiallyProjected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PostNetworkReconcile_SkipsStableTree_AndUpdatesChangedBranchParentFirst()
|
||||||
|
{
|
||||||
|
using var fixture = new ControllerFixture((_, _, _) =>
|
||||||
|
new(
|
||||||
|
ExactProjectionWithdrawalDisposition.Completed,
|
||||||
|
Failure: null));
|
||||||
|
LiveEntityRecord root = fixture.Spawn(0x70000241u, generation: 1);
|
||||||
|
LiveEntityRecord child = fixture.Spawn(
|
||||||
|
0x70000242u,
|
||||||
|
generation: 1,
|
||||||
|
LiveEntityProjectionKind.Attached);
|
||||||
|
LiveEntityRecord grandchild = fixture.Spawn(
|
||||||
|
0x70000243u,
|
||||||
|
generation: 1,
|
||||||
|
LiveEntityProjectionKind.Attached);
|
||||||
|
fixture.InstallAttached(root, child);
|
||||||
|
fixture.InstallAttached(child, grandchild);
|
||||||
|
fixture.Poses.Publish(root.WorldEntity!, Array.Empty<Matrix4x4>());
|
||||||
|
|
||||||
|
fixture.Controller.Tick();
|
||||||
|
Assert.Equal(2, fixture.Controller.LastFullPoseCompositionVisits);
|
||||||
|
|
||||||
|
fixture.Controller.ReconcileSpatialMutations();
|
||||||
|
Assert.Equal(0, fixture.Controller.LastReconcilePoseCompositionVisits);
|
||||||
|
|
||||||
|
root.WorldEntity!.SetPosition(new Vector3(7f, 8f, 9f));
|
||||||
|
Assert.True(fixture.Poses.UpdateRoot(root.WorldEntity));
|
||||||
|
fixture.Controller.ReconcileSpatialMutations();
|
||||||
|
|
||||||
|
Assert.Equal(2, fixture.Controller.LastReconcilePoseCompositionVisits);
|
||||||
|
Assert.Equal(root.WorldEntity.Position, child.WorldEntity!.Position);
|
||||||
|
Assert.Equal(child.WorldEntity.Position, grandchild.WorldEntity!.Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StablePostNetworkReconcile_AllocatesNoTransitionSnapshots()
|
||||||
|
{
|
||||||
|
using var fixture = new ControllerFixture((_, _, _) =>
|
||||||
|
new(
|
||||||
|
ExactProjectionWithdrawalDisposition.Completed,
|
||||||
|
Failure: null));
|
||||||
|
LiveEntityRecord root = fixture.Spawn(0x70000244u, generation: 1);
|
||||||
|
LiveEntityRecord child = fixture.Spawn(
|
||||||
|
0x70000245u,
|
||||||
|
generation: 1,
|
||||||
|
LiveEntityProjectionKind.Attached);
|
||||||
|
fixture.InstallAttached(root, child);
|
||||||
|
fixture.Poses.Publish(root.WorldEntity!, Array.Empty<Matrix4x4>());
|
||||||
|
fixture.Controller.Tick();
|
||||||
|
fixture.Controller.ReconcileSpatialMutations();
|
||||||
|
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (int i = 0; i < 1_000; i++)
|
||||||
|
fixture.Controller.ReconcileSpatialMutations();
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
|
||||||
|
Assert.Equal(0L, allocated);
|
||||||
|
Assert.Equal(0, fixture.Controller.LastReconcilePoseCompositionVisits);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AcceptedValidParent_WithdrawsWorldProjectionBeforePosePrerequisitesExist()
|
public void AcceptedValidParent_WithdrawsWorldProjectionBeforePosePrerequisitesExist()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -356,7 +356,7 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
source,
|
source,
|
||||||
"public void Reconcile()",
|
"public void Reconcile()",
|
||||||
"_entityEffects.RefreshLiveOwnerPoses",
|
"_entityEffects.RefreshLiveOwnerPoses",
|
||||||
"_equippedChildren.Tick",
|
"_equippedChildren.ReconcileSpatialMutations",
|
||||||
"_particleSink.RefreshAttachedEmitters",
|
"_particleSink.RefreshAttachedEmitters",
|
||||||
"_lights.Refresh");
|
"_lights.Refresh");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue