fix(animation): harden presentation phase handoffs
Own every borrowed PartArray frame across callbacks, version presentation state across appearance rebinding, reject recursive phase consumption, and preserve static MotionDone when only a stale visual pose is discarded.
This commit is contained in:
parent
a2fd61684a
commit
833520253a
10 changed files with 657 additions and 140 deletions
|
|
@ -34,7 +34,11 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
public PhysicsBody? Body;
|
||||
public AnimationSequencer? PendingProcessHooks;
|
||||
public ulong PendingResidencyVersion;
|
||||
public IReadOnlyList<PartTransform>? PreparedLivePartFrames;
|
||||
public readonly List<PartTransform> PreparedLivePartFrames = new();
|
||||
public bool HasPreparedLivePartFrames;
|
||||
public AnimationSequencer? PreparedFrameSequencer;
|
||||
public ulong PreparedPresentationRevision;
|
||||
public LiveEntityAnimationState? LiveAnimation;
|
||||
public double ElapsedSinceUpdate;
|
||||
public readonly Frame RootFrameScratch = new();
|
||||
public readonly List<MeshRef> MeshRefs = new();
|
||||
|
|
@ -137,11 +141,11 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
/// </summary>
|
||||
public bool BindLiveOwner(
|
||||
WorldEntity entity,
|
||||
AnimationSequencer sequencer,
|
||||
LiveEntityAnimationState animation,
|
||||
PhysicsBody body)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(sequencer);
|
||||
ArgumentNullException.ThrowIfNull(animation);
|
||||
ArgumentNullException.ThrowIfNull(body);
|
||||
if (!_owners.TryGetValue(entity.Id, out Owner? owner))
|
||||
return false;
|
||||
|
|
@ -150,10 +154,12 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
throw new InvalidOperationException(
|
||||
$"Static animation owner 0x{entity.Id:X8} does not match this live incarnation.");
|
||||
}
|
||||
if (!sequencer.HasCurrentNode)
|
||||
if (animation.Sequencer is not { HasCurrentNode: true } sequencer)
|
||||
return false;
|
||||
|
||||
InvalidatePending(owner);
|
||||
owner.Sequencer = sequencer;
|
||||
owner.LiveAnimation = animation;
|
||||
owner.Body = body;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -169,33 +175,64 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
LiveEntityAnimationState animation,
|
||||
ulong objectClockEpoch,
|
||||
ulong projectionMutationVersion,
|
||||
ulong presentationRevision,
|
||||
out IReadOnlyList<PartTransform> frames)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(animation);
|
||||
uint ownerId = entity.Id;
|
||||
if (_owners.TryGetValue(ownerId, out Owner? owner)
|
||||
&& ReferenceEquals(record.WorldEntity, entity)
|
||||
if (!_owners.TryGetValue(ownerId, out Owner? owner))
|
||||
{
|
||||
frames = Array.Empty<PartTransform>();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool exactOwner = ReferenceEquals(record.WorldEntity, entity)
|
||||
&& ReferenceEquals(record.AnimationRuntime, animation)
|
||||
&& record.ObjectClockEpoch == objectClockEpoch
|
||||
&& record.ProjectionMutationVersion == projectionMutationVersion
|
||||
&& animation.PresentationRevision == presentationRevision
|
||||
&& ReferenceEquals(owner.Entity, entity)
|
||||
&& ReferenceEquals(owner.LiveAnimation, animation)
|
||||
&& ReferenceEquals(owner.Sequencer, animation.Sequencer)
|
||||
&& owner.Entity.ServerGuid != 0
|
||||
&& owner.PreparedLivePartFrames is { } prepared
|
||||
&& IsResidentAtVersion(owner, owner.PendingResidencyVersion))
|
||||
&& owner.Entity.ServerGuid != 0;
|
||||
if (!exactOwner)
|
||||
{
|
||||
owner.PreparedLivePartFrames = null;
|
||||
frames = prepared;
|
||||
return true;
|
||||
InvalidatePending(owner);
|
||||
frames = Array.Empty<PartTransform>();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_owners.TryGetValue(ownerId, out owner))
|
||||
InvalidatePending(owner);
|
||||
// An already-consumed frame is a normal same-phase recursive query;
|
||||
// process_hooks still belongs to the outer presentation tail.
|
||||
if (!owner.HasPreparedLivePartFrames)
|
||||
{
|
||||
frames = Array.Empty<PartTransform>();
|
||||
return false;
|
||||
}
|
||||
|
||||
frames = Array.Empty<PartTransform>();
|
||||
return false;
|
||||
if (!ReferenceEquals(owner.PreparedFrameSequencer, animation.Sequencer)
|
||||
|| !IsResidentAtVersion(owner, owner.PendingResidencyVersion))
|
||||
{
|
||||
InvalidatePending(owner);
|
||||
frames = Array.Empty<PartTransform>();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (owner.PreparedPresentationRevision != presentationRevision)
|
||||
{
|
||||
// Appearance rebinding invalidates only the prepared visual pose.
|
||||
// The exact live owner and sequencer still own process_hooks from
|
||||
// this quantum, including MotionDone, so retain that semantic tail.
|
||||
InvalidatePreparedFrame(owner);
|
||||
frames = Array.Empty<PartTransform>();
|
||||
return false;
|
||||
}
|
||||
|
||||
owner.HasPreparedLivePartFrames = false;
|
||||
frames = owner.PreparedLivePartFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -207,18 +244,25 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
uint ownerId,
|
||||
out IReadOnlyList<PartTransform> frames)
|
||||
{
|
||||
if (_owners.TryGetValue(ownerId, out Owner? owner)
|
||||
&& owner.PreparedLivePartFrames is { } prepared
|
||||
&& IsResidentAtVersion(owner, owner.PendingResidencyVersion))
|
||||
if (!_owners.TryGetValue(ownerId, out Owner? owner))
|
||||
{
|
||||
owner.PreparedLivePartFrames = null;
|
||||
frames = prepared;
|
||||
return true;
|
||||
frames = Array.Empty<PartTransform>();
|
||||
return false;
|
||||
}
|
||||
if (_owners.TryGetValue(ownerId, out owner))
|
||||
if (!owner.HasPreparedLivePartFrames)
|
||||
{
|
||||
frames = Array.Empty<PartTransform>();
|
||||
return false;
|
||||
}
|
||||
if (!IsResidentAtVersion(owner, owner.PendingResidencyVersion))
|
||||
{
|
||||
InvalidatePending(owner);
|
||||
frames = Array.Empty<PartTransform>();
|
||||
return false;
|
||||
frames = Array.Empty<PartTransform>();
|
||||
return false;
|
||||
}
|
||||
owner.HasPreparedLivePartFrames = false;
|
||||
frames = owner.PreparedLivePartFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Unregister(uint ownerId) => _owners.Remove(ownerId);
|
||||
|
|
@ -273,8 +317,10 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
continue;
|
||||
}
|
||||
|
||||
IReadOnlyList<PartTransform> frames = sequencer.Advance(
|
||||
(float)ownerElapsed);
|
||||
IReadOnlyList<PartTransform> frames = sequencer.Advance((float)ownerElapsed);
|
||||
owner.PreparedLivePartFrames.Clear();
|
||||
for (int i = 0; i < frames.Count; i++)
|
||||
owner.PreparedLivePartFrames.Add(frames[i]);
|
||||
|
||||
if (owner.Body is { } body)
|
||||
{
|
||||
|
|
@ -319,11 +365,14 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
|
||||
if (owner.Entity.ServerGuid != 0)
|
||||
{
|
||||
owner.PreparedLivePartFrames = frames;
|
||||
owner.HasPreparedLivePartFrames = true;
|
||||
owner.PreparedFrameSequencer = sequencer;
|
||||
owner.PreparedPresentationRevision =
|
||||
owner.LiveAnimation?.PresentationRevision ?? 0UL;
|
||||
}
|
||||
else
|
||||
{
|
||||
Compose(owner, frames);
|
||||
Compose(owner, owner.PreparedLivePartFrames);
|
||||
_publishPartPoses(owner.Entity, owner.PartPoses, owner.PartAvailable);
|
||||
if (!_owners.TryGetValue(owner.Entity.Id, out current)
|
||||
|| !ReferenceEquals(current, owner)
|
||||
|
|
@ -383,11 +432,19 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
|||
|
||||
private static void InvalidatePending(Owner owner)
|
||||
{
|
||||
owner.PreparedLivePartFrames = null;
|
||||
InvalidatePreparedFrame(owner);
|
||||
owner.PendingProcessHooks = null;
|
||||
owner.PendingResidencyVersion = 0UL;
|
||||
}
|
||||
|
||||
private static void InvalidatePreparedFrame(Owner owner)
|
||||
{
|
||||
owner.PreparedLivePartFrames.Clear();
|
||||
owner.HasPreparedLivePartFrames = false;
|
||||
owner.PreparedFrameSequencer = null;
|
||||
owner.PreparedPresentationRevision = 0UL;
|
||||
}
|
||||
|
||||
private static void Compose(Owner owner, IReadOnlyList<PartTransform> frames)
|
||||
{
|
||||
EnsureRetainedPoses(owner);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue