Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Stride.Math with the new .NET 7 System.Numerics interfaces #1570

Open
ykafia opened this issue Dec 3, 2022 · 5 comments
Open

[RFC] Stride.Math with the new .NET 7 System.Numerics interfaces #1570

ykafia opened this issue Dec 3, 2022 · 5 comments
Labels
enhancement New feature or request

Comments

@ykafia
Copy link
Contributor

ykafia commented Dec 3, 2022

Using the newly added abstract static method for interfaces we can reduce code repetition.
This shouldn't affect anything but number of lines of code for the vector implementations.

Some questions to answer before going forward with it :

  • Is it usable and comfortable ?
  • Is it viable performance wise ?
  • Can we do SIMD with it ?
  • Does it allow inlining ?

Implementation

using System;

namespace Stride.Maths

public struct Double2 : IVector2<Double2, double>
{
    public double X {get;set;}
    public double Y {get;set;}

    public Double2(double value)
    {
        X = value;
        Y = value;
    }
    public Double2(double x, double y)
    {
        X = x;
        Y = y;
    }

    public static double Distance(in Double2 value)
    {
        return (double)Math.Sqrt(value.X * value.X + value.Y * value.Y);
    }

    public static Double2 New(double value)
    {
        return new(value);
    }

    public static Double2 New(double x, double y)
    {
        return new(x,y);
    }

    public static void SquareRoot(in Double2 value, out Double2 result)
    {
        result = new((double)Math.Sqrt(value.X), (double)Math.Sqrt(value.Y));
    }
}

public struct Half2 : IVector2<Half2, Half>
{
    public Half X {get;set;}
    public Half Y {get;set;}

    public Half2(Half value)
    {
        X = value;
        Y = value;
    }
    public Half2(Half x, Half y)
    {
        X = x;
        Y = y;
    }

    public static Half Distance(in Half2 value)
    {
        return (Half)MathF.Sqrt((float)(value.X * value.X + value.Y * value.Y));
    }

    public static Half2 New(Half value)
    {
        return new(value);
    }

    public static Half2 New(Half x, Half y)
    {
        return new(x,y);
    }

    public static void SquareRoot(in Half2 value, out Half2 result)
    {
        result = new((Half)MathF.Sqrt((float)value.X), (Half)MathF.Sqrt((float)value.Y));
    }
}

Interface

using System.Numerics;

internal interface IVector2<T, Num>
    where T :
        struct,
        IVector2<T, Num>
    where Num : INumber<Num>
{
    public Num X { get; set; }
    public Num Y { get; set; }
    public T One => T.New(Num.One);
    public T Zero => T.New(Num.Zero);

    public static abstract T New(Num value);
    public static abstract T New(Num x, Num y);
    public static virtual void Abs(in T value, out T result)
    {
        result = T.New(Num.Abs(value.X), Num.Abs(value.Y));
    }

    public static virtual void Add(in T left, Num scalar, out T result)
    {
        result = T.New(
            left.X + scalar,
            left.Y + scalar
        );
    }
    public static virtual void Add(in T left, in T right, out T result)
    {
        result = T.New(
            left.X + right.X,
            left.Y + right.Y
        );
    }

    public static virtual void Clamp(in T value, in T min, in T max, out T result)
    {
        result = T.New(
            Num.MaxMagnitude(Num.MinMagnitude(value.X,max.X), min.X),
            Num.MaxMagnitude(Num.MinMagnitude(value.Y,max.Y), min.Y)
        );
    }
    public static abstract Num Distance(in T value);
    public static virtual Num DistanceSquared(in T value)
    {
        return value.X * value.X + value.Y * value.Y;
    }
    public static virtual void Divide(Num scalar, in T value, out T result)
    {
        result = T.New(
            scalar / value.X,
            scalar / value.Y
        );
    }
    public static virtual void Divide(in T value, Num scalar, out T result)
    {
        result = T.New(
            value.X / scalar,
            value.Y / scalar
        );
    }
    public static virtual void Divide(in T left, in T right, out T result)
    {
        result = T.New(
            left.X / right.X,
            left.Y / right.Y
        );
    }
    public static virtual Num Dot(in T left, in T right)
    {
        return left.X * right.X + left.Y * right.Y;
    }
    public static virtual void Lerp(in T value1, in T value2, Num amount, out T result)
    {
        T.Subtract(value2, value1, out var sub);
        T.Multiply(sub, amount, out var mul);
        T.Add(value1, mul, out result);
    }
    public static virtual void Multiply(in T value, Num scalar, out T result)
    {
        result = T.New(
            value.X * scalar,
            value.Y * scalar
        );
    }
    public static virtual void Multiply(in T left, in T right, out T result)
    {
        result = T.New(
            left.X * right.X,
            left.Y * right.Y
        );
    }
    public static virtual void Negate(in T value, out T result)
    {
        result = T.New(
            -value.X,
            -value.Y
        );
    }
    public static virtual void Normalize(in T value, out T result)
    {
        var x = value.X;
        var y = value.Y;
        result = T.New(
            x < -Num.One ? -Num.One : x > Num.One ? Num.One : x, 
            x < -Num.One ? -Num.One : x > Num.One ? Num.One : y 
        );
    }
    public static virtual void Reflect(in T vector, in T normal, out T result)
    {
        Num dot = T.Dot(vector,normal);
        result = T.New(
            vector.X - (Num.One + Num.One) * dot * normal.X,
            vector.Y - (Num.One + Num.One) * dot * normal.Y
        );
    }
    public static abstract void SquareRoot(in T value, out T result);
    
    public static virtual void Subtract(in T left, Num scalar, out T result)
    {
        result = T.New(
            left.X - scalar,
            left.Y - scalar
        );
    }
    public static virtual void Subtract(Num scalar, in T left, out T result)
    {
        result = T.New(
            scalar - left.X,
            scalar - left.Y
        );
    }
    public static virtual void Subtract(in T left, in T right, out T result)
    {
        result = T.New(
            left.X - right.X,
            left.Y - right.Y
        );
    }

    public static virtual void Transform(in T vector, in T transform, out T result)
    {
        T.Add(vector,transform, out result);
    }
    // public static virtual void TransformNormal(in T left, in T right, out T result);
}
@ykafia ykafia added the enhancement New feature or request label Dec 3, 2022
@manio143
Copy link
Member

manio143 commented Dec 4, 2022

I like this idea. But we definitely need to figure out a good shape of the interface (for example I don't like that distance is abstract but distance squared isn't - what if this vector is using a different distance measure?) and I think it should be public rather than internal.
Another thing - I don't remember what was the difference between Stride.Math and System. Numerics in terms of data shape/order but would it make sense to do a breaking change for the next release and move to Numerics types fully? Especially if they already have such static interfaces like this one.

From the performance standpoint - these methods are static. For each struct type a new specialized variant will be generated at runtime and inlining should apply as normal.
On the SIMD question - it will be used for the operations as it normally would. For batch SIMD operations (say on array of vectors) I don't know if JIT supports it or if it's something additional we'd need to implement.

@ykafia
Copy link
Contributor Author

ykafia commented Dec 4, 2022

For System.Numerics i think we have two things related to that discussion
#291
#1161

And for the abstract Distance function, i haven't yet found a way to call Math.Sqrt without having to cast the number to double, there doesn't seem to be any interface that allows this behavior. Will keep searching or we can wait an see what's going to come in .NET 8+

@manio143
Copy link
Member

manio143 commented Dec 4, 2022

Thanks for the reference to those issues.
Yeah, I would be definitely for using those static interfaces to simplify things.

Have you seen this reddit question? https://www.reddit.com/r/csharp/comments/ti11e1/can_i_cast_an_inumber_or_similar_to_systemdouble/ You can use double.CreateChecked to cast onto a double and feed it to the Sqrt and then Num.CreateTruncating to convert it back onto a Num or something like that. Though my quick check of the source code didn't find a conversion between Half and Double (maybe I wasn't looking in the right place)...

@manio143
Copy link
Member

manio143 commented Dec 4, 2022

Also some good discussion here dotnet/runtime#24168 and one of the recent comments suggested looking at Silk.NET.Maths

@ericwj
Copy link
Collaborator

ericwj commented Dec 8, 2022

Can I suggest not doing this unless it is part of a replacement for Stride.Math? The current implementation is up to at least 40x slower than necessary and the programming experience with it, and/or the availability of a large amount of duplicated operations is not quite right anymore and this also makes changing the code as opposed to rewriting a lot of work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants