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
|
|
@ -671,23 +671,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
public System.Numerics.Vector3 PrevServerPos;
|
||||
public double PrevServerPosTime;
|
||||
public double LastOmegaDiagLogTime;
|
||||
/// <summary>
|
||||
/// Diagnostic-only (gated on <c>ACDREAM_REMOTE_VEL_DIAG=1</c>): own
|
||||
/// throttle clock for the SEQSTATE log line in TickAnimations.
|
||||
/// Previously SEQSTATE shared <see cref="LastOmegaDiagLogTime"/> with
|
||||
/// the OMEGA_DIAG block, which fires at 0.5s and resets the clock —
|
||||
/// any remote that turned during a transition silently swallowed
|
||||
/// SEQSTATE for 0.5–1.5s, masking the bug we're trying to diagnose
|
||||
/// (walk↔run leg-cycle sticking on observed retail chars). Split
|
||||
/// 2026-05-03 (Commit A).
|
||||
/// </summary>
|
||||
public double LastSeqStateLogTime;
|
||||
/// <summary>
|
||||
/// Diagnostic-only (gated on <c>ACDREAM_REMOTE_VEL_DIAG=1</c>): own
|
||||
/// throttle clock for the PARTSDIAG log line in TickAnimations
|
||||
/// (D5). One log per remote per ~1s.
|
||||
/// </summary>
|
||||
public double LastPartsDiagLogTime;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic-only: maximum scaled CSequence root-motion speed
|
||||
/// observed since the last UpdatePosition arrival. The next UP
|
||||
|
|
@ -4539,7 +4523,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
});
|
||||
_staticAnimationScheduler?.BindLiveOwner(
|
||||
entity,
|
||||
staticSequencer,
|
||||
registeredAnimation,
|
||||
staticBody);
|
||||
}
|
||||
}
|
||||
|
|
@ -11540,7 +11524,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
DatReaderWriter.Types.Frame rootFrame = ae.RootMotionScratch;
|
||||
rootFrame.Origin = System.Numerics.Vector3.Zero;
|
||||
rootFrame.Orientation = System.Numerics.Quaternion.Identity;
|
||||
ae.PreparedSequenceFrames = sequencer.Advance(dt, rootFrame);
|
||||
ae.PreparedSequenceFrames = ae.CaptureSequenceFrames(
|
||||
sequencer.Advance(dt, rootFrame));
|
||||
ae.SequenceAdvancedBeforeAnimationPass = true;
|
||||
|
||||
output.Origin = rootFrame.Origin;
|
||||
|
|
|
|||
|
|
@ -17,5 +17,6 @@ internal interface ILiveStaticPartFrameSource
|
|||
LiveEntityAnimationState animation,
|
||||
ulong objectClockEpoch,
|
||||
ulong projectionMutationVersion,
|
||||
ulong presentationRevision,
|
||||
out IReadOnlyList<PartTransform> frames);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,13 +139,18 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
projectile,
|
||||
objectClockEpoch))
|
||||
{
|
||||
IReadOnlyList<PartTransform>? ownedFrames = schedule.SequenceFrames is { } produced
|
||||
? animation.CaptureScheduleFrames(produced)
|
||||
: null;
|
||||
_schedules[entity.Id] = schedule with
|
||||
{
|
||||
SequenceFrames = ownedFrames,
|
||||
Record = record,
|
||||
Entity = entity,
|
||||
Animation = animation,
|
||||
ObjectClockEpoch = objectClockEpoch,
|
||||
ProjectionMutationVersion = record.ProjectionMutationVersion,
|
||||
PresentationRevision = animation.PresentationRevision,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -217,7 +222,9 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
}
|
||||
|
||||
return new LiveEntityAnimationSchedule(
|
||||
prepared ?? (composeHidden ? sequencer?.SampleCurrentPose() : null),
|
||||
prepared ?? (composeHidden && sequencer is not null
|
||||
? animation?.CaptureSequenceFrames(sequencer.SampleCurrentPose())
|
||||
: null),
|
||||
0f,
|
||||
ComposeParts: advanced || composeHidden);
|
||||
}
|
||||
|
|
@ -311,7 +318,9 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
rootFrame.Origin = Vector3.Zero;
|
||||
rootFrame.Orientation = Quaternion.Identity;
|
||||
if (sequencer is not null)
|
||||
frames = sequencer.Advance(quantum, rootFrame);
|
||||
frames = animation?.CaptureSequenceFrames(
|
||||
sequencer.Advance(quantum, rootFrame))
|
||||
?? sequencer.Advance(quantum, rootFrame);
|
||||
else if (animation is not null)
|
||||
legacyElapsed += quantum;
|
||||
|
||||
|
|
@ -441,7 +450,9 @@ internal sealed class LiveEntityAnimationScheduler
|
|||
if (hidden)
|
||||
{
|
||||
return new LiveEntityAnimationSchedule(
|
||||
sequencer?.SampleCurrentPose(),
|
||||
sequencer is not null && animation is not null
|
||||
? animation.CaptureSequenceFrames(sequencer.SampleCurrentPose())
|
||||
: null,
|
||||
0f,
|
||||
ComposeParts: animation is not null);
|
||||
}
|
||||
|
|
@ -531,4 +542,5 @@ internal readonly record struct LiveEntityAnimationSchedule(
|
|||
WorldEntity? Entity = null,
|
||||
LiveEntityAnimationState? Animation = null,
|
||||
ulong ObjectClockEpoch = 0UL,
|
||||
ulong ProjectionMutationVersion = 0UL);
|
||||
ulong ProjectionMutationVersion = 0UL,
|
||||
ulong PresentationRevision = 0UL);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ internal sealed class LiveEntityAnimationState : ILiveEntityAnimationRuntime
|
|||
public bool SequenceAdvancedBeforeAnimationPass;
|
||||
public readonly Frame RootMotionScratch = new();
|
||||
public readonly MotionDeltaFrame RootMotionDeltaScratch = new();
|
||||
public readonly List<PartTransform> SequenceFramesScratch = new();
|
||||
public readonly List<PartTransform> ScheduleFramesScratch = new();
|
||||
|
||||
/// <summary>Reusable compact drawable channel, consumed before draw.</summary>
|
||||
public readonly List<MeshRef> MeshRefsScratch = new();
|
||||
|
|
@ -45,16 +47,40 @@ internal sealed class LiveEntityAnimationState : ILiveEntityAnimationRuntime
|
|||
/// </summary>
|
||||
public readonly List<Matrix4x4> VisualPartPosesScratch = new();
|
||||
public bool PresentationPosesInitialized;
|
||||
public ulong PresentationRevision { get; private set; } = 1UL;
|
||||
|
||||
public double LastSequenceDiagnosticTime;
|
||||
public double LastPartDiagnosticTime;
|
||||
|
||||
public void InvalidatePresentationPoses()
|
||||
{
|
||||
PresentationRevision++;
|
||||
if (PresentationRevision == 0UL)
|
||||
PresentationRevision++;
|
||||
PresentationPosesInitialized = false;
|
||||
VisualPartPosesScratch.Clear();
|
||||
EffectPartPosesScratch.Clear();
|
||||
}
|
||||
|
||||
public IReadOnlyList<PartTransform> CaptureSequenceFrames(
|
||||
IReadOnlyList<PartTransform> source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
SequenceFramesScratch.Clear();
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
SequenceFramesScratch.Add(source[i]);
|
||||
return SequenceFramesScratch;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PartTransform> CaptureScheduleFrames(
|
||||
IReadOnlyList<PartTransform> source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ScheduleFramesScratch.Clear();
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
ScheduleFramesScratch.Add(source[i]);
|
||||
return ScheduleFramesScratch;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly record struct LiveAnimationPartTemplate(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -248,10 +248,9 @@ public sealed class AnimationSequencer
|
|||
// allocation instead of one `new PartTransform[partCount]` per Advance()
|
||||
// call (every animated entity, every tick, including idle NPCs on a
|
||||
// breathe cycle). BuildBlendedFrame/BuildIdentityFrame overwrite every
|
||||
// slot before returning this array, so the values are bit-identical to
|
||||
// the old fresh-array version; callers (GameWindow.TickAnimations)
|
||||
// consume the returned IReadOnlyList<PartTransform> synchronously within
|
||||
// the same loop iteration and never cache it across Advance() calls.
|
||||
// authored slot before returning this view. The view is borrowed only for
|
||||
// the producing call: App schedulers copy its authored prefix into their
|
||||
// incarnation-owned handoff buffer before any callback or later phase.
|
||||
private readonly PartTransform[] _partTransformScratch;
|
||||
private readonly PartTransformBuffer _partTransformView;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue