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
|
|
@ -19,6 +19,7 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
private readonly AnimationPresentationDiagnostics _diagnostics;
|
||||
private readonly int _hidePartIndex;
|
||||
private readonly List<LiveEntityRecord> _snapshot = new();
|
||||
private bool _isPresenting;
|
||||
|
||||
public LiveEntityAnimationPresenter(
|
||||
Func<LiveEntityRuntime?> liveEntities,
|
||||
|
|
@ -79,94 +80,107 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(schedules);
|
||||
LiveEntityRuntime? runtime = _liveEntities();
|
||||
if (runtime is null)
|
||||
if (runtime is null || _isPresenting)
|
||||
return;
|
||||
|
||||
// Private snapshot: EffectPoseChanged is synchronous and may perform a
|
||||
// nested runtime-view enumeration. Never borrow that view's scratch.
|
||||
runtime.CopySpatialRootObjectRecordsTo(_snapshot);
|
||||
foreach (LiveEntityRecord record in _snapshot)
|
||||
// EffectPoseChanged is synchronous. Presentation is one consuming
|
||||
// object-frame phase; recursive entry cannot consume legacy elapsed or
|
||||
// a static prepared frame a second time.
|
||||
_isPresenting = true;
|
||||
try
|
||||
{
|
||||
if (!TryGetCurrent(runtime, record, out WorldEntity entity, out LiveEntityAnimationState animation))
|
||||
continue;
|
||||
|
||||
PrepareAnimation(record, animation);
|
||||
if (!TryGetCurrent(runtime, record, entity, animation))
|
||||
continue;
|
||||
|
||||
schedules.TryGetValue(entity.Id, out LiveEntityAnimationSchedule schedule);
|
||||
bool hasOrdinarySchedule = IsCurrentSchedule(runtime, schedule, record, entity, animation);
|
||||
IReadOnlyList<PartTransform>? sequenceFrames = hasOrdinarySchedule
|
||||
? schedule.SequenceFrames
|
||||
: null;
|
||||
bool composeParts = hasOrdinarySchedule && schedule.ComposeParts;
|
||||
float legacyAdvanceSeconds = hasOrdinarySchedule
|
||||
? schedule.LegacyAdvanceSeconds
|
||||
: 0f;
|
||||
ulong objectClockEpoch = record.ObjectClockEpoch;
|
||||
ulong projectionVersion = record.ProjectionMutationVersion;
|
||||
|
||||
if (_staticFrames()?.TryTakeLivePartFrames(
|
||||
record,
|
||||
entity,
|
||||
animation,
|
||||
objectClockEpoch,
|
||||
projectionVersion,
|
||||
out IReadOnlyList<PartTransform> staticPartFrames) == true)
|
||||
runtime.CopySpatialRootObjectRecordsTo(_snapshot);
|
||||
foreach (LiveEntityRecord record in _snapshot)
|
||||
{
|
||||
if (!TryGetCurrent(
|
||||
if (!TryGetCurrent(runtime, record, out WorldEntity entity, out LiveEntityAnimationState animation))
|
||||
continue;
|
||||
|
||||
PrepareAnimation(record, animation);
|
||||
if (!TryGetCurrent(runtime, record, entity, animation))
|
||||
continue;
|
||||
|
||||
schedules.TryGetValue(entity.Id, out LiveEntityAnimationSchedule schedule);
|
||||
bool hasOrdinarySchedule = IsCurrentSchedule(runtime, schedule, record, entity, animation);
|
||||
IReadOnlyList<PartTransform>? sequenceFrames = hasOrdinarySchedule
|
||||
? schedule.SequenceFrames
|
||||
: null;
|
||||
bool composeParts = hasOrdinarySchedule && schedule.ComposeParts;
|
||||
float legacyAdvanceSeconds = hasOrdinarySchedule
|
||||
? schedule.LegacyAdvanceSeconds
|
||||
: 0f;
|
||||
ulong objectClockEpoch = record.ObjectClockEpoch;
|
||||
ulong projectionVersion = record.ProjectionMutationVersion;
|
||||
ulong presentationRevision = animation.PresentationRevision;
|
||||
|
||||
if (_staticFrames()?.TryTakeLivePartFrames(
|
||||
record,
|
||||
entity,
|
||||
animation,
|
||||
objectClockEpoch,
|
||||
projectionVersion,
|
||||
presentationRevision,
|
||||
out IReadOnlyList<PartTransform> staticPartFrames) == true)
|
||||
{
|
||||
if (!TryGetCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
animation,
|
||||
objectClockEpoch,
|
||||
projectionVersion,
|
||||
presentationRevision))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sequenceFrames = staticPartFrames;
|
||||
composeParts = true;
|
||||
}
|
||||
|
||||
if (animation.Sequencer is not null)
|
||||
{
|
||||
EmitSequenceDiagnostics(record, animation, sequenceFrames);
|
||||
}
|
||||
else
|
||||
{
|
||||
int span = animation.HighFrame - animation.LowFrame;
|
||||
bool hiddenLegacy = (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
|
||||
if (span <= 0 && !hiddenLegacy)
|
||||
continue;
|
||||
if (span > 0 && legacyAdvanceSeconds > 0f)
|
||||
{
|
||||
animation.CurrFrame += legacyAdvanceSeconds * animation.Framerate;
|
||||
if (animation.CurrFrame > animation.HighFrame)
|
||||
{
|
||||
float over = animation.CurrFrame - animation.LowFrame;
|
||||
animation.CurrFrame = animation.LowFrame + (over % (span + 1));
|
||||
}
|
||||
else if (animation.CurrFrame < animation.LowFrame)
|
||||
{
|
||||
animation.CurrFrame = animation.LowFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!composeParts
|
||||
|| !TryGetCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
animation,
|
||||
objectClockEpoch,
|
||||
projectionVersion))
|
||||
projectionVersion,
|
||||
presentationRevision))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sequenceFrames = staticPartFrames;
|
||||
composeParts = true;
|
||||
}
|
||||
|
||||
if (animation.Sequencer is not null)
|
||||
{
|
||||
EmitSequenceDiagnostics(record, animation, sequenceFrames);
|
||||
ComposeAndPublish(runtime, record, entity, animation, sequenceFrames,
|
||||
objectClockEpoch, projectionVersion, presentationRevision);
|
||||
}
|
||||
else
|
||||
{
|
||||
int span = animation.HighFrame - animation.LowFrame;
|
||||
bool hiddenLegacy = (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
|
||||
if (span <= 0 && !hiddenLegacy)
|
||||
continue;
|
||||
if (span > 0 && legacyAdvanceSeconds > 0f)
|
||||
{
|
||||
animation.CurrFrame += legacyAdvanceSeconds * animation.Framerate;
|
||||
if (animation.CurrFrame > animation.HighFrame)
|
||||
{
|
||||
float over = animation.CurrFrame - animation.LowFrame;
|
||||
animation.CurrFrame = animation.LowFrame + (over % (span + 1));
|
||||
}
|
||||
else if (animation.CurrFrame < animation.LowFrame)
|
||||
{
|
||||
animation.CurrFrame = animation.LowFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!composeParts
|
||||
|| !TryGetCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
animation,
|
||||
objectClockEpoch,
|
||||
projectionVersion))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ComposeAndPublish(runtime, record, entity, animation, sequenceFrames,
|
||||
objectClockEpoch, projectionVersion);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isPresenting = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +191,8 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
LiveEntityAnimationState animation,
|
||||
IReadOnlyList<PartTransform>? sequenceFrames,
|
||||
ulong objectClockEpoch,
|
||||
ulong projectionVersion)
|
||||
ulong projectionVersion,
|
||||
ulong presentationRevision)
|
||||
{
|
||||
int partCount = animation.PartTemplate.Count;
|
||||
EmitPartDiagnostics(record, animation, sequenceFrames, partCount);
|
||||
|
|
@ -224,11 +239,11 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
});
|
||||
}
|
||||
|
||||
if (!TryGetCurrent(runtime, record, entity, animation, objectClockEpoch, projectionVersion))
|
||||
if (!TryGetCurrent(runtime, record, entity, animation, objectClockEpoch, projectionVersion, presentationRevision))
|
||||
return;
|
||||
entity.MeshRefs = meshRefs;
|
||||
entity.SetIndexedPartPoses(rigidPoses, animation.PartAvailability);
|
||||
if (!TryGetCurrent(runtime, record, entity, animation, objectClockEpoch, projectionVersion))
|
||||
if (!TryGetCurrent(runtime, record, entity, animation, objectClockEpoch, projectionVersion, presentationRevision))
|
||||
return;
|
||||
_effectPoses.Publish(entity, rigidPoses, animation.PartAvailability);
|
||||
}
|
||||
|
|
@ -415,7 +430,8 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
entity,
|
||||
animation,
|
||||
schedule.ObjectClockEpoch,
|
||||
schedule.ProjectionMutationVersion);
|
||||
schedule.ProjectionMutationVersion,
|
||||
schedule.PresentationRevision);
|
||||
|
||||
private static bool TryGetCurrent(
|
||||
LiveEntityRuntime runtime,
|
||||
|
|
@ -448,8 +464,10 @@ internal sealed class LiveEntityAnimationPresenter
|
|||
WorldEntity entity,
|
||||
LiveEntityAnimationState animation,
|
||||
ulong objectClockEpoch,
|
||||
ulong projectionVersion) =>
|
||||
ulong projectionVersion,
|
||||
ulong presentationRevision) =>
|
||||
TryGetCurrent(runtime, record, entity, animation)
|
||||
&& record.ObjectClockEpoch == objectClockEpoch
|
||||
&& record.ProjectionMutationVersion == projectionVersion;
|
||||
&& record.ProjectionMutationVersion == projectionVersion
|
||||
&& animation.PresentationRevision == presentationRevision;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue