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
|
|
@ -157,6 +157,36 @@ public sealed class LiveEntityAnimationPresenterTests
|
|||
Assert.Single(available);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SchedulePreparedBeforeAppearanceRebind_IsRejected()
|
||||
{
|
||||
var fixture = Build(partCount: 2);
|
||||
LiveEntityAnimationSchedule stale = Schedule(fixture,
|
||||
[
|
||||
new PartTransform(Vector3.UnitX, Quaternion.Identity),
|
||||
new PartTransform(Vector3.UnitY, Quaternion.Identity),
|
||||
]);
|
||||
var replacement = new Setup();
|
||||
replacement.Parts.Add(0x01000099u);
|
||||
replacement.DefaultScale.Add(Vector3.One);
|
||||
GameWindow.RebindAnimatedEntityForAppearance(
|
||||
fixture.State,
|
||||
fixture.Entity,
|
||||
replacement,
|
||||
1f,
|
||||
[new LiveAnimationPartTemplate(0x01000099u, null, true)],
|
||||
[true]);
|
||||
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), new Context());
|
||||
|
||||
presenter.Present(new Dictionary<uint, LiveEntityAnimationSchedule>
|
||||
{
|
||||
[fixture.Entity.Id] = stale,
|
||||
});
|
||||
|
||||
Assert.Empty(fixture.State.MeshRefsScratch);
|
||||
Assert.Empty(fixture.State.EffectPartPosesScratch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MotionDone_OldComponentCannotResolveReplacementByGuid()
|
||||
{
|
||||
|
|
@ -173,6 +203,22 @@ public sealed class LiveEntityAnimationPresenterTests
|
|||
Assert.Equal(0, context.ResolveCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MotionDone_SameComponentStillResolvesAfterObjectClockRebase()
|
||||
{
|
||||
var fixture = Build(partCount: 1, withSequencer: true);
|
||||
var context = new Context();
|
||||
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), context);
|
||||
presenter.PrepareAnimation(fixture.Record, fixture.State);
|
||||
|
||||
fixture.Record.SuspendObjectClock();
|
||||
fixture.Record.ResetObjectClockForEnterWorld(isStatic: false);
|
||||
fixture.State.Sequencer!.MotionDoneTarget!(0x10000001u, true);
|
||||
|
||||
Assert.Equal(1, context.ResolveCount);
|
||||
Assert.Same(fixture.Record, context.LastRecord);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectPoseCallback_NestedRuntimeSnapshotDoesNotTruncatePresentation()
|
||||
{
|
||||
|
|
@ -200,6 +246,120 @@ public sealed class LiveEntityAnimationPresenterTests
|
|||
Assert.Single(second.Entity.MeshRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectPoseCallback_ReplacesNextOwnerWithoutPublishingItsStaleSchedule()
|
||||
{
|
||||
var first = Build(partCount: 1, guid: Guid);
|
||||
var second = Add(first.Live, Guid + 1, partCount: 1);
|
||||
LiveEntityAnimationState replacement = State(second.Entity, 1, 1f, withSequencer: true);
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
bool replaced = false;
|
||||
poses.EffectPoseChanged += ownerId =>
|
||||
{
|
||||
if (ownerId != first.Entity.Id || replaced)
|
||||
return;
|
||||
replaced = true;
|
||||
Assert.True(first.Live.ClearAnimationRuntime(Guid + 1));
|
||||
first.Live.SetAnimationRuntime(Guid + 1, replacement);
|
||||
};
|
||||
var presenter = Presenter(first.Live, poses, new Context());
|
||||
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
|
||||
{
|
||||
[first.Entity.Id] = Schedule(first,
|
||||
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
|
||||
[second.Entity.Id] = Schedule(second,
|
||||
[new PartTransform(new Vector3(99f, 0f, 0f), Quaternion.Identity)]),
|
||||
};
|
||||
|
||||
presenter.Present(schedules);
|
||||
|
||||
Assert.True(replaced);
|
||||
Assert.Empty(replacement.MeshRefsScratch);
|
||||
Assert.Empty(replacement.EffectPartPosesScratch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectPoseCallback_RecursivePresentDoesNotTruncateOuterPass()
|
||||
{
|
||||
var first = Build(partCount: 1, guid: Guid);
|
||||
var second = Add(first.Live, Guid + 1, partCount: 1);
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
var presenter = Presenter(first.Live, poses, new Context());
|
||||
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
|
||||
{
|
||||
[first.Entity.Id] = Schedule(first,
|
||||
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]),
|
||||
[second.Entity.Id] = Schedule(second,
|
||||
[new PartTransform(Vector3.UnitY, Quaternion.Identity)]),
|
||||
};
|
||||
bool recursed = false;
|
||||
poses.EffectPoseChanged += _ =>
|
||||
{
|
||||
if (recursed)
|
||||
return;
|
||||
recursed = true;
|
||||
presenter.Present(schedules);
|
||||
};
|
||||
|
||||
presenter.Present(schedules);
|
||||
|
||||
Assert.True(recursed);
|
||||
Assert.Single(first.Entity.MeshRefs);
|
||||
Assert.Single(second.Entity.MeshRefs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectPoseCallback_RecursivePresentConsumesLegacyElapsedOnce()
|
||||
{
|
||||
var fixture = Build(partCount: 1);
|
||||
fixture.State.Sequencer = null;
|
||||
fixture.State.LowFrame = 0;
|
||||
fixture.State.HighFrame = 1;
|
||||
fixture.State.Framerate = 1f;
|
||||
fixture.State.CurrFrame = 0f;
|
||||
var animation = new Animation();
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var frame = new AnimationFrame(1);
|
||||
frame.Frames.Add(new Frame
|
||||
{
|
||||
Origin = new Vector3(i, 0f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
});
|
||||
animation.PartFrames.Add(frame);
|
||||
}
|
||||
fixture.State.Animation = animation;
|
||||
var schedule = new LiveEntityAnimationSchedule(
|
||||
SequenceFrames: null,
|
||||
LegacyAdvanceSeconds: 0.5f,
|
||||
ComposeParts: true,
|
||||
fixture.Record,
|
||||
fixture.Entity,
|
||||
fixture.State,
|
||||
fixture.Record.ObjectClockEpoch,
|
||||
fixture.Record.ProjectionMutationVersion,
|
||||
fixture.State.PresentationRevision);
|
||||
var schedules = new Dictionary<uint, LiveEntityAnimationSchedule>
|
||||
{
|
||||
[fixture.Entity.Id] = schedule,
|
||||
};
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
var presenter = Presenter(fixture.Live, poses, new Context());
|
||||
bool recursed = false;
|
||||
poses.EffectPoseChanged += _ =>
|
||||
{
|
||||
if (recursed)
|
||||
return;
|
||||
recursed = true;
|
||||
presenter.Present(schedules);
|
||||
};
|
||||
|
||||
presenter.Present(schedules);
|
||||
|
||||
Assert.True(recursed);
|
||||
Assert.Equal(0.5f, fixture.State.CurrFrame);
|
||||
}
|
||||
|
||||
private static LiveEntityAnimationPresenter Presenter(
|
||||
LiveEntityRuntime live,
|
||||
EntityEffectPoseRegistry poses,
|
||||
|
|
@ -229,7 +389,8 @@ public sealed class LiveEntityAnimationPresenterTests
|
|||
fixture.Entity,
|
||||
fixture.State,
|
||||
fixture.Record.ObjectClockEpoch,
|
||||
fixture.Record.ProjectionMutationVersion);
|
||||
fixture.Record.ProjectionMutationVersion,
|
||||
fixture.State.PresentationRevision);
|
||||
|
||||
private static Fixture Build(
|
||||
int partCount,
|
||||
|
|
@ -359,9 +520,11 @@ public sealed class LiveEntityAnimationPresenterTests
|
|||
{
|
||||
public uint LocalPlayerGuid => 0x50000001u;
|
||||
public int ResolveCount { get; private set; }
|
||||
public LiveEntityRecord? LastRecord { get; private set; }
|
||||
public MotionInterpreter? ResolveMotionInterpreter(LiveEntityRecord record)
|
||||
{
|
||||
ResolveCount++;
|
||||
LastRecord = record;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue