Editor/Inference/MhrBody.cs
using System;
using System.Threading;

namespace HumanoidMocapX.Inference;

/// <summary>Evaluates the MHR mesh and global joint states with identity,
/// expression and learned pose deformations. Owns one inference session's buffers.</summary>
internal sealed class MhrBody
{
    readonly MhrSkeleton skeleton;
    readonly MhrSkinning skinning;
    readonly MhrBlendShapes blendShapes;
    readonly MhrCorrectives correctives;
    readonly float[] parameters, jointParameters, unposed, corrections;
    internal int ModelParameters => skeleton.Parameters - blendShapes.Identities;
    internal int Identities => blendShapes.Identities;
    internal int Expressions => blendShapes.Expressions;
    internal int Joints => skeleton.Joints;
    internal int Vertices => skinning.Vertices;

    internal MhrBody(MhrSkeleton skeleton, MhrSkinning skinning, MhrBlendShapes blendShapes, MhrCorrectives correctives)
    {
        ArgumentNullException.ThrowIfNull(skeleton); ArgumentNullException.ThrowIfNull(skinning);
        ArgumentNullException.ThrowIfNull(blendShapes); ArgumentNullException.ThrowIfNull(correctives);
        if (skeleton.Joints != skinning.Joints || skeleton.Joints != correctives.Joints
            || skinning.Vertices != blendShapes.Vertices || skinning.Vertices * 3 != correctives.Coordinates || skeleton.Parameters <= blendShapes.Identities)
            throw new ArgumentException("MHR body components have incompatible dimensions.");
        this.skeleton = skeleton; this.skinning = skinning; this.blendShapes = blendShapes; this.correctives = correctives;
        parameters = new float[skeleton.Parameters]; jointParameters = new float[skeleton.Joints * MhrSkeleton.JointParameters];
        unposed = new float[skinning.Vertices * 3]; corrections = new float[unposed.Length];
    }

    /// <summary>Applies pose corrections only to the given vertices; the rest keep their
    /// blend-shape positions. Use only when consumers read just these vertices.</summary>
    internal void Restrict(ReadOnlySpan<int> vertices) => correctives.Restrict(vertices);

    /// <summary>The pose correctives, for sharing their projection with another body's (MhrCheckpoint.LoadBody).</summary>
    internal MhrCorrectives Correctives => correctives;

    /// <summary>Writes XYZ vertices and eight values per global joint state, in
    /// checkpoint units. Identity coefficients affect the mesh; their slots in the
    /// skeleton parameter vector stay zero. Rejects bad dimensions and aliasing.</summary>
    internal void Run(ReadOnlySpan<float> identity, ReadOnlySpan<float> model, ReadOnlySpan<float> expression,
        Span<float> vertices, Span<float> globalStates, CancellationToken cancellation = default)
    {
        cancellation.ThrowIfCancellationRequested();
        if (identity.Length != Identities || model.Length != ModelParameters || expression.Length != Expressions
            || vertices.Length != unposed.Length || globalStates.Length != Joints * MhrState.Elements)
            throw new ArgumentException("MHR body input/output dimensions do not match the model.");
        if (vertices.Overlaps(globalStates) || identity.Overlaps(vertices) || model.Overlaps(vertices) || expression.Overlaps(vertices)
            || identity.Overlaps(globalStates) || model.Overlaps(globalStates) || expression.Overlaps(globalStates))
            throw new ArgumentException("MHR body outputs must not overlap inputs or each other.");
        model.CopyTo(parameters);
        skeleton.Run(parameters, jointParameters, globalStates, cancellation);
        blendShapes.Run(identity, expression, unposed, cancellation);
        correctives.Run(jointParameters, corrections, cancellation);
        NeuralOps.AddInPlace(unposed, corrections);
        skinning.Run(globalStates, unposed, vertices, cancellation);
    }
}