refactor(animation): extract live PartArray presenter
Move final live part composition, exact-incarnation schedule handoff, MotionDone binding, and presentation diagnostics out of GameWindow while preserving the retail frame order. Reject stale schedule and completion ABA at the new owner boundary. Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
9760ff5a8d
commit
f004ac5562
14 changed files with 963 additions and 438 deletions
403
src/AcDream.App/Rendering/LiveEntityAnimationPresenter.cs
Normal file
403
src/AcDream.App/Rendering/LiveEntityAnimationPresenter.cs
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the final visual and rigid part channels after retail object and
|
||||
/// static-animation scheduling. It advances no time and drains no hooks.
|
||||
/// </summary>
|
||||
internal sealed class LiveEntityAnimationPresenter
|
||||
{
|
||||
private readonly Func<LiveEntityRuntime?> _liveEntities;
|
||||
private readonly Func<ILiveStaticPartFrameSource?> _staticFrames;
|
||||
private readonly EntityEffectPoseRegistry _effectPoses;
|
||||
private readonly ILiveAnimationPresentationContext _context;
|
||||
private readonly AnimationPresentationDiagnostics _diagnostics;
|
||||
private readonly int _hidePartIndex;
|
||||
private readonly List<LiveEntityRecord> _snapshot = new();
|
||||
|
||||
public LiveEntityAnimationPresenter(
|
||||
Func<LiveEntityRuntime?> liveEntities,
|
||||
Func<ILiveStaticPartFrameSource?> staticFrames,
|
||||
EntityEffectPoseRegistry effectPoses,
|
||||
ILiveAnimationPresentationContext context,
|
||||
AnimationPresentationDiagnostics diagnostics,
|
||||
int hidePartIndex)
|
||||
{
|
||||
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_staticFrames = staticFrames ?? throw new ArgumentNullException(nameof(staticFrames));
|
||||
_effectPoses = effectPoses ?? throw new ArgumentNullException(nameof(effectPoses));
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
||||
_hidePartIndex = hidePartIndex;
|
||||
}
|
||||
|
||||
public void PrepareAnimation(
|
||||
LiveEntityRecord record,
|
||||
LiveEntityAnimationState animation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(animation);
|
||||
LiveEntityRuntime? runtime = _liveEntities();
|
||||
if (runtime is null
|
||||
|| !runtime.IsCurrentSpatialAnimation(record, animation)
|
||||
|| !ReferenceEquals(record.WorldEntity, animation.Entity)
|
||||
|| animation.Sequencer is not { MotionDoneTarget: null } sequencer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WorldEntity capturedEntity = animation.Entity;
|
||||
sequencer.MotionDoneTarget = (motion, success) =>
|
||||
{
|
||||
LiveEntityRuntime? current = _liveEntities();
|
||||
if (current is null
|
||||
|| !current.IsCurrentSpatialAnimation(record, animation)
|
||||
|| !ReferenceEquals(record.WorldEntity, capturedEntity)
|
||||
|| !ReferenceEquals(animation.Entity, capturedEntity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MotionInterpreter? interpreter = _context.ResolveMotionInterpreter(record);
|
||||
interpreter?.MotionDone(motion, success);
|
||||
if (_diagnostics.DumpMotionEnabled)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[MOTIONDONE] guid={record.ServerGuid:X8} motion=0x{motion:X8} "
|
||||
+ $"success={success} pending={(interpreter?.MotionsPending() ?? false)}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void Present(
|
||||
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(schedules);
|
||||
LiveEntityRuntime? runtime = _liveEntities();
|
||||
if (runtime is null)
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (!TryGetCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
animation,
|
||||
objectClockEpoch,
|
||||
projectionVersion))
|
||||
{
|
||||
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))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ComposeAndPublish(runtime, record, entity, animation, sequenceFrames,
|
||||
objectClockEpoch, projectionVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private void ComposeAndPublish(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
LiveEntityAnimationState animation,
|
||||
IReadOnlyList<PartTransform>? sequenceFrames,
|
||||
ulong objectClockEpoch,
|
||||
ulong projectionVersion)
|
||||
{
|
||||
int partCount = animation.PartTemplate.Count;
|
||||
EmitPartDiagnostics(record, animation, sequenceFrames, partCount);
|
||||
List<MeshRef> meshRefs = animation.MeshRefsScratch;
|
||||
meshRefs.Clear();
|
||||
List<Matrix4x4> rigidPoses = animation.EffectPartPosesScratch;
|
||||
rigidPoses.Clear();
|
||||
Matrix4x4 scale = animation.Scale == 1f
|
||||
? Matrix4x4.Identity
|
||||
: Matrix4x4.CreateScale(animation.Scale);
|
||||
|
||||
for (int i = 0; i < partCount; i++)
|
||||
{
|
||||
ResolvePartFrame(animation, sequenceFrames, i, out Vector3 origin, out Quaternion orientation);
|
||||
Vector3 defaultScale = i < animation.Setup.DefaultScale.Count
|
||||
? animation.Setup.DefaultScale[i]
|
||||
: Vector3.One;
|
||||
Matrix4x4 visual = Matrix4x4.CreateScale(defaultScale)
|
||||
* Matrix4x4.CreateFromQuaternion(orientation)
|
||||
* Matrix4x4.CreateTranslation(origin);
|
||||
if (animation.Scale != 1f)
|
||||
visual *= scale;
|
||||
|
||||
Matrix4x4 rigid = Matrix4x4.CreateFromQuaternion(orientation)
|
||||
* Matrix4x4.CreateTranslation(origin * animation.Scale);
|
||||
rigidPoses.Add(rigid);
|
||||
|
||||
LiveAnimationPartTemplate template = animation.PartTemplate[i];
|
||||
if (!template.IsDrawable
|
||||
|| (_hidePartIndex >= 0 && i == _hidePartIndex && partCount >= 10))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
meshRefs.Add(new MeshRef(template.GfxObjId, visual)
|
||||
{
|
||||
SurfaceOverrides = template.SurfaceOverrides,
|
||||
});
|
||||
}
|
||||
|
||||
if (!TryGetCurrent(runtime, record, entity, animation, objectClockEpoch, projectionVersion))
|
||||
return;
|
||||
entity.MeshRefs = meshRefs;
|
||||
entity.SetIndexedPartPoses(rigidPoses, animation.PartAvailability);
|
||||
if (!TryGetCurrent(runtime, record, entity, animation, objectClockEpoch, projectionVersion))
|
||||
return;
|
||||
_effectPoses.Publish(entity, rigidPoses, animation.PartAvailability);
|
||||
}
|
||||
|
||||
private static void ResolvePartFrame(
|
||||
LiveEntityAnimationState animation,
|
||||
IReadOnlyList<PartTransform>? sequenceFrames,
|
||||
int partIndex,
|
||||
out Vector3 origin,
|
||||
out Quaternion orientation)
|
||||
{
|
||||
if (sequenceFrames is not null)
|
||||
{
|
||||
if (partIndex < sequenceFrames.Count)
|
||||
{
|
||||
origin = sequenceFrames[partIndex].Origin;
|
||||
orientation = sequenceFrames[partIndex].Orientation;
|
||||
}
|
||||
else
|
||||
{
|
||||
origin = Vector3.Zero;
|
||||
orientation = Quaternion.Identity;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int frameIndex = (int)Math.Floor(animation.CurrFrame);
|
||||
if (frameIndex < animation.LowFrame
|
||||
|| frameIndex > animation.HighFrame
|
||||
|| frameIndex >= animation.Animation.PartFrames.Count)
|
||||
{
|
||||
frameIndex = animation.LowFrame;
|
||||
}
|
||||
int nextIndex = frameIndex + 1;
|
||||
if (nextIndex > animation.HighFrame
|
||||
|| nextIndex >= animation.Animation.PartFrames.Count)
|
||||
{
|
||||
nextIndex = animation.LowFrame;
|
||||
}
|
||||
float t = Math.Clamp(animation.CurrFrame - frameIndex, 0f, 1f);
|
||||
var frames = animation.Animation.PartFrames[frameIndex].Frames;
|
||||
var nextFrames = animation.Animation.PartFrames[nextIndex].Frames;
|
||||
if (partIndex < frames.Count)
|
||||
{
|
||||
var first = frames[partIndex];
|
||||
var next = partIndex < nextFrames.Count ? nextFrames[partIndex] : first;
|
||||
origin = Vector3.Lerp(first.Origin, next.Origin, t);
|
||||
orientation = Quaternion.Slerp(first.Orientation, next.Orientation, t);
|
||||
}
|
||||
else
|
||||
{
|
||||
origin = Vector3.Zero;
|
||||
orientation = Quaternion.Identity;
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitSequenceDiagnostics(
|
||||
LiveEntityRecord record,
|
||||
LiveEntityAnimationState animation,
|
||||
IReadOnlyList<PartTransform>? sequenceFrames)
|
||||
{
|
||||
if (!_diagnostics.RemoteVelocityEnabled
|
||||
|| record.ServerGuid == 0
|
||||
|| record.ServerGuid == _context.LocalPlayerGuid
|
||||
|| record.RemoteMotionRuntime is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
double now = _diagnostics.Now();
|
||||
if (now - animation.LastSequenceDiagnosticTime <= 1d)
|
||||
return;
|
||||
AnimationSequencer sequencer = animation.Sequencer!;
|
||||
Console.WriteLine(
|
||||
$"[SEQSTATE] guid={record.ServerGuid:X8} CurrentMotion=0x{sequencer.CurrentMotion:X8} "
|
||||
+ $"CurrentSpeedMod={sequencer.CurrentSpeedMod:F3}");
|
||||
var node = sequencer.CurrentNodeDiag;
|
||||
int firstHash = sequencer.FirstCyclicAnimRefHash;
|
||||
Console.WriteLine(
|
||||
$"[CURRNODE] guid={record.ServerGuid:X8} animRef=0x{node.AnimRefHash:X8} "
|
||||
+ $"firstCyclicAnimRef=0x{firstHash:X8} "
|
||||
+ $"isOnCyclic={node.AnimRefHash == firstHash && firstHash != 0} "
|
||||
+ $"isLooping={node.IsLooping} fr={node.Framerate:F2} "
|
||||
+ $"frame=[{node.StartFrame}..{node.EndFrame}] "
|
||||
+ $"pos={node.FramePosition:F2} qCount={node.QueueCount}");
|
||||
animation.LastSequenceDiagnosticTime = now;
|
||||
}
|
||||
|
||||
private void EmitPartDiagnostics(
|
||||
LiveEntityRecord record,
|
||||
LiveEntityAnimationState animation,
|
||||
IReadOnlyList<PartTransform>? sequenceFrames,
|
||||
int partCount)
|
||||
{
|
||||
if (!_diagnostics.RemoteVelocityEnabled
|
||||
|| record.ServerGuid == 0
|
||||
|| record.ServerGuid == _context.LocalPlayerGuid
|
||||
|| record.RemoteMotionRuntime is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
double now = _diagnostics.Now();
|
||||
if (now - animation.LastPartDiagnosticTime <= 1d)
|
||||
return;
|
||||
int animationPartCount = animation.Animation is not null
|
||||
&& animation.Animation.PartFrames.Count > 0
|
||||
? animation.Animation.PartFrames[0].Frames.Count
|
||||
: -1;
|
||||
double hash = 0d;
|
||||
if (sequenceFrames is not null)
|
||||
{
|
||||
foreach (PartTransform frame in sequenceFrames)
|
||||
{
|
||||
hash += frame.Origin.X + frame.Origin.Y + frame.Origin.Z
|
||||
+ frame.Orientation.X + frame.Orientation.Y
|
||||
+ frame.Orientation.Z + frame.Orientation.W;
|
||||
}
|
||||
}
|
||||
Console.WriteLine(
|
||||
$"[PARTSDIAG] guid={record.ServerGuid:X8} pt.Count={partCount} "
|
||||
+ $"seqFrames.Count={sequenceFrames?.Count ?? -1} "
|
||||
+ $"setup.Parts.Count={animation.Setup.Parts.Count} "
|
||||
+ $"anim.PartFrames[0].Frames.Count={animationPartCount} seqHash={hash:F4}");
|
||||
animation.LastPartDiagnosticTime = now;
|
||||
}
|
||||
|
||||
private static bool IsCurrentSchedule(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityAnimationSchedule schedule,
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
LiveEntityAnimationState animation) =>
|
||||
schedule.ComposeParts
|
||||
&& ReferenceEquals(schedule.Record, record)
|
||||
&& ReferenceEquals(schedule.Entity, entity)
|
||||
&& ReferenceEquals(schedule.Animation, animation)
|
||||
&& TryGetCurrent(
|
||||
runtime,
|
||||
record,
|
||||
entity,
|
||||
animation,
|
||||
schedule.ObjectClockEpoch,
|
||||
schedule.ProjectionMutationVersion);
|
||||
|
||||
private static bool TryGetCurrent(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord record,
|
||||
out WorldEntity entity,
|
||||
out LiveEntityAnimationState animation)
|
||||
{
|
||||
WorldEntity? currentEntity = record.WorldEntity;
|
||||
LiveEntityAnimationState? currentAnimation =
|
||||
record.AnimationRuntime as LiveEntityAnimationState;
|
||||
entity = currentEntity!;
|
||||
animation = currentAnimation!;
|
||||
return currentEntity is not null
|
||||
&& currentAnimation is not null
|
||||
&& TryGetCurrent(runtime, record, entity, animation);
|
||||
}
|
||||
|
||||
private static bool TryGetCurrent(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
LiveEntityAnimationState animation) =>
|
||||
runtime.IsCurrentSpatialAnimation(record, animation)
|
||||
&& ReferenceEquals(record.WorldEntity, entity)
|
||||
&& ReferenceEquals(animation.Entity, entity);
|
||||
|
||||
private static bool TryGetCurrent(
|
||||
LiveEntityRuntime runtime,
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
LiveEntityAnimationState animation,
|
||||
ulong objectClockEpoch,
|
||||
ulong projectionVersion) =>
|
||||
TryGetCurrent(runtime, record, entity, animation)
|
||||
&& record.ObjectClockEpoch == objectClockEpoch
|
||||
&& record.ProjectionMutationVersion == projectionVersion;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue